diff --git a/.gitignore b/.gitignore index 50e96e73..225064a7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pip-log.txt .coverage .tox nosetests.xml +.noseids # Translations *.mo @@ -33,4 +34,5 @@ uweb3-venv features *.vscode *.log -*.out \ No newline at end of file +*.out +scaffold/ \ No newline at end of file diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 87031f30..38253df4 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -3,6 +3,7 @@ uWeb was created by: - Jan Klopper - Arjen Pander - Elmer de Looff +- Stef van Houten And has had code contributions from: diff --git a/DEVELOPMENT b/DEVELOPMENT new file mode 100644 index 00000000..7625b72e --- /dev/null +++ b/DEVELOPMENT @@ -0,0 +1,26 @@ +Development on µWeb3 follows the pep8 rules + +# Development environment. +You can setup a working development environment by issueing: + +```bash +python3 setup.py develop --user + +# clone the uweb3scaffold project to get started +git clone git@github.com:underdarknl/uweb3scaffold.git +cd uweb3scaffold + +python3 serve.py +``` + +This will setup a local + + +# Coding conventions: + +* Tabs are two spaces. +* Each method and class is required to contain a docstring +* Text files outside the python scope are written in markdown + +Each File has an __author__ variable, in which you can list your name and email address if you whish to do so. +You can do a pull request on CONTRIBUTORS to have your name added. diff --git a/LICENSE b/LICENSE index 7d2ba457..f288702d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,674 @@ -Copyright (c) 2012, Underdark + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + 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. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + 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/MANIFEST.in b/MANIFEST.in index 1217d068..aeadd4b5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.md LICENSE -recursive-include uweb3 *.js *.css *.html *.conf *.txt *.utp -recursive-include uweb3/ext_lib *.py +include README.md LICENSE DEVELOPMENT CONTRIBUTORS requirements.txt +recursive-include uweb3 *.html *.conf *.py +recursive-include test *.py diff --git a/README.md b/README.md index 080dae25..9c00c233 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Since µWeb inception we have used it for many projects, and while it did its job, there were plenty of rough edges. This new version intends to remove those and pull it into the current age. +µWeb3 is free software, distributed under the terms of the [GNU] General Public License as published by the Free Software Foundation, version 3 of the License (or any later version). For more information, see the file LICENSE + # Notable changes * wsgi complaint interface @@ -15,12 +17,13 @@ Since µWeb inception we have used it for many projects, and while it did its jo The following example applications for uWeb3 exist: -* [uWeb3-info](https://github.com/edelooff/uWeb3-info): This demonstrates most µWeb3 features, and gives you examples on how to use most of them. -* [uWeb3-logviewer](https://github.com/edelooff/uWeb3-logviewer): This allows you to view and search in the logs generated by all µWeb and µWeb3 applications. +* [uWeb3-scaffold](https://github.com/underdarknl/uweb3scaffold): This is an empty project which you can fork to start your own website # µWeb3 installation -The easiest and quickest way to install µWeb3 is using Python's `virtualenv`. Install using the setuptools installation script, which will automatically gather dependencies. +The easiest and quickest way to install µWeb3 is by running pip3 install uwebthree. + +For a development version using Python's `virtualenv`. Install using the setuptools installation script, which will automatically gather dependencies. ```bash # Set up the Python3 virtualenv @@ -31,9 +34,11 @@ source env/bin/activate python3 setup.py install # Or you can install in development mode which allows easy modification of the source: -python3 setup.py develop +python3 setup.py develop --user -cd uweb3/scaffold +# clone the uweb3scaffold project to get started +git clone git@github.com:underdarknl/uweb3scaffold.git +cd uweb3scaffold python3 serve.py ``` @@ -64,159 +69,32 @@ database = 'dbname' ``` To access your database connection simply use the connection attribute in any class that inherits from PageMaker. -# Config settings -If you are working on µWeb3 core make sure to enable the following setting in the config: -``` -[development] -dev = True -``` -This makes sure that µWeb3 restarts every time you modify something in the core of the framework aswell. - -µWeb3 has inbuild XSRF protection. You can import it from uweb3.pagemaker.new_decorators checkxsrf. -This is a decorator and it will handle validation and generation of the XSRF. -The only thing you have to do is add the ```{{ xsrf [xsrf]}}``` tag into a form. -The xsrf token is accessible in any pagemaker with self.xsrf. - # Routing The default way to create new routes in µWeb3 is to create a folder called routes. In the routes folder create your pagemaker class of choice, the name doesn't matter as long as it inherits from PageMaker. After creating your pagemaker be sure to add the route endpoint to routes list in base/__init__.py. -# New +# New since v3 - In uweb3 __init__ a class called HotReload - In pagemaker __init__: - A classmethod called loadModules that loads all pagemaker modules inheriting from PageMaker class - A XSRF class - Generates a xsrf token and creates a cookie if not in place - - Validates the xsrf token in a post request if the enable_xsrf flag is set in the config.ini - In requests: - Self.method attribute - self.post.form attribute. This is the post request as a dict, includes blank values. - - Method called Redirect #Moved from the response class to the request class so cookies that are set before a redirect are actually set. + - Method called Redirect #Moved from the response class to the request class so cookies that are set before a redirect are actually persist to the next request. - Method called DeleteCookie - - A if statement that checks string like cookies and raises an error if the size is equal or bigger than 4096 bytes. - - AddCookie method, edited this and the response class to handle the setting of multiple cookies. Previously setting multiple cookies with the Set-Cookie header would make the last cookie the only cookie. -- In pagemaker/new_login Users class: - - Create user - - Find user by name - - Create a cookie with userID + secret - - Validate if user messed with given cookie and render it useless if so -- In pagemaker/new_decorators: + - An if statement that checks string like cookies and raises an error if the size is equal or bigger than 4096 bytes. + - AddCookie method, now supports multiple calls to Set-Cookie setting all cookies instead of just the last. +- In pagemaker/decorators: - Loggedin decorator that validates if user is loggedin based on cookie with userid - Checkxsrf decorator that checks if the incorrect_xsrf_token flag is set - In templatepaser: - - A function called _TemplateConstructXsrf that generates a hidden input field with the supplied value: {{ xsrf [xsrf_variable]}} -- In libs/sqltalk - - Tried to make sqltalk python3 compatible by removing references to: long, unicode and basestring - - So far so good but it might crash on functions that I didn't use yet - - -# Login validation -Instead of using sessions to keep track of logged in users µWeb3 uses secure cookies. So how does this work? -When a user logs in for the first time there is no cookie in place, to set one we go through the normal process of validating a user and loggin in. - -To create a secure cookie inherit from the Model.SecureCookie. The SecureCookie class has a few build in methods, Create, Update and Delete. -To create a new cookie make use of the `Create` method, it works the same ass the AddCookie method. - -If you want to see which cookies are managed by the SecureCookie class you can call the session attribute. -The session attribute decodes all managed cookies and can be used to read them. - -# SQLAlchemy -SQLAlchemy is available in uWeb3 by using the SqAlchemyPageMaker instead of the regular pagemaker. -SQLAlchemy comes with most of the methods that are available in the default model.Record class, however because SQLAlchemy works like an ORM -there are some adjustments. Instead of inheriting from dict the SQLAlchemy model.Record inherits from object, meaning you can no longer use -dict like functions such as get and set. Instead the model is accessible by the columns defined in the class you want to create. - -The SQLAlchemy model.Record class makes use of the session attribute accessible in the SqAlchemyPageMaker. - -The session keeps track of all queries to the database and comes with some usefull features. - -An example of a few usefull features: -`session.new`: The set of all instances marked as ‘new’ within this Session. -`session.dirty`: Instances are considered dirty when they were modified but not deleted. -`session.deleted`: The set of all instances marked as ‘deleted’ within this Session -the rest can be found at https://docs.sqlalchemy.org/en/13/orm/session_api.html - -Objects in the session will only be updated/created in the actual database on session.commit()/session.flush(). - -Defining classes that represent a table is different from how we used to do it in uWeb2. -SQLAlchemy requires you to define all columns from the table that you want to use. -For example, creating a class that represents the user table could look like this: - -``` -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - -class User(Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) -``` -We can now use this class to query our users table in the SqAlchemyPageMaker to get the user with id 1: -`self.session.query(User).filter(User.id == 1).first() ` -or to list all users: -`self.session.query(User).all()` -uWeb3's SQLAlchemy model.Record has almost the same functionality as uWeb3's regular model.Record so we can simplify our code to this: - -``` -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - -#Notice how we load in the uweb3.model.AlchemyRecord class to gain access to all sorts of functionality -class User(uweb3.model.AlchemyRecord, Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) -``` -We can now query the users table like this: -``` -User.FromPrimary(self.session, 1) ->>> User({'id': 1, 'username': 'username', 'password': 'password'}) -``` -Or to get a list of all users: -``` -User.List(self.session, conditions=[User.id <= 2]) ->>> [ - User({'id': 1, 'username': 'name', 'password': 'password'}), - User({'id': 2, 'username': 'user2', 'password': 'password'}) - ] -``` - -Now if we want to automatically load related tables we can set it up like this: - -``` -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship - -Base = declarative_base() - -class User(uweb3.model.AlchemyRecord, Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) - userinfoid = Column('userinfoid', Integer, ForeignKey('UserInfo.id')) - userdata = relationship("UserInfo", lazy="select") - - def __init__(self, *args, **kwargs): - super(User, self).__init__(*args, **kwargs) - -class UserInfo(uweb3.model.AlchemyRecord, Base): - __tablename__ = 'UserInfo' - - id = Column(Integer, primary_key=True) - name = Column(String, unique=True) -``` -Now the UserInfo table will be loaded on the `userinfoid` attribute, but only after we try and access -this key a seperate query is send to retrieve the related information. -SQLAlchemy's lazy loading is fast but should be avoided while in loops. Take a look at SQLAlchemys documentation for optimal use. + - Its possible to register tags to the parser, for example in your _postInit call + - Its possible to register 'Just in Time' tags to the parser, which will be evaluated only when needed. +- In libs/sqltalk, use of PyMysql instead of c mysql functions +- Connections + - All Connections are now all availabe on the self.connections member of the pagemaker, regardless of what type of backend they connect to + - Cookies (signed and safe) are available as a connection + - Config files (read/write) are available as a connection diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..03d26900 --- /dev/null +++ b/pylintrc @@ -0,0 +1,311 @@ +# lint Python modules using external checkers. +# +# This is the main checker controling the other ones and the reports +# generation. It is itself both a raw checker and an astng checker in order +# to: +# * handle message activation / deactivation at the module level +# * handle some basic but necessary stats'data (number of classes, methods...) +# +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# Set the cache size for astng objects. +cache-size=500 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable only checker(s) with the given id(s). This option conflicts with the +# disable-checker option +#enable-checker= + +# Enable all checker(s) except those with the given id(s). This option +# conflicts with the enable-checker option +#disable-checker= + +# Enable all messages in the listed categories. +#enable-msg-cat= + +# Disable all messages in the listed categories. +#disable-msg-cat= + +# Enable the message(s) with the given id(s). +#enable-msg= + +# Disable the message(s) with the given id(s). +# W0142: Used * or ** magic -- this is in fact a very Pythonic approach. +# W0403: Relative import %r -- Underdark tree structure demands this. +# E1103: %s %r has no %r member (but some types could not be inferred) +# This is generally not an actual problem, and causes false positives +disable-msg=W0403, W0142, E1103 +disable=W0403, W0142, E1103 + + +[REPORTS] + +# set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=colorized + +# Include message's id in output +include-ids=yes + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells wether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note).You have access to the variables errors warning, statement which +# respectivly contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (R0004). +evaluation=10.0 - (5.0 * error + warning + refactor + convention) / statement * 10 + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (R0004). +comment=yes + +# Enable the report(s) with the given id(s). +#enable-report= + +# Disable the report(s) with the given id(s). +# R0801 is the "similar lines" report, which is not used. +disable-report=R0801 + + +# checks for +# * unused variables / imports +# * undefined variables +# * redefinition of variable from builtins or from an outer scope +# * use of variable before assigment +# +[VARIABLES] + +# Tells wether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching names used for dummy variables (i.e. not used). +dummy-variables-rgx=_.*$ + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +# try to find bugs in the code using type inference +# +[TYPECHECK] + +# Tells wether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# When zope mode is activated, consider the acquired-members option to ignore +# access to some undefined attributes. +zope=no + +# List of members which are usually get through zope's acquisition mecanism and +# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). +acquired-members=REQUEST,acl_users,aq_parent + + +# checks for : +# * doc strings +# * modules / classes / functions / methods / arguments / variables name +# * number of arguments, local variables, branchs, returns and statements in +# functions, methods +# * required module attributes +# * dangerous default values as arguments +# * redefinition of function / method / class +# * uses of the global statement +# +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes=__author__, __version__ + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=_.*$ + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=([A-Z_][a-zA-Z0-9_]{2,30})|(main)$ + +# Regular expression which should only match correct method names +method-rgx=([a-zA-Z_][a-zA-Z0-9_]{2,30})|(test.*)$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,n + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar + +# List of builtins function names that should not be used, separated by a comma +bad-functions=filter,apply,input + + +# checks for +# * external modules dependencies +# * relative / wildcard imports +# * cyclic imports +# * uses of deprecated modules +# +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report R0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report R0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report R0402 must +# not be disabled) +int-import-graph= + + +# checks for : +# * methods without self as first argument +# * overridden methods signature +# * access only to existant members via self +# * attributes not defined in the __init__ method +# * supported interfaces implementation +# * unreachable code +# +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods= + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp,run + + +# checks for sign of poor/misdesign: +# * number of methods, attributes, local variables... +# * size, complexity of functions, methods +# +[DESIGN] + +# Maximum number of arguments for function / method +max-args=8 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=20 + +# Maximum number of statements in function / method body +max-statements=40 + +# Maximum number of parents for a class (see R0901). +max-parents=12 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +# checks for: +# * warning notes in the code like FIXME, XXX +# * PEP 263: source code with non ascii character but no encoding declaration +# +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +# checks for : +# * unauthorized constructions +# * strict indentation +# * line length +# * use of <> instead of != +# +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +# checks for similarities and duplicated code. This computation may be +# memory / CPU intensive, so you should disable it if you experiments some +# problems. +# +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..374b58cb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..3c23c9a2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +PyMySQL==1.0.2 +pytz==2021.3 +python-magic==0.4.24 +python-socketio==5.5.1 diff --git a/setup.py b/setup.py index bb82a251..6451b3aa 100644 --- a/setup.py +++ b/setup.py @@ -1,51 +1,48 @@ +#!/usr/bin/python3 """uWeb3 installer.""" import os import re from setuptools import setup, find_packages -REQUIREMENTS = [ - 'decorator', - 'PyMySQL', - 'python-magic', - 'python3-openid', - 'pytz', - 'simplejson', - 'sqlalchemy', - 'bcrypt', - 'werkzeug', - 'mysqlclient', -] +def Requirements(): + """Returns the contents of the Requirements.txt file.""" + with open(os.path.join(os.path.dirname(__file__), 'requirements.txt')) as r_file: + return r_file.read() -def description(): +def Description(): + """Returns the contents of the README.md file as description information.""" with open(os.path.join(os.path.dirname(__file__), 'README.md')) as r_file: return r_file.read() - -def version(): +def Version(): + """Returns the version of the library as read from the __init__.py file""" main_lib = os.path.join(os.path.dirname(__file__), 'uweb3', '__init__.py') with open(main_lib) as v_file: return re.match(".*__version__ = '(.*?)'", v_file.read(), re.S).group(1) setup( - name='uWeb3 test', - version=version(), + name='uWebthree', + version=Version(), description='uWeb, python3, uswgi compatible micro web platform', - long_description=description(), + long_description=Description(), long_description_content_type='text/markdown', license='ISC', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', + 'Environment :: Web Environment', 'License :: OSI Approved :: ISC License (ISCL)', 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', ], author='Jan Klopper', author_email='jan@underdark.nl', - url='https://github.com/underdark.nl/uWeb3', - keywords='minimal web framework', + url='https://github.com/underdark.nl/uweb3', + keywords='minimal python web framework', packages=find_packages(), include_package_data=True, - zip_safe=False, - install_requires=REQUIREMENTS) \ No newline at end of file + install_requires=Requirements(), + python_requires='>=3.5') diff --git a/tables.sql b/tables.sql deleted file mode 100644 index f4c008e1..00000000 --- a/tables.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE users( - id INTEGER AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL -); \ No newline at end of file diff --git a/uweb3/test_model.py b/test/test_model.py old mode 100644 new mode 100755 similarity index 99% rename from uweb3/test_model.py rename to test/test_model.py index cdd8a9ec..1cc44522 --- a/uweb3/test_model.py +++ b/test/test_model.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """Test suite for the database abstraction module (model).""" # Too many public methods @@ -8,7 +8,7 @@ import unittest # Importing uWeb3 makes the SQLTalk library available as a side-effect -from uweb3.ext_lib.underdark.libs.sqltalk import mysql +from uweb3.libs.sqltalk import mysql # Unittest target from uweb3 import model from pymysql.err import InternalError @@ -435,7 +435,7 @@ def DatabaseConnection(): passwd='24192419', db='uweb_test', charset='utf8') - + if __name__ == '__main__': diff --git a/uweb3/test_model_alchemy.py b/test/test_model_alchemy.py old mode 100644 new mode 100755 similarity index 98% rename from uweb3/test_model_alchemy.py rename to test/test_model_alchemy.py index 3923e23e..063f728a --- a/uweb3/test_model_alchemy.py +++ b/test/test_model_alchemy.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """Test suite for the database abstraction module (model).""" # Too many public methods @@ -6,11 +6,8 @@ # Standard modules import unittest -from contextlib import contextmanager -import pymysql import sqlalchemy -from pymysql.err import InternalError from sqlalchemy import (Column, ForeignKey, Integer, MetaData, String, Table, create_engine) from sqlalchemy.exc import IntegrityError, OperationalError @@ -19,7 +16,7 @@ import uweb3 from uweb3.alchemy_model import AlchemyRecord -from uweb3.ext_lib.underdark.libs.sqltalk import mysql +from uweb3.libs.sqltalk import mysql # ############################################################################## # Record classes for testing diff --git a/uweb3/test_request.py b/test/test_request.py old mode 100644 new mode 100755 similarity index 97% rename from uweb3/test_request.py rename to test/test_request.py index 1967cfef..2bcea9ff --- a/uweb3/test_request.py +++ b/test/test_request.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/python3 +# -*- coding: utf-8 -*- """Tests for the request module.""" # Method could be a function @@ -17,7 +18,7 @@ import urllib # Unittest target -from . import request +from uweb3 import request class IndexedFieldStorageTest(unittest.TestCase): @@ -36,7 +37,6 @@ def testEmptyStorage(self): def testBasicStorage(self): """A basic IndexedFieldStorage has the proper key + value pair""" ifs = self.CreateFieldStorage('key=value') - self.assertTrue(ifs) self.assertEqual(ifs.getfirst('key'), 'value') self.assertEqual(ifs.getlist('key'), ['value']) diff --git a/uweb3/test_templateparser.py b/test/test_templateparser.py old mode 100644 new mode 100755 similarity index 76% rename from uweb3/test_templateparser.py rename to test/test_templateparser.py index b3af77f9..88a11e9c --- a/uweb3/test_templateparser.py +++ b/test/test_templateparser.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """Tests for the templateparser module.""" # Too many public methods @@ -77,8 +77,8 @@ class ParserPerformance(unittest.TestCase): @staticmethod def testPerformance(): """[Parser] Basic performance test for 2 template replacements""" + template = 'This [obj:foo] is just a quick [bar]' for _template in range(100): - template = 'This [obj:foo] is just a quick [bar]' tmpl = templateparser.Template(template) for _parse in range(100): tmpl.Parse(obj={'foo': 'template'}, bar='hack') @@ -107,6 +107,18 @@ def testSingleTagTemplate(self): result = self.tmpl(template).Parse(single='just one') self.assertEqual(result, 'Template with just one tag') + def testSaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is already html safe""" + template = 'Template with just [single] tag' + result = self.tmpl(template).Parse(single=templateparser.HTMLsafestring('a safe')) + self.assertEqual(result, 'Template with just a safe tag') + + def testUnsaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is not html safe""" + template = 'Template with just [single] tag' + result = self.tmpl(template).Parse(single='an unsafe') + self.assertEqual(result, 'Template with just <b>an unsafe</b> tag') + def testCasedTag(self): """[BasicTag] Tag names are case-sensitive""" template = 'The parser has no trouble with [cAsE] [case].' @@ -136,7 +148,7 @@ def testBadCharacterTags(self): bad_chars = """ :~!@#$%^&*()+-={}\|;':",./<>? """ template = ''.join('[%s] [check]' % char for char in bad_chars) expected = ''.join('[%s] ..' % char for char in bad_chars) - replaces = dict((char, 'FAIL') for char in bad_chars) + replaces = {char: 'FAIL' for char in bad_chars} replaces['check'] = '..' self.assertEqual(self.tmpl(template).Parse(**replaces), expected) @@ -145,6 +157,11 @@ def testUnreplacedTag(self): template = 'Template with an [undefined] tag.' self.assertEqual(self.tmpl(template).Parse(), template) + def testUnreplacedTag(self): + """[BasicTag] Access to private members is not allowed""" + template = 'Template with an [private.__class__] tag.' + self.assertEqual(self.tmpl(template).Parse(), template) + def testBracketsInsideTag(self): """[BasicTag] Innermost bracket pair are the tag's delimiters""" template = 'Template tags may not contain [[spam][eggs]].' @@ -196,9 +213,12 @@ class Mapping(dict): self.assertEqual(self.tmpl(template).Parse(tag=mapp), lookup_dict) def testTemplateIndexingCharacters(self): - """[IndexedTag] Tags indexes may be made of word chars and dashes only""" - good_chars = "aAzZ0123-_" - bad_chars = """ :~!@#$%^&*()+={}\|;':",./<>? """ + """[IndexedTag] Tags indexes may be made of word chars and dashes only, + they should however not start and end with _ to avoid access to + private vars. + _ is allowed elsewhere in the string.""" + good_chars = "aAzZ0123-." + bad_chars = """ :~!@#$%^&*()+={}\|;':",/<>? """ for index in good_chars: tag = {index: 'SUCCESS'} template = '[tag:%s]' % index @@ -208,9 +228,32 @@ def testTemplateIndexingCharacters(self): template = '[tag:%s]' % index self.assertEqual(self.tmpl(template).Parse(tag=tag), template) + def testTemplateUnderscoreCharacters(self): + """[IndexedTag] Tags indexes may be made of word chars and dashes and dots + only, they should however not start and end with _ to avoid access to + private vars. + _ is allowed elsewhere in the string.""" + # see if objects with underscores are reachable + tag = {'test_test': 'SUCCESS'} + template = '[tag:%s]' % 'test_test' + self.assertEqual(self.tmpl(template).Parse(tag=tag), 'SUCCESS') + + tag = {'_test': 'SUCCESS'} + template = '[tag:%s]' % '_test' + self.assertEqual(self.tmpl(template).Parse(tag=tag), 'SUCCESS') + + tag = {'test_': 'SUCCESS'} + template = '[tag:%s]' % 'test_' + self.assertEqual(self.tmpl(template).Parse(tag=tag), 'SUCCESS') + + # check if private vars are impossible to reach. + tag = {'_test_': 'SUCCESS'} + template = '[tag:%s|raw]' % '_test_' + self.assertEqual(self.tmpl(template).Parse(tag=tag), repr(tag)) + def testTemplateMissingIndexes(self): """[IndexedTag] Tags with bad indexes will be returned verbatim""" - class Object(object): + class Object: """A simple object to store an attribute on.""" NAME = 'Freeman' @@ -234,6 +277,18 @@ def setUp(self): self.parse = self.parser.ParseString def testBasicFunction(self): + """[TagFunctions] and html safe output""" + template = 'This function does [none].' + result = self.parse(template, none='"nothing"') + self.assertEqual(result, 'This function does "nothing".') + + def testBasicFunctionNumeric(self): + """[TagFunctions] and html safe output for non string outputs""" + template = '[tag]' + result = self.parse(template, tag=1) + self.assertEqual(result, '1') + + def testBasicFunctionRaw(self): """[TagFunctions] Raw function does not affect output""" template = 'This function does [none|raw].' result = self.parse(template, none='"nothing"') @@ -244,7 +299,7 @@ def testNonexistantFuntion(self): template = 'This tag function is missing [num|zoink].' self.assertEqual(self.parse(template), template) # Error is only thrown if we actually pass an argument for the tag: - self.assertRaises(templateparser.TemplateNameError, + self.assertRaises(templateparser.TemplateFunctionError, self.parse, template, num=1) def testAlwaysString(self): @@ -326,14 +381,14 @@ def testTagFunctionUrl(self): def testTagFunctionItems(self): """[TagFunctions] The tag function 'items' is present and works""" - template = '[tag|items]' + template = '[tag|items|raw]' tag = {'ham': 'eggs'} result = "[('ham', 'eggs')]" self.assertEqual(result, self.parse(template, tag=tag)) def testTagFunctionValues(self): """[TagFunctions] The tag function 'values' is present and works""" - template = '[tag|values]' + template = '[tag|values|raw]' self.assertEqual(self.parse(template, tag={'ham': 'eggs'}), "['eggs']") def testTagFunctionSorted(self): @@ -347,6 +402,7 @@ def testTagFunctionLen(self): template = '[numbers|len]' self.assertEqual(self.parse(template, numbers=range(12)), "12") + class TemplateTagFunctionClosures(unittest.TestCase): """Tests the functions that are performed on replaced tags.""" @staticmethod @@ -383,6 +439,27 @@ def testSimpleClosureArgument(self): result = self.parse(template, tag=self.tag) self.assertEqual(result, self.tag[:20]) + def testMathClosureArgument(self): + """[TagClosures] Math tag-closure functions operate on their argument""" + template = '[tag|limit(5*4)]' + result = self.parse(template, tag=self.tag) + self.assertEqual(result, self.tag[:20]) + + def testFunctionClosureArgument(self): + """[TagClosures] tags that use function calls in their function input should + never be parsed""" + template = '[tag|limit(abs(-20))]' + result = self.parse(template, tag=self.tag) + self.assertEqual(result, template) + + def testVariableClosureArgument(self): + """[TagClosures] tags that try to use vars in their function arguments + should never have access to the python scope.""" + test = 20 + template = '[tag|limit(test)]' + self.assertRaises(templateparser.TemplateNameError, + self.parse, template, tag=self.tag) + def testComplexClosureWithoutArguments(self): """[TagClosures] Complex tag closure-functions without arguments succeed""" template = '[tag|strlimit()]' @@ -401,7 +478,7 @@ def testComplexClosureArguments(self): def testCharactersInClosureArguments(self): """[TagClosures] Arguments strings may contain specialchars""" - template = '[tag|strlimit(20, "`-=./<>?`!@#$%^&*_+[]\{}|;\':")]' + template = '[tag|strlimit(20, "`-=./<>?`!@#$%^&*_+[]\{}|;\':")|raw]' result = self.parser.ParseString(template, tag=self.tag) self.assertTrue(result.endswith('`-=./<>?`!@#$%^&*_+[]\{}|;\':')) @@ -521,13 +598,28 @@ def testCompareTag(self): self.assertFalse(self.parse(template, variable=12)) self.assertTrue(self.parse(template, variable=5)) + def testCompareMath(self): + """{{ if }} Basic math""" + template = '{{ if 5*5 == 25 }} foo {{ endif }}' + self.assertEqual(self.parse(template, variable=5).strip(), 'foo') + def testTagIsInstance(self): - """{{ if }} Basic tag value comparison""" + """{{ if }} Tag value after python function comparison""" template = '{{ if isinstance([variable], int) }} ack {{ endif }}' self.assertFalse(self.parse(template, variable=[1])) self.assertFalse(self.parse(template, variable='number')) self.assertEqual(self.parse(template, variable=5), ' ack ') + def testComparePythonFunction(self): + """{{ if }} Tag value after python len comparison""" + template = '{{ if len([variable]) == 5 }} foo {{ endif }}' + self.assertEqual(self.parse(template, variable=[1,2,3,4,5]).strip(), 'foo') + + def testCompareNotallowdPythonFunction(self): + """{{ if }} Tag value after python len comparison""" + template = '{{ if open([variable]) == 5 }} foo {{ endif }}' + self.assertRaises(templateparser.TemplateEvaluationError, self.parse, template) + def testDefaultElse(self): """{{ if }} Else block will be parsed when `if` fails""" template = '{{ if [var] }}foo{{ else }}bar{{ endif }}' @@ -689,7 +781,9 @@ def testLoopAbsentIndex(self): class TemplateTagPresenceCheck(unittest.TestCase): """Test cases for the `ifpresent` TemplateParser construct.""" def setUp(self): - self.parse = templateparser.Parser().ParseString + self.parser = templateparser.Parser() + self.parse = self.parser.ParseString + self.templatefilename = 'ifpresent.utp' def testBasicTagPresence(self): """{{ ifpresent }} runs the code block if the tag is present""" @@ -701,6 +795,20 @@ def testBasicTagAbsence(self): template = '{{ ifpresent [tag] }} hello {{ endif }}' self.assertFalse(self.parse(template)) + def testBasicTagNotPresence(self): + """{{ ifnotpresent }} runs the code block if the tag is present""" + template = '{{ ifnotpresent [tag] }} hello {{ endif }}' + self.assertEqual(self.parse(template, othertag='spam'), ' hello ') + + def testNestedNotPresence(self): + """{{ ifnotpresent }} runs the code block if the tag is present""" + template = """{{ ifnotpresent [tag] }} + {{ ifnotpresent [nestedtag] }} + hello + {{ endif }} + {{ endif }}""" + self.assertEqual(self.parse(template, othertag='spam').strip(), 'hello') + def testTagPresenceElse(self): """{{ ifpresent }} has a functioning `else` clause""" template = '{{ ifpresent [tag] }} yes {{ else }} no {{ endif }}' @@ -734,6 +842,25 @@ def testBadSyntax(self): template = '{{ ifpresent var }} {{ endif }}' self.assertRaises(templateparser.TemplateSyntaxError, self.parse, template) + def testMultiTagPresenceFile(self): + """{{ ifpresent }} checks if multiple runs on a file template containing an + Ifpresent block work""" + + template = '{{ ifpresent [one] }} [one] {{ endif }}Blank' + with open(self.templatefilename, 'w') as templatefile: + templatefile.write(template) + self.assertEqual(self.parser.Parse(self.templatefilename), 'Blank') + #self.assertEqual(self.parser.Parse(self.templatefilename), 'Blank') + #self.assertEqual(self.parser.Parse(self.templatefilename, one=1), ' 1 Blank') + + def tearDown(self): + for tmpfile in (self.templatefilename, ): + if os.path.exists(tmpfile): + if os.path.isdir(tmpfile): + os.rmdir(tmpfile) + else: + os.unlink(tmpfile) + class TemplateStringRepresentations(unittest.TestCase): """Test cases for string representation of various TemplateParser parts.""" @@ -858,5 +985,136 @@ def testReplaceTemplateWithDirectory(self): self.assertEqual(self.parser[self.simple].Parse(), self.simple_raw) +class DictTemplateTagBasic(unittest.TestCase): + """Tests validity and parsing of simple tags with dict output.""" + def setUp(self): + """Makes the Template class available on the instance.""" + self.tmpl = templateparser.Template + + def testTaglessTemplate(self): + """[BasicTag] Templates without tags get returned verbatim as SafeString""" + template = 'Template without any tags' + output = {'tags': {}, 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(), output) + + def testSingleTagTemplate(self): + """[BasicTag] Templates with basic tags get returned proper""" + template = 'Template with [single] tag' + output = {'tags': { + '[single]': 'just one' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(single='just one') + self.assertEqual(result, output) + + def testSaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is already html safe""" + template = 'Template with just [single] tag' + output = {'tags': { + '[single]': 'a safe' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(single=templateparser.HTMLsafestring('a safe')) + self.assertEqual(result, output) + + def testUnsaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is not html safe""" + template = 'Template with just [single] tag' + output = {'tags': { + '[single]': '<b>an unsafe</b>' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(single='an unsafe') + self.assertEqual(result, output) + + def testCasedTag(self): + """[BasicTag] Tag names are case-sensitive""" + template = 'The parser has no trouble with [cAsE] [case].' + output = {'tags': { + '[cAsE]': 'mixed', + '[case]': '[case]' + }, + 'templatecontent': template} + + result = self.tmpl(template, dictoutput=True).Parse(cAsE='mixed') + self.assertEqual(result, output) + + def testUnderscoredTag(self): + """[BasicTag] Tag names may contain underscores""" + template = 'The template may contain [under_scored] tags.' + output = {'tags': { + '[under_scored]': 'underscored' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(under_scored='underscored') + self.assertEqual(result, output) + + def testMultiTagTemplate(self): + """[BasicTag] Multiple instances of a tag will all be replaced""" + template = '[adjective] [noun] are better than other [noun].' + output = {'tags': { + '[noun]': 'cows', + '[adjective]': 'Beefy' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(noun='cows', adjective='Beefy') + self.assertEqual(result, output) + + def testEmptyOrWhitespace(self): + """[BasicTag] Empty tags or tags containing whitespace aren't actual tags""" + template = 'This [is a] broken [] template, really' + output = {'tags': {}, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(**{'is a': 'HORRIBLY', '': ', NASTY'}) + self.assertEqual(result, output) + + def testBadCharacterTags(self): + """[BasicTag] Tags containing bad characters are not considered tags""" + bad_chars = """ :~!@#$%^&*()+-={}\|;':",./<>? """ + template = ''.join('[%s] [check]' % char for char in bad_chars) + tags = {'[check]': '..'} + replaces = {char: 'FAIL' for char in bad_chars} + replaces['check'] = '..' + output = {'tags': tags, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(**replaces), output) + + def testUnreplacedTag(self): + """[BasicTag] Template tags without replacement are returned verbatim""" + template = 'Template with an [undefined] tag.' + output = {'tags': {}, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(), output) + + def testUnreplacedTag(self): + """[BasicTag] Access to private members is not allowed""" + template = 'Template with an [private.__class__] tag.' + output = {'tags': {}, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(), output) + + def testBracketsInsideTag(self): + """[BasicTag] Innermost bracket pair are the tag's delimiters""" + template = 'Template tags may not contain [[spam][eggs]].' + expected = 'Template tags may not contain [opening or closing brackets].' + result = self.tmpl(template, dictoutput=True).Parse( + **{'[spam': 'EPIC', 'eggs]': 'FAIL', 'spam][eggs': 'EPIC FAIL', + 'spam': 'opening or ', 'eggs': 'closing brackets'}) + output = {'tags': { + '[spam]': 'opening or ', + '[eggs]': 'closing brackets' + }, + 'templatecontent': template} + self.assertEqual(output, result) + + def testTemplateInterpolationSyntax(self): + """[BasicTag] Templates support string interpolation of dicts""" + template = 'Hello [name]' + output = {'tags': { + '[name]': 'Bob', + }, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True) % {'name': 'Bob'}, output) + if __name__ == '__main__': unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 95bbc51e..e4db38c7 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -1,58 +1,46 @@ -#!/usr/bin/python -"""uWeb3 Framework""" +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +"""µWeb3 Framework""" -__version__ = '0.4.4-dev' +__version__ = '3.0.5' # Standard modules -try: - import ConfigParser as configparser -except ImportError: - import configparser +import configparser +import datetime import logging import os import re import sys import time -import threading +from importlib import reload from wsgiref.simple_server import make_server -import socket, errno -import datetime - - -# Add the ext_lib directory to the path -sys.path.append( - os.path.abspath(os.path.join(os.path.dirname(__file__), 'ext_lib'))) # Package modules -from . import pagemaker -from . import request +from . import pagemaker, request # Package classes -from .response import Response -from .response import Redirect -from .pagemaker import PageMaker -from .pagemaker import DebuggingPageMaker -from .pagemaker import SqAlchemyPageMaker -from .helpers import StaticMiddleware -from uweb3.model import SettingsManager +from .response import Response, Redirect +from .pagemaker import PageMaker, decorators, WebsocketPageMaker, DebuggingPageMaker, LoginMixin, SparseAsyncPages +from .model import SettingsManager +from .libs.safestring import HTMLsafestring, JSONsafestring, JsonEncoder, Basesafestring class Error(Exception): """Superclass used for inheritance and external exception handling.""" - class ImmediateResponse(Exception): """Used to trigger an immediate response, foregoing the regular returns.""" +class HTTPException(Error): + """SuperClass for HTTP exceptions.""" + +class HTTPRequestException(HTTPException): + """Exception for http request errors.""" class NoRouteError(Error): """The server does not know how to route this request""" -class Registry(object): - """Something to hook stuff to""" - - -class Router(object): +class Router: def __init__(self, page_class): self.pagemakers = page_class.LoadModules() self.pagemakers.append(page_class) @@ -74,26 +62,33 @@ def router(self, routes): request_router: Configured closure that processes urls. """ req_routes = [] + # Variable used to store websocket pagemakers, + # these pagemakers are only created at startup but can have multiple routes. + # To prevent creating the same instance for each route we store them in a dict + websocket_pagemaker = {} for pattern, *details in routes: - pagemaker = None + page_maker = None for pm in self.pagemakers: - #Check if the pagemaker has the method/handler we are looking for + # Check if the page_maker has the method/handler we are looking for if hasattr(pm, details[0]): - pagemaker = pm + page_maker = pm break if callable(pattern): - #TODO: Pass environment to a custom pagemaker for websockets? - pattern(getattr(pagemaker, details[0])) + # Check if the page_maker is already in the dict, if not instantiate + # if so just use that one. This prevents creating multiple instances for one route. + if not websocket_pagemaker.get(page_maker.__name__): + websocket_pagemaker[page_maker.__name__] = page_maker() + pattern(getattr(websocket_pagemaker[page_maker.__name__], details[0])) continue - if not pagemaker: - raise NoRouteError(f"""There is no handler called: {details[0]} in any of the projects PageMaker. - Static routes are automatically handled so there is no need to define them in routes anymore.""") + if not page_maker: + raise NoRouteError(f"µWeb3 could not find a route handler called '{details[0]}' in any of the PageMakers, your application will not start.") req_routes.append((re.compile(pattern + '$', re.UNICODE), details[0], #handler, - details[1] if len(details) > 1 else 'ALL', #request types - details[2] if len(details) > 2 else '*', #host - pagemaker #pagemaker + details[1].upper() if len(details) > 1 else 'ALL', #request types + details[2].lower() if len(details) > 2 else '*', #host + page_maker #pagemaker class )) + def request_router(url, method, host): """Returns the appropriate handler and arguments for the given `url`. @@ -120,12 +115,11 @@ def request_router(url, method, host): 2-tuple: handler method (unbound), and tuple of pattern matches. """ - for pattern, handler, routemethod, hostpattern, pagemaker in req_routes: + for pattern, handler, routemethod, hostpattern, page_maker in req_routes: if routemethod != 'ALL': # clearly not the route we where looking for - if isinstance(routemethod, tuple): - if method not in routemethod: - continue + if isinstance(routemethod, tuple) and method not in routemethod: + continue if method != routemethod: continue @@ -139,11 +133,15 @@ def request_router(url, method, host): hostmatch = hostmatch.groups() match = pattern.match(url) if match: - return handler, match.groups(), hostmatch, pagemaker + # strip out optional groups, as they return '', which would override + # the handlers default argument values later on in the page_maker + groups = (group for group in match.groups() if group) + return handler, groups, hostmatch, page_maker raise NoRouteError(url +' cannot be handled') return request_router -class uWeb(object): + +class uWeb: """Returns a configured closure for handling page requests. This closure is configured with a precomputed set of routes and handlers using @@ -166,121 +164,225 @@ class uWeb(object): Returns: RequestHandler: Configured closure that is ready to process requests. """ - def __init__(self, page_class, routes, executing_path=None): - self.executing_path = executing_path - self.config = SettingsManager(filename='config', executing_path=executing_path) - self.logger = self.setup_logger() - self.page_class = page_class - self.registry = Registry() - self.registry.logger = logging.getLogger('root') + def __init__(self, page_class, routes, executing_path=None, config='config'): + self.executing_path = executing_path or os.path.dirname(__file__) + self.config = SettingsManager(filename=config, path=self.executing_path) + self._accesslogger = None + self._errorlogger = None + self.initial_pagemaker = page_class self.router = Router(page_class).router(routes) - self.secure_cookie_secret = str(os.urandom(32)) self.setup_routing() - + self.encoders = { + 'text/html': lambda x: HTMLsafestring(x, unsafe=True), + 'text/plain': str, + 'text/csv': str, + 'application/json': lambda x: JSONsafestring(x, unsafe=True), + 'default': lambda x: HTMLsafestring(x, unsafe=True) if str(x).endswith('xml') else str(x)} + + accesslogging = self.config.options.get('log', {}).get('access_logging', True) != 'False' + self._logrequest = self.logrequest if accesslogging else lambda *args: None + # log exceptions even when development is present, but error_logging was not disabled specifically + errorlogging = self.config.options.get('log', {'error_logging': 'False'}).get('error_logging', 'True') == 'True' + self._logerror = self.logerror if errorlogging else lambda *args: None def __call__(self, env, start_response): """WSGI request handler. Accepts the WSGI `environment` dictionary and a function to start the response and returns a response iterator. """ - req = request.Request(env, self.registry) + req = request.Request(env, self.logger, self.errorlogger) + req.env['REAL_REMOTE_ADDR'] = request.return_real_remote_addr(req.env) + response = None + method = '_NotFound' + args = None + rollback = False try: - method, args, hostargs, pagemaker = self.router(req.path, + method, args, hostargs, page_maker = self.router(req.path, req.env['REQUEST_METHOD'], - req.env['host'] - ) - pagemaker = pagemaker(req, config=self.config.options, secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path) - response = self.get_response(pagemaker, method, args) + req.env['host']) except NoRouteError: - #When we catch this error this means there is no method for the expected function - #If this happens we default to the standard pagemaker because we don't know what the target pagemaker should be. - #Then we set an internalservererror and move on - pagemaker = self.page_class(req, config=self.config.options, secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path) - response = pagemaker.InternalServerError(*sys.exc_info()) + # When we catch this error this means there is no method for the route in the currently selected pagemaker. + # If this happens we default to the initial pagemaker because we don't know what the target pagemaker should be. + # Then we set an internalservererror and move on + page_maker = self.initial_pagemaker + try: + # instantiate the pagemaker for this request + pagemaker_instance = page_maker(req, + config=self.config, + executing_path=self.executing_path) + # specifically call _PreRequest as promised in documentation + if hasattr(pagemaker_instance, '_PreRequest'): + try: + # we handle the preRequest seperately because otherwise we cannot show debugging info + pagemaker_instance = pagemaker_instance._PreRequest() or pagemaker_instance + except Exception: + # lets use the intended pagemaker, but skip prerequest as it crashes, this enabled rich debugging info if enabled. + response = pagemaker_instance.InternalServerError(*sys.exc_info()) + if not response: + response = self.get_response(req, pagemaker_instance, method, args) except Exception: - #This should only happend when something is very wrong - pagemaker = PageMaker(req, config=self.config.options, secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path) - response = pagemaker.InternalServerError(*sys.exc_info()) - - if not isinstance(response, Response): - req.response.text = response - response = req.response - - if hasattr(pagemaker, '_PostRequest'): - response = pagemaker._PostRequest(response) - - self._logging(req, response) + # something broke in our pagemaker_instance, lets fall back to the most basic pagemaker for error output + if hasattr(pagemaker_instance, '_ConnectionRollback'): + try: + pagemaker_instance._ConnectionRollback() + except: + pass + pagemaker_instance = PageMaker(req, + config=self.config, + executing_path=self.executing_path) + response = pagemaker_instance.InternalServerError(*sys.exc_info()) + + static = (method == 'Static') + + if not static: + if not isinstance(response, Response): + req.response.text = response + response = req.response + + if req.noparse: + response.content_type = 'application/json' + + if not isinstance(response.text, Basesafestring): + # make sure we always output Safe Strings for our known content-types + encoder = self.encoders.get(response.clean_content_type(), self.encoders['default']) + response.text = encoder(response.text) + + # CSP might be unneeded for some static content, + # https://github.com/w3c/webappsec/issues/520 + if hasattr(pagemaker_instance, '_CSPheaders'): + pagemaker_instance._CSPheaders() + + # provide users with a PostRequest method to overide too + if not static and hasattr(pagemaker_instance, 'PostRequest'): + response = pagemaker_instance.PostRequest(response) or response + pagemaker_instance.CloseRequestConnections() + + # we should at least send out something to make sure we are wsgi compliant. + if not response.text: + response.text = '' + + self._logrequest(req, response) start_response(response.status, response.headerlist) - yield response.content.encode(response.charset) - - def setup_logger(self): - logger = logging.getLogger('uweb3_logger') - logger.setLevel(logging.INFO) - fh = logging.FileHandler(os.path.join(self.executing_path, 'access_logging.log')) - fh.setLevel(logging.INFO) - logger.addHandler(fh) - return logger - - def _logging(self, req, response): - """Logs incoming requests to a logfile. - This is enabled by default, even if its missing in the config file. - """ - if self.config.options.get('development', None): - if self.config.options['development'].get('access_logging', True) == 'False': - return - + try: + yield response.text.encode(response.charset) + except AttributeError: + yield response.text + + @property + def logger(self): + if not self._accesslogger: + logger = logging.getLogger('uweb3_logger') + logger.setLevel(logging.INFO) + logpath = os.path.join(self.executing_path, self.config.options.get('log', {}).get('acces_log', 'access_log.log')) + delay = self.config.options.get('log', {}).get('acces_log_delay', False) != False + encoding = self.config.options.get('log', {}).get('acces_log_encoding', None) + fh = logging.FileHandler(logpath, encoding=encoding, delay=delay) + fh.setLevel(logging.INFO) + logger.addHandler(fh) + self._accesslogger = logger + return self._accesslogger + + @property + def errorlogger(self): + if not self._errorlogger: + logger = logging.getLogger('uweb3_exception_logger') + logger.setLevel(logging.ERROR) + logpath = os.path.join(self.executing_path, self.config.options.get('log', {}).get('exception_log', 'uweb3_exceptions.log')) + delay = self.config.options.get('log', {}).get('exception_log_delay', False) != False + encoding = self.config.options.get('log', {}).get('exception_log_encoding', None) + fh = logging.FileHandler(logpath, encoding=encoding, delay=delay) + fh.setLevel(logging.INFO) + logger.addHandler(fh) + self._errorlogger = logger + return self._errorlogger + + def logrequest(self, req, response): + """Logs incoming requests to the logfile.""" host = req.env['HTTP_HOST'].split(':')[0] date = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') method = req.method path = req.path + get = req.vars['get'] status = response.httpcode protocol = req.env.get('SERVER_PROTOCOL') - self.logger.info(f"""{host} - - [{date}] \"{method} {path} {status} {protocol}\"""") + if not response.log: + return self.logger.info(f"""{host} - - [{date}] \"{method} {path} {get} {status} {protocol}\"""") + data = response.log + return self.logger.info(f"""{host} - - [{date}] \"{method} {path} {get} {status} {protocol} {data}\"""") - def get_response(self, page_maker, method, args): + def logerror(self, req, page_maker, pythonmethod, args): + """Logs errors and exceptions to the logfile.""" + host = req.env['HTTP_HOST'].split(':')[0] + date = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') + method = req.method + path = req.path + protocol = req.env.get('SERVER_PROTOCOL') + args = [str(arg) for arg in args] + return self.errorlogger.exception(f"""{host} - - [{date}] \"{method} {path} {protocol} {page_maker}.{pythonmethod}({args})\"""") + + def get_response(self, req, page_maker, method, args): try: - # We're specifically calling _PostInit here as promised in documentation. - # pylint: disable=W0212 - page_maker._PostInit() + if method != 'Static': + # We're specifically calling _PostInit here as promised in documentation. + # pylint: disable=W0212 + page_maker._PostInit() + elif hasattr(page_maker, '_StaticPostInit'): + # We're specifically calling _StaticPostInit here as promised in documentation, seperate from the regular PostInit to keep things fast for static pages + page_maker._StaticPostInit() + # pylint: enable=W0212 return getattr(page_maker, method)(*args) except pagemaker.ReloadModules as message: - reload_message = reload(sys.modules[self.page_class.__module__]) + reload_message = reload(sys.modules[self.initial_pagemaker.__module__]) return Response(content='%s\n%s' % (message, reload_message)) except ImmediateResponse as err: return err[0] except Exception: - if self.config.options.get('development', None): - if self.config.options['development'].get('error_logging', True) == 'True': - logger = logging.getLogger('uweb3_exception_logger') - fh = logging.FileHandler(os.path.join(self.executing_path, 'uweb3_uncaught_exceptions.log')) - logger.addHandler(fh) - logger.exception("UNCAUGHT EXCEPTION:") + self._logerror(req, page_maker, method, args) return page_maker.InternalServerError(*sys.exc_info()) - def serve(self, hot_reloading=True): + def serve(self): """Sets up and starts WSGI development server for the current app.""" - host = self.config.options['development'].get('host', 'localhost') - port = self.config.options['development'].get('port', 8001) - static_directory = [os.path.join(sys.path[0], os.path.join(self.executing_path, 'static'))] - app = StaticMiddleware(self, static_root='static', static_dirs=static_directory) - server = make_server(host, int(port), app) - + host = 'localhost' + port = 8001 + hotreload = False + interval = None + + if self.config.options.get('development', False): + devconfig = self.config.options['development'] + host = devconfig.get('host', host) + port = devconfig.get('port', port) + hotreload = devconfig.get('reload', False) in ('True', 'true') + + server = make_server(host, int(port), self) print(f'Running µWeb3 server on http://{server.server_address[0]}:{server.server_address[1]}') + print(f'Root dir is: {self.executing_path}') + if hotreload: + ignored_directories = ['__pycache__', + self.initial_pagemaker.PUBLIC_DIR, + self.initial_pagemaker.TEMPLATE_DIR] + ignored_extensions = [] + interval = int(devconfig.get('checkinterval', 0)) + if 'ignored_extensions' in devconfig: + ignored_extensions = devconfig.get('ignored_extensions', '').split(',') + if 'ignored_directories' in devconfig: + ignored_directories += devconfig.get('ignored_directories', '').split(',') + + print(f'Hot reload is enabled for changes in: {self.executing_path}') + HotReload(self.executing_path, interval=interval, + ignored_extensions=ignored_extensions, + ignored_directories=ignored_directories) try: - if self.config.options['development'].get('dev', False) == 'True': - HotReload(self.executing_path, uweb_dev=self.config.options['development'].get('uweb_dev', 'False')) server.serve_forever() - except: + except Exception as error: + print(error) server.shutdown() def setup_routing(self): - if isinstance(self.page_class, list): - routes = [] - for route in self.page_class[1:]: - routes.append(route) - self.page_class[0].AddRoutes(tuple(routes)) - self.page_class = self.page_class[0] + if isinstance(self.initial_pagemaker, list): + routes = [route for route in self.initial_pagemaker[1:]] + self.initial_pagemaker[0].AddRoutes(tuple(routes)) + self.initial_pagemaker = self.initial_pagemaker[0] default_route = "routes" automatic_detection = True @@ -289,69 +391,61 @@ def setup_routing(self): automatic_detection = self.config.options['routing'].get('disable_automatic_route_detection', 'False') != 'True' if automatic_detection: - self.page_class.LoadModules(default_routes=default_route) - -def read_config(config_file): - """Parses the given `config_file` and returns it as a nested dictionary.""" - parser = configparser.SafeConfigParser() - try: - parser.read(config_file) - except configparser.ParsingError: - raise ValueError('Not a valid config file: %r.' % config_file) - return dict((section, dict(parser.items(section))) - for section in parser.sections()) - -class HotReload(object): - def __init__(self, path, interval=1, uweb_dev=False): + self.initial_pagemaker.LoadModules(routes=default_route) + + +class HotReload: + """This class handles the thread which scans for file changes in the + execution path and restarts the server if needed""" + IGNOREDEXTENSIONS = [".pyc", '.ini', '.md', '.html', '.log', '.sql'] + + def __init__(self, path, interval=1, ignored_extensions=None, ignored_directories=None): + """Takes a path, an optional interval in seconds and an optional flag + signaling a development environment which will set the path for new and + changed file checking on the parent folder of the serving file.""" + import threading self.running = threading.Event() self.interval = interval self.path = os.path.dirname(path) - if uweb_dev == 'True': - from pathlib import Path - self.path = str(Path(self.path).parents[1]) - self.thread = threading.Thread(target=self.run, args=()) - self.thread.daemon = True + self.ignoredextensions = self.IGNOREDEXTENSIONS + (ignored_extensions or []) + self.ignoreddirectories = ignored_directories + self.thread = threading.Thread(target=self.Run, daemon=True) self.thread.start() - def run(self): - """ Method runs forever and watches all files in the project folder. - - Does not trigger a reload when the following files change: - - .pyc - - .ini - - .md - - .html - - .log - - Changes in the HTML are noticed by the TemplateParser, - which then reloads the HTML file into the object and displays the updated version. - """ - self.WATCHED_FILES = self.getListOfFiles()[1] - WATCHED_FILES_MTIMES = [(f, os.path.getmtime(f)) for f in self.WATCHED_FILES] + def Run(self): + """ Method runs forever and watches all files in the project folder.""" + self.watched_files = self.Files() + self.mtimes = [(f, os.path.getmtime(f)) for f in self.watched_files] while True: - if len(self.WATCHED_FILES) != self.getListOfFiles()[0]: + time.sleep(self.interval) + new = self.Files(self.watched_files) + if new: print('{color}New file added or deleted\x1b[0m \nRestarting µWeb3'.format(color='\x1b[7;30;41m')) - self.restart() - for f, mtime in WATCHED_FILES_MTIMES: + self.Restart() + for f, mtime in self.mtimes: if os.path.getmtime(f) != mtime: print('{color}Detected changes in {file}\x1b[0m \nRestarting µWeb3'.format(color='\x1b[7;30;41m', file=f)) - self.restart() - time.sleep(self.interval) - - def getListOfFiles(self): - """Returns all files inside the working directory of uweb3. - Also returns a count so that we can restart on file add/remove. - """ - watched_files = [] - for r, d, f in os.walk(self.path): - for file in f: + self.Restart() + + def Files(self, current=None): + """Returns all files inside the working directory of µWeb3.""" + if not current: + current = set() + new = set() + for dirpath, dirnames, filenames in os.walk(self.path): + if any(list(map(lambda dirname: dirname in dirpath, self.ignoreddirectories))): + continue + for file in filenames: + fullname = os.path.join(dirpath, file) + if fullname in current or fullname.endswith('~'): + continue ext = os.path.splitext(file)[1] - if ext not in (".pyc", '.ini', '.md', '.html', '.log'): - watched_files.append(os.path.join(r, file)) - return (len(watched_files), watched_files) + if ext not in self.ignoredextensions: + new.add(fullname) + return new - def restart(self): - """Restart uweb3 with all provided system arguments.""" + def Restart(self): + """Restart µWeb3 with all provided system arguments.""" self.running.clear() - os.execl(sys.executable, sys.executable, * sys.argv) \ No newline at end of file + os.execl(sys.executable, sys.executable, * sys.argv) diff --git a/uweb3/access_logging.log b/uweb3/access_logging.log deleted file mode 100644 index 89c8f793..00000000 --- a/uweb3/access_logging.log +++ /dev/null @@ -1,286 +0,0 @@ -127.0.0.1 - - [29/04/2020 09:44:40] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:40] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:41] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:42] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:49] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:53] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:54] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:57] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:57] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:45:10] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:45:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:52:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:52:27] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:54:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:54:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:55:07] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:55:30] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:55:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:19] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:20] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:41] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:42] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:13] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:21] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:28] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:57] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:59:07] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:59:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:59:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:00:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:01:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:02:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:29:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:29:41] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:30:09] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:30:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:30:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:30:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:31:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:31:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:34:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:34:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:12] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:35] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:45:00] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:45:35] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:45:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:17] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:38] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:46:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:47:11] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:48:00] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:48:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:48:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:49:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:49:59] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:50:00] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:58:21] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:58:21] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:58:21] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:28] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:28] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:28] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:33] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:33] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /signup 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:49] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:49] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:49] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:04:02] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:04:02] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:04:02] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:06:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:06:42] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:06:43] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:46] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:47] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:08:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:08:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:11:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:26] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:13:06] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:15:02] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:15:03] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:16:02] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:19:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:06] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:06] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:06] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:37:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:34] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:43] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:55] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:57] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:04:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:04:48] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:04:49] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:34] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:38] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:31] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:37] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:47] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:48] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:34:06] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:34:07] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:43:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:43:33] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:44:13] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:44:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:44:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:45:26] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:45:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:45:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:47:18] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:47:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:48:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:49:45] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:49:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:50:12] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:50:21] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:52:00] "GET / 200 HTTP/1.0" diff --git a/uweb3/alchemy_model.py b/uweb3/alchemy_model.py index df042d57..f9536ad7 100644 --- a/uweb3/alchemy_model.py +++ b/uweb3/alchemy_model.py @@ -1,30 +1,34 @@ -from uweb3.model import NotExistError +from itertools import chain + from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.orm import sessionmaker, reconstructor -from sqlalchemy.orm.session import object_session from sqlalchemy.inspection import inspect -from itertools import chain +from sqlalchemy.orm import reconstructor, sessionmaker +from sqlalchemy.orm.session import object_session -class AlchemyBaseRecord(object): +from uweb3.model import NotExistError + + +class AlchemyBaseRecord: def __init__(self, session, record): self.session = session self._BuildClassFromRecord(record) def _BuildClassFromRecord(self, record): - if isinstance(record, dict): - for key, value in record.items(): - if not key in self.__table__.columns.keys(): - raise AttributeError(f"Key '{key}' not specified in class '{self.__class__.__name__}'") - setattr(self, key, value) - if self.session: - try: - self.session.add(self) - except: - self.session.rollback() - raise - else: - self.session.commit() + if not isinstance(record, dict): + return + for key, value in record.items(): + if key not in self.__table__.columns.keys(): + raise AttributeError(f"Key '{key}' not specified in class '{self.__class__.__name__}'") + setattr(self, key, value) + if self.session: + try: + self.session.add(self) + except: + self.session.rollback() + raise + else: + self.session.commit() def __hash__(self): """Returns the hashed value of the key.""" @@ -68,7 +72,10 @@ def __ne__(self, other): return not self == other def __len__(self): - return len(dict((col, getattr(self, col)) for col in self.__table__.columns.keys() if getattr(self, col))) + return len({ + col: getattr(self, col) + for col in self.__table__.columns.keys() if getattr(self, col) + }) def __int__(self): """Returns the integer key value of the Record. @@ -177,7 +184,7 @@ def _AlchemyRecordToDict(cls, record): None: when record is empty """ if not isinstance(record, type(None)): - return dict((col, getattr(record, col)) for col in record.__table__.columns.keys()) + return {col: getattr(record, col) for col in record.__table__.columns.keys()} return None @reconstructor @@ -345,4 +352,157 @@ def Save(self): """Saves any changes made in the current record. Sqlalchemy automatically detects these changes and only updates the changed values. If no values are present no query will be commited.""" - self.session.commit() \ No newline at end of file + self.session.commit() + + +class AlchemyRecord(AlchemyBaseRecord): + """ """ + @classmethod + def FromPrimary(cls, session, p_key): + """Finds record based on given class and supplied primary key. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ p_key: integer + primary_key of the object to delete + Returns + cls + None + """ + try: + record = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).first() + except: + session.rollback() + raise + else: + if not record: + raise NotExistError(f"Record with primary key {p_key} does not exist") + return record + + @classmethod + def DeletePrimary(cls, session, p_key): + """Deletes record base on primary key from given class. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ p_key: integer + primary_key of the object to delete + + Returns: + isdeleted: boolean + """ + try: + isdeleted = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).delete() + except: + session.rollback() + raise + else: + session.commit() + return isdeleted + + @classmethod + def Create(cls, session, record): + """Creates a new instance and commits it to the database + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ record: dict + Dictionary with all key:value pairs that are required for the db record + Returns: + cls + """ + return cls(session, record) + + @classmethod + def List(cls, session, conditions=None, limit=None, offset=None, + order=None, yield_unlimited_total_first=False): + """Yields a Record object for every table entry. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + % conditions: list + Optional query portion that will be used to limit the list of results. + If multiple conditions are provided, they are joined on an 'AND' string. + For example: conditions=[User.id <= 10, User.id >=] + % limit: int ~~ None + Specifies a maximum number of items to be yielded. The limit happens on + the database side, limiting the query results. + % offset: int ~~ None + Specifies the offset at which the yielded items should start. Combined + with limit this enables proper pagination. + % order: tuple of operants + For example the User class has 3 fields; id, username, password. We can pass + the field we want to order on to the tuple like so; + (User.id.asc(), User.username.desc()) + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. + + Returns: + integer: integer with length of results. + list: List of classes from request type + """ + try: + query = session.query(cls) + if conditions: + for condition in conditions: + query = query.filter(condition) + if order: + for item in order: + query = query.order_by(item) + if limit: + query = query.limit(limit) + if offset: + query = query.offset(offset) + result = query.all() + except: + session.rollback() + raise + else: + if yield_unlimited_total_first: + return len(result) + return result + + @classmethod + def Update(cls, session, conditions, values): + """Update table based on conditions. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ conditions: list|tuple + for example: [User.id > 2, User.id < 100] + @ values: dict + for example: {User.username: 'value'} + """ + try: + query = session.query(cls) + for condition in conditions: + query = query.filter(condition) + query = query.update(values) + except: + session.rollback() + raise + else: + session.commit() + + def Delete(self): + """Delete current instance from the database""" + try: + isdeleted = self.session.query(type(self)).filter(self._PrimaryKeyCondition(self) == self.key).delete() + except: + self.session.rollback() + raise + else: + self.session.commit() + return isdeleted + + def Save(self): + """Saves any changes made in the current record. Sqlalchemy automatically detects + these changes and only updates the changed values. If no values are present + no query will be commited.""" + self.session.commit() diff --git a/uweb3/connections.py b/uweb3/connections.py new file mode 100644 index 00000000..1895a8c0 --- /dev/null +++ b/uweb3/connections.py @@ -0,0 +1,193 @@ +#!/usr/bin/python3 +"""This file contains all the connectionManager classes that interact with +databases, restfull apis, secure cookies, config files etc.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.2 + +import sys + +import uweb3 + +from .connectors import * + +class ConnectionError(Exception): + """Error class thrown when the underlying connectors thrown an error on + connecting.""" + +class ConnectionManager(object): + """This is the connection manager object that is handled by all Model Objects. + It finds out which connection was requested by looking at the call stack, and + figuring out what database type the model class calling it belongs to. + + Connected databases are stored and reused. + On delete, the databases are closed and any lingering transactions are + committed. to complete the database writes. + """ + + DEFAULTCONNECTIONMANAGER = None + + def __init__(self, config, options, debug, requestdepth=3, requestmaxdept=100): + """Initializes the ConnectionManager + + Arguments: + % config: reference to config parser + config instance + % options: reference to config parsers dict + options for the various settings provided in the config + % debug: bool, Optional defaults to False + Outputs extra debugging information if set to True + % requestdept: int, indicates how many stack layers we should start at + lookin up the stack to find our request object. Optional defaults to 2. + % requestdept: int, requestmaxdepth indicates how many stack layers at + maximum we should start at lookin up the stack to find our request object. + Optional defaults to 100. + + """ + self.__connectors = {} # classes + self.__connections = {} # instances + self.config = config + self.options = options + self.debug = debug + self.LoadDefaultConnectors() + self.requestdepth = requestdepth + self.requestmaxdepth = requestmaxdept + + def LoadDefaultConnectors(self): + """Populates the list of Connectors with the default available connectors""" + self.RegisterConnector(SignedCookie) + self.RegisterConnector(Mysql, True) + self.RegisterConnector(Sqlite) + self.RegisterConnector(Mongo) + self.RegisterConnector(SqlAlchemy) + + def RegisterConnector(self, handler, default=False): + """Make the ConnectonManager aware of a new type of connector. + + Arguments: + % handler: class + Reference to the class that will handle the connections + % default: bool, Optional defaults to False + Should this Connector be considers the default connector? + """ + if default: + self.DEFAULTCONNECTIONMANAGER = handler.Name() + self.__connectors[handler.Name()] = handler + + def RelevantConnection(self, level=2): + """Returns the relevant database connection dependant on the caller model + class. + + If the caller model cannot be determined, the 'relational' database + connection is returned as a fallback method. + + Level indicates how many stack layers we should go up to find the current + caller_class which indicates our connector type. Defaults to 2. + + When no connection can be found or made due to a missing request from this + context a TypeError will be raised. + + When no connection can be found or made Due to a missing connector class a + TypeError will be raised. + """ + # Figure out caller type or instance + # pylint: disable=W0212 + #TODO use inspect module instead, and iterate over frames + caller_locals = sys._getframe(level).f_locals + # pylint: enable=W0212 + # Caller might be a Class or Class instance + if 'self' in caller_locals: + caller_cls = type(caller_locals['self']) + else: + caller_cls = caller_locals.get('cls', type) + # Decide the type of connection to return for this caller + con_type = (caller_cls._CONNECTOR if hasattr(caller_cls, '_CONNECTOR') else + self.DEFAULTCONNECTIONMANAGER) + if (con_type in self.__connections and + hasattr(self.__connections[con_type], 'connection')): + return self.__connections[con_type].connection + + try: + # instantiate a connection + self.__connections[con_type] = self.__connectors[con_type]( + self.config, self.options, self.request, self.debug) + return self.__connections[con_type].connection + except KeyError as error: + raise TypeError('No connector for: %r, available: %r, %r' % ( + con_type, self.__connectors, error)) + + @property + def request(self): + """Returns the request object as looked up in the stack. + + When no connection can be found or made due to a missing request from this + context a TypeError will be raised. + + When no connection can be found or made Due to a missing connector class a + TypeError will be raised. + """ + requestdepth = self.requestdepth + while requestdepth < self.requestmaxdepth: + try: + parent = sys._getframe(requestdepth).f_locals['self'] + if isinstance(parent, uweb3.PageMaker) and hasattr(parent, 'req'): + return parent.req + except (KeyError, AttributeError, ValueError): + pass + requestdepth = requestdepth + 1 + raise TypeError('No request could be found in call Stack or no "model" connections are present.') + + def __enter__(self): + """Proxies the transaction to the underlying relevant connection.""" + return self.RelevantConnection().__enter__() + + def __exit__(self, *args): + """Proxies the transaction to the underlying relevant connection.""" + return self.RelevantConnection().__exit__(*args) + + def __getattr__(self, attribute): + return getattr(self.RelevantConnection(), attribute) + + def RollbackAll(self): + """Performs a rollback on all connectors with pending commits.""" + if self.debug: + print('Rolling back uncommited transaction on all connectors.') + for classname in self.__connections: + try: + self.__connections[classname].Rollback() + except NotImplementedError: + pass + + def PostRequest(self): + """This cleans up any non persistent connections. + Eg, connections that rely on request information, or connections that should + not be kept alive beyond the scope of a request. + """ + cleanups = [ + classname for classname in self.__connections + if (hasattr(self.__connections[classname], 'PERSISTENT') + and not self.__connections[classname].PERSISTENT) + ] + for classname in cleanups: + try: + self.__connections[classname].Disconnect() + except (NotImplementedError, TypeError, ConnectionError): + pass + del(self.__connections[classname]) + + def __iter__(self): + """Pass tru to the Relevant connection as an Iterable, so variable unpacking + can be used by the consuming class. This is used in the SecureCookie Model + class.""" + return iter(self.RelevantConnection()) + + def __del__(self): + """Cleans up all references, and closes all connectors""" + if self.debug: + print('Deleting model connections.') + for classname in self.__connectors: + if not hasattr(self.__connectors[classname], 'connection'): + continue + try: + self.__connections[classname].Disconnect() + except (NotImplementedError, TypeError, ConnectionError): + pass diff --git a/uweb3/connectors/Mongo.py b/uweb3/connectors/Mongo.py new file mode 100644 index 00000000..bc68480c --- /dev/null +++ b/uweb3/connectors/Mongo.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 +"""This file contains the connector for Mongo.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class Mongo(Connector): + """Adds MongoDB support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a MongoDB database connection.""" + self.debug = debug + import pymongo + self.options = options.get(self.Name(), {}) + try: + self.connection = pymongo.connection.Connection( + host=self.options.get('host', 'localhost'), + port=self.options.get('port', 27017)) + if 'database' in self.options: + self.connection = self.connection[self.options['database']] + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Disconnect(self): + """Closes the Mongo connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database', 'Unspecified'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/connectors/Mysql.py b/uweb3/connectors/Mysql.py new file mode 100644 index 00000000..77dad345 --- /dev/null +++ b/uweb3/connectors/Mysql.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 +"""This file contains the connector for Mysql.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class Mysql(Connector): + """Adds MySQL support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a MySQL database connection.""" + self.debug = debug + self.options = {'host': 'localhost', + 'user': None, + 'password': None, + 'database': ''} + try: + from ..libs.sqltalk import mysql + try: + self.options = options[self.Name()] + except KeyError: + pass + self.connection = mysql.Connect( + host=self.options.get('host', 'localhost'), + user=self.options.get('user'), + passwd=self.options.get('password'), + db=self.options.get('database'), + charset=self.options.get('charset', 'utf8'), + debug=self.debug) + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Rollback(self): + with self.connection as cursor: + return cursor.Execute("ROLLBACK") + + def Disconnect(self): + """Closes the MySQL connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/connectors/SignedCookie.py b/uweb3/connectors/SignedCookie.py new file mode 100644 index 00000000..d4a1b1a7 --- /dev/null +++ b/uweb3/connectors/SignedCookie.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +"""This file contains the connector for Signed cookies.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +import os +from base64 import b64encode + +from . import Connector + +class SignedCookie(Connector): + """Adds a signed cookie connection to the connection manager object. + + The name of the class is used as the Cookiename""" + + PERSISTENT = False + + def __init__(self, config, options, request, debug=False): + """Sets up the local connection to the signed cookie store, and generates a + new secret key if no key can be found in the config""" + self.debug = debug + # Generating random seeds on uWeb3 startup or fetch from config + try: + self.options = options[self.Name()] + self.secure_cookie_secret = self.options['secret'] + except KeyError: + secret = self.GenerateNewKey() + config.Create(self.Name(), 'secret', secret) + if self.debug: + print('SignedCookie: Wrote new secret random to config.') + self.secure_cookie_secret = secret + self.connection = (request, request.vars['cookie'], self.secure_cookie_secret) + + @staticmethod + def GenerateNewKey(length=128): + return b64encode(os.urandom(length)).decode('utf-8') diff --git a/uweb3/connectors/SqlAlchemy.py b/uweb3/connectors/SqlAlchemy.py new file mode 100644 index 00000000..b934a29c --- /dev/null +++ b/uweb3/connectors/SqlAlchemy.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +"""This file contains the connector for SqlAlchemy.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class SqlAlchemy(Connector): + """Adds MysqlAlchemy connection to ConnectionManager.""" + + def __init__(self, config, options, request, debug=False): + """Returns a Mysql database connection wrapped in a SQLAlchemy session.""" + from sqlalchemy.orm import sessionmaker + self.debug = debug + self.options = {'host': 'localhost', + 'user': None, + 'password': None, + 'database': ''} + try: + self.options = options[self.Name()] + except KeyError: + pass + Session = sessionmaker() + Session.configure(bind=self.engine, expire_on_commit=False) + try: + self.connection = Session() + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def engine(self): + from sqlalchemy import create_engine + return create_engine('mysql://{username}:{password}@{host}/{database}'.format( + username=self.options.get('user'), + password=self.options.get('password'), + host=self.options.get('host', 'localhost'), + database=self.options.get('database')), + pool_size=5, + max_overflow=0, + encoding=self.options.get('charset', 'utf8'),) diff --git a/uweb3/connectors/Sqlite.py b/uweb3/connectors/Sqlite.py new file mode 100644 index 00000000..9e9780d1 --- /dev/null +++ b/uweb3/connectors/Sqlite.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 +"""This file contains the connector for Sqlite.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class Sqlite(Connector): + """Adds SQLite support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a SQLite database connection. + The name of the class is used as the local filename. + """ + from ..libs.sqltalk import sqlite + self.debug = debug + self.options = options[self.Name()] + try: + self.connection = sqlite.Connect(self.options.get('database')) + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Rollback(self): + """Rolls back any uncommited transactions.""" + return self.connection.rollback() + + def Disconnect(self): + """Closes the SQLite connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/connectors/__init__.py b/uweb3/connectors/__init__.py new file mode 100644 index 00000000..16995f0c --- /dev/null +++ b/uweb3/connectors/__init__.py @@ -0,0 +1,44 @@ +#!/usr/bin/python3 +"""This file contains the Base connector for model connections and imports all +available connectors.""" + +__author__ = 'Jan Klopper (jan@underdark.nl)' +__version__ = 0.1 + +class Connector(object): + """Base Connector class, subclass from this to create your own connectors. + Usually the name of your class is used to lookup its config in the + configuration file, or the database or local filename. + + Connectors based on this class are Usually Singletons. One global connection + is kept alive, and multiple model classes use it to connect to their + respective tables, cookies, or files. + """ + _NAME = None + + @classmethod + def Name(cls): + """Returns the 'connector' name, which is usally used to lookup its config + in the config file. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + """ + if cls._NAME: + return cls._NAME + name = cls.__name__ + return name[0].lower() + name[1:] + + def Disconnect(self): + """Standard interface to disconnect from data source""" + raise NotImplementedError + + def Rollback(self): + """Standard interface to rollback any pending commits""" + raise NotImplementedError + +from .SignedCookie import SignedCookie +from .Mysql import Mysql +from .Mongo import Mongo +from .Sqlite import Sqlite +from .SqlAlchemy import SqlAlchemy diff --git a/uweb3/ext_lib/underdark/libs/__init__.py b/uweb3/ext_lib/underdark/libs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/__init__.py b/uweb3/ext_lib/underdark/libs/sqltalk/mysql/__init__.py deleted file mode 100644 index 97e2028c..00000000 --- a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python2.5 -"""SQLTalk MySQL interface package. - -This package provides a full SQLTalk interface against a MySQL database. -Required MySQL version is unknown but expected to be 4, however, migrating to -MySQL version 5 is in everyone's best interest :). - -This package is built on a heavily modified version of MySQLdb 1.2.2 which was -originally written by Andy Dustman 4.1 returns TIMESTAMP in the same format as DATETIME - if len(stamp) == 19: - return DateTimeOrNone(stamp) - try: - stamp = stamp.ljust(14, '0') - return INTERPRET_AS_UTC(datetime.datetime( - *map(int, (stamp[:4], stamp[4:6], stamp[6:8], - stamp[8:10], stamp[10:12], stamp[12:14])))) - except ValueError: - return None - - -def TimeDeltaOrNone(string): - """Converts an input string to a datetime.timedelta object. - - Returns: - datetime.timedelta object, or None if input is bad. - """ - try: - hour, minute, second = map(float, string.split(':')) - return (-1, 1)[hour > 0] * datetime.timedelta( - hours=hour, minutes=minute, seconds=second) - except ValueError: - return None - - -def TimeFromTicks(ticks): - """Convert UNIX ticks into a time instance.""" - return INTERPRET_AS_UTC(datetime.time(*time.gmtime(ticks)[3:6])) - - -def TimeOrNone(string): - """Converts an input string to a datetime.time object. - - Returns: - datetime.time object, or None if input is bad. - """ - try: - hour, minute, second = string.split(':') - micro, second = math.modf(float(second)) - return INTERPRET_AS_UTC(datetime.time( - hour=int(hour), minute=int(minute), - second=int(second), microsecond=int(micro * 1000000))) - except ValueError: - return None - - -def TimestampFromTicks(ticks): - """Convert UNIX ticks into a datetime instance.""" - return INTERPRET_AS_UTC(datetime.datetime.utcfromtimestamp(ticks)) diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/__init__.py b/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/__init__.py deleted file mode 100644 index e28c74e0..00000000 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python2.5 -"""SQLTalk SQLite interface package.""" - -# Standard modules -import _sqlite3 - -# Application specific modules -import connection - -VERSION_INFO = tuple(map(int, _sqlite3.version.split('.'))) -SQLITE_VERSION_INFO = tuple(map(int, _sqlite3.sqlite_version.split('.'))) - - -def Connect(*args, **kwds): - """Factory function for connection.Connection.""" - kwds['detect_types'] = _sqlite3.PARSE_DECLTYPES - return connection.Connection(*args, **kwds) - - -def ThreadConnect(*args, **kwds): - """Factory function for connection.ThreadedConnection.""" - kwds['detect_types'] = _sqlite3.PARSE_DECLTYPES - return connection.ThreadedConnection(*args, **kwds) - - -DataError = _sqlite3.DataError -DatabaseError = _sqlite3.DatabaseError -Error = _sqlite3.Error -IntegrityError = _sqlite3.IntegrityError -InterfaceError = _sqlite3.InterfaceError -InternalError = _sqlite3.InternalError -NotSupportedError = _sqlite3.NotSupportedError -OperationalError = _sqlite3.OperationalError diff --git a/uweb3/ext_lib/underdark/libs/urlsplitter/__init__.py b/uweb3/ext_lib/underdark/libs/urlsplitter/__init__.py deleted file mode 100644 index f9d91af5..00000000 --- a/uweb3/ext_lib/underdark/libs/urlsplitter/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/python3 -"""This module is used to split an url into a dict and simultaneously validates -if the url is valid or not. - -Every url provided should start with http:// or https:// otherwise it is seen as invalid. -""" - -__author__ = 'Stef van Houten (stefvanhouten@gmail.com)' -__version__ = 0.1 - -import re - -from tld import get_tld - -def split_url(url): - # If fail_silently is True return None if url suffix is invalid - if not isinstance(url, str): - raise Exception("Url should be a string") - - suffix = get_tld(url, fail_silently=True) - - if not suffix: - # TODO: What should the function return on a invalid url suffix? - raise Exception("Url not valid") - - # Get the leftovers after the suffix - route = url[url.find(suffix) + len(suffix):] - - # Use regex to filter out the https:// or www. - regex = re.compile(r"https?://(www\.)?") - domain = regex.sub('', url).strip().strip('/')[:-(len(route) + len(suffix))] - - url_type = url[:url.find(domain)] - - # Remove trailing dot if there is any - if domain[-1] == ".": - domain = domain[:-1] - - if url_type[-1] == ".": - url_type = url_type[:-1] - - return { - 'type': url_type, - 'domain': domain, - 'suffix': suffix, - 'route': target, - } diff --git a/uweb3/ext_lib/underdark/libs/urlsplitter/test.py b/uweb3/ext_lib/underdark/libs/urlsplitter/test.py deleted file mode 100644 index 7570148e..00000000 --- a/uweb3/ext_lib/underdark/libs/urlsplitter/test.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -from __init__ import split_url - -class testUrlSplitter(unittest.TestCase): - - def testValidUrl(self): - """See if we correctly clean up header injection attemps""" - self.assertEqual(split_url('https://test.test.com'), { - 'type': 'https://', - 'domain': 'test.test', - 'suffix': 'com', - 'target': '' - }) - - self.assertEqual(split_url('https://test.test.com/someurl'), { - 'type': 'https://', - 'domain': 'test.test', - 'suffix': 'com', - 'target': '/someurl' - }) - - self.assertEqual(split_url('https://www.google.co.uk'), { - 'type': 'https://www', - 'domain': 'google', - 'suffix': 'co.uk', - 'target': '' - }) - - self.assertEqual( split_url('https://www.test.com.nl.de'), { - 'type': 'https://www', - 'domain': 'test.com.nl', - 'suffix': 'de', - 'target': '' - }) - - - def testInvalidUrl(self): - message = 'Url not valid' - with self.assertRaises(Exception) as context: - split_url('www.google.com') - self.assertTrue(message in str(context.exception)) - - with self.assertRaises(Exception) as context: - split_url(' ') - self.assertTrue(message in str(context.exception)) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/uweb3/helpers.py b/uweb3/helpers.py deleted file mode 100644 index f64d75d6..00000000 --- a/uweb3/helpers.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import time -import mimetypes -import datetime -from wsgiref.headers import Headers -from uweb3.pagemaker import MimeTypeDict - - -RFC_1123_DATE = '%a, %d %b %Y %T GMT' - -# Search File -def is_accessible(abs_file_path): - return ( - os.path.exists(abs_file_path) and - os.path.isfile(abs_file_path) and - os.access(abs_file_path, os.R_OK) - ) - - -def search_file(relative_file_path, dirs): - for d in dirs: - if not os.path.isabs(d): - d = os.path.abspath(d) + os.sep - - file = os.path.join(d, relative_file_path) - if is_accessible(file): - return file - - -# Header utils -def get_content_length(filename): - stats = os.stat(filename) - return str(stats.st_size) - - -def generate_last_modified(filename): - stats = os.stat(filename) - last_modified = time.strftime("%a, %d %b %Y %H:%M:%sS GMT", time.gmtime(stats.st_mtime)) - return last_modified - - -def get_content_type(mimetype, charset): - if mimetype.startswith('text/') or mimetype == 'application/javascript': - mimetype += '; charset={}'.format(charset) - return mimetype - - -# Response body iterator -def _iter_and_close(file_obj, block_size, charset): - """Yield file contents by block then close the file.""" - while True: - try: - block = file_obj.read(block_size) - if block: - if isinstance(block, bytes): - yield block - else: - yield block.encode(charset) - else: - raise StopIteration - except StopIteration: - file_obj.close() - break - - -def _get_body(filename, method, block_size, charset): - if method == 'HEAD': - return [b''] - return _iter_and_close(open(filename, 'rb'), block_size, charset) - - -# View functions -def static_file_view(env, start_response, filename, block_size, charset, CACHE_DURATION): - method = env['REQUEST_METHOD'].upper() - if method not in ('HEAD', 'GET'): - start_response('405 METHOD NOT ALLOWED', - [('Content-Type', 'text/plain; UTF-8')]) - return [b''] - mimetype, encoding = mimetypes.guess_type(filename) - headers = Headers([]) - - cache_days = CACHE_DURATION.get(mimetype, 0) - expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) - headers.add_header('Cache-control', f'public, max-age={expires.strftime(RFC_1123_DATE)}') - headers.add_header('Expires', expires.strftime(RFC_1123_DATE)) - if env.get('HTTP_IF_MODIFIED_SINCE'): - if env.get('HTTP_IF_MODIFIED_SINCE') >= generate_last_modified(filename): - start_response('304 ok', headers.items()) - return [b'304'] - headers.add_header('Content-Encodings', encoding) - if mimetype: - headers.add_header('Content-Type', get_content_type(mimetype, charset)) - headers.add_header('Content-Length', get_content_length(filename)) - headers.add_header('Last-Modified', generate_last_modified(filename)) - headers.add_header("Accept-Ranges", "bytes") - start_response('200 OK', headers.items()) - return _get_body(filename, method, block_size, charset) - - -def http404(env, start_response): - start_response('404 Not Found', - [('Content-type', 'text/plain; charset=utf-8')]) - return [b'404 Not Found'] - - -class StaticMiddleware: - CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7}) - - def __init__(self, app, static_root, static_dirs=None, - block_size=16*4096, charset='UTF-8'): - self.app = app - self.static_root = static_root.lstrip('/').rstrip('/') - if static_dirs is None: - static_dirs = [os.path.join(os.path.abspath('.'), 'static')] - self.static_dirs = static_dirs - self.charset = charset - self.block_size = block_size - - def __call__(self, env, start_response): - path = env['PATH_INFO'].lstrip('/') - if path.startswith(self.static_root): - relative_file_path = '/'.join(path.split('/')[1:]) - p = os.path.join(self.static_dirs[0], relative_file_path) - if os.path.commonprefix((os.path.realpath(p), self.static_dirs[0])) != self.static_dirs[0]: - return http404(env, start_response) - return self.handle(env, start_response, relative_file_path) - return self.app(env, start_response) - - def handle(self, env, start_response, filename): - abs_file_path = search_file(filename, self.static_dirs) - if abs_file_path: - res = static_file_view(env, start_response, abs_file_path, - self.block_size, self.charset, self.CACHE_DURATION) - return res - else: - return http404(env, start_response) - - \ No newline at end of file diff --git a/uweb3/ext_lib/underdark/__init__.py b/uweb3/libs/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/__init__.py rename to uweb3/libs/__init__.py diff --git a/uweb3/libs/mail.py b/uweb3/libs/mail.py new file mode 100644 index 00000000..60e6ad10 --- /dev/null +++ b/uweb3/libs/mail.py @@ -0,0 +1,154 @@ +#!/usr/bin/python3 +"""Module to send emails through an smtp server""" + +__author__ = 'Elmer de Looff ' +__version__ = '0.3' + +# Standard modules +import base64 +import os +import smtplib +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from uweb3.libs.safestring import EmailAddresssafestring, EmailHeadersafestring + + +class MailError(Exception): + """Something went wrong sending your email""" + + +class MailSender: + """Easy context-interface for sending mail.""" + def __init__(self, host='localhost', port=25, + local_hostname=None, timeout=5): + """Sets up the connection to the SMTP server. + + Arguments: + % host: str ~~ 'localhost' + The SMTP hostname to connect to. + % port: int ~~ 25 + Port for the SMTP server. + % local_hostname: str ~~ from local hostname + The hostname for which we want to send messages. + % timeout: int ~~ 5 + Timeout in seconds. + """ + self.server = None + self.options = {'host': host, 'port': port, + 'local_hostname': local_hostname or os.uname()[1], + 'timeout': timeout} + + def __enter__(self): + """Returns a SendMailContext for sending emails.""" + try: + self.server = smtplib.SMTP(**self.options) + except ConnectionRefusedError as error: + raise SMTPConnectError(error, 'Connection refused.') + return SendMailContext(self.server) + + def __exit__(self, *_exc_args): + """Done sending mail, closes the smtp server connection.""" + self.server.quit() + + +class SendMailContext: + """Context to use for sending emails.""" + def __init__(self, server): + """Stores the server object locally.""" + self.server = server + + def Text(self, recipients, subject, content, + sender=None, reply_to=None, charset='utf8'): + """Send a text message + + Arguments: + @ recipients: str / list of str + Email address(es) of all TO: recipients. + @ subject: str + Email subject + @ content: str + Body of the email + % sender: str ~~ self.Noreply() + The sender email addres, this defaults to the no-reply address. + % reply_to: str ~~ None + Optional reply-to address that differs from sender. + % charset: str ~~ 'utf8' + Character set to encode mail to. + """ + message = MIMEMultipart() + message['From'] = EmailAddresssafestring('') + (sender or self.Noreply()) + message['To'] = self.ParseRecipients(recipients) + message['Subject'] = EmailHeadersafestring('') + ' '.join(subject.strip().split()) + message.attach(MIMEText(content.encode(charset), 'plain', charset)) + if reply_to: + message['Reply-to'] = self.ParseRecipients(reply_to) + self.server.sendmail(message['From'], recipients, message.as_string()) + + def Attachments(self, recipients, subject, content, + attachments, sender=None, reply_to=None, charset='utf8'): + """Sends email with attachments. + + Arguments like `Text()` but adds `attachments` after content. This should + be a list of `str` (filename), `file` or 2-tuples with name and content. + Content in case of 2-tuple can be `str` or any file-like object. + """ + message = MIMEMultipart() + message['From'] = EmailAddresssafestring('') + (sender or self.Noreply()) + message['To'] = self.ParseRecipients(recipients) + message['Subject'] = EmailHeadersafestring('') + ' '.join(subject.strip().split()) + if reply_to: + message['Reply-to'] = self.ParseRecipients(reply_to) + message.attach(MIMEText(content.encode(charset), 'plain', charset)) + if isinstance(attachments, str): + message.attach(self.ParseAttachment(attachments)) + else: + for attachment in attachments: + message.attach(self.ParseAttachment(attachment)) + self.server.sendmail(message['From'], recipients, str(message)) + + @staticmethod + def ParseAttachment(attachment): + """Parses an attachment descriptor and returns a MIMEBase part for email.""" + if isinstance(attachment, tuple): + name, contents = attachment + if hasattr(contents, 'read'): + contents = contents.read() + elif isinstance(attachment, str): + name = os.path.basename(attachment) + contents = file(attachment, 'rb').read() + elif isinstance(attachment, file): + name = os.path.basename(attachment.name) + attachment.seek(0) + contents = attachment.read() + + part = MIMEBase('application', 'octet-stream') + part.set_payload(Wrap(base64.b64encode(contents))) + part.add_header('Content-Transfer-Encoding', 'base64') + part.add_header('Content-Disposition', 'attachment; filename="%s"' % name) + return part + + @staticmethod + def ParseRecipients(recipients): + """Ensures multiple recipients are returned as a safestring without + newlines.""" + if isinstance(recipients, str): + return EmailAddresssafestring('') + recipients + return EmailAddresssafestring('') + ', '.join(recipients) + + def Noreply(self): + """Returns the no-reply email address for the configured local hostname.""" + return 'no-reply ' % self.server.local_hostname + + +def Wrap(content, cols=76): + """Wraps multipart mime content into 76 column lines for niceness.""" + lines = [] + while content: + lines.append(content[:cols]) + content = content[cols:] + return '\r\n'.join(lines) + + +SMTPConnectError = smtplib.SMTPConnectError +SMTPRecipientsRefused = smtplib.SMTPRecipientsRefused diff --git a/uweb3/ext_lib/underdark/libs/safestring/__init__.py b/uweb3/libs/safestring/__init__.py similarity index 60% rename from uweb3/ext_lib/underdark/libs/safestring/__init__.py rename to uweb3/libs/safestring/__init__.py index ed32b72e..d9e37d45 100644 --- a/uweb3/ext_lib/underdark/libs/safestring/__init__.py +++ b/uweb3/libs/safestring/__init__.py @@ -20,24 +20,30 @@ type. Handy escape() functions are present to do manual escaping if required. """ -#TODO: logger geen enters -#bash injection -#mysql escaping +#TODO: logger, dont output Enters +# bash injection +# mysql escaping __author__ = 'Jan Klopper (jan@underdark.nl)' __version__ = 0.1 import html +from json import JSONEncoder import json import urllib.parse as urlparse import re -from ast import literal_eval -from sqlalchemy import text + +# json encoder modules +import datetime +import uuid +from uweb3 import model + class Basesafestring(str): """Base safe string class This does not signal any safety against injection itself, use the child - classes instead!""" - "" + classes instead! + """ + def __add__(self, other): """Adds a second string to this string, upgrading it in the process""" data = ''.join(( # do not use the __add__ since that creates a loop @@ -50,11 +56,10 @@ def __upgrade__(self, other): the current object""" if type(other) == self.__class__: #same type, easy, lets add return other - elif isinstance(other, Basesafestring): # lets unescape the other 'safe' type, + if isinstance(other, Basesafestring): # lets unescape the other 'safe' type, otherdata = other.unescape(other) # its escaping is not needed for his context return self.escape(otherdata) # escape it using our context - else: - return self.escape(str(other)) # escape it using our context + return self.escape(str(other)) # escape it using our context def __new__(cls, data, **kwargs): return super().__new__(cls, @@ -81,47 +86,60 @@ def escape(self, data): def unescape(self, data): raise NotImplementedError + def join(self, items): + output = [] + for item in items: + output.append(self.__upgrade__(item)) + return self.__class__(''.join(output)) + class SQLSAFE(Basesafestring): CHARS_ESCAPE_DICT = { - '\0' : '\\0', - '\b' : '\\b', - '\t' : '\\t', - '\n' : '\\n', - '\r' : '\\r', - '\x1a' : '\\Z', - '"' : '\\"', - '\'' : '\\\'', - '\\' : '\\\\' + '\0': '\\0', + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\r': '\\r', + '\x1a': '\\Z', + '"': '\\"', + '\'': '\\\'', + '\\': '\\\\' } - + + CHARS_UNESCAPE_DICT = { + '\\0': '\0', + '\\b': '\b', + '\\t': '\t', + '\\n': '\n', + '\\r': '\r', + '\\Z': '\x1a', + '\\"': '"', + '\\\'': '\'', + '\\\\': '\\' + } + CHARS_ESCAPE_REGEX = re.compile(r"""[\0\b\t\n\r\x1a\"\'\\]""") + CHARS_UNESCAPE_REGEX = re.compile(r"""((\\t)|(\\0)|(\\n)|(\\b)|(\\r)|(\\Z)|(\\")|(\\\\')|(\\\\\\))""") PLACEHOLDERS_REGEX = re.compile(r"""\?+""") QUOTES_REGEX = re.compile(r"""([\"'])(?:(?=(\\?))\2.)*?\1""", re.DOTALL) - def __new__(cls, data, values=(), *args, **kwargs): - return super().__new__(cls, - cls.escape(cls, str(data), values) if 'unsafe' in kwargs else data) - def __upgrade__(self, other): - """Upgrade a given object to be as safe, and in the same safety context as - the current object""" - if type(other) == self.__class__: #same type, easy, lets add - return other - elif isinstance(other, Basesafestring): # lets unescape the other 'safe' type, - otherdata = other.unescape(other) # its escaping is not needed for his context - return self.sanitize(otherdata) # escape it using our context - else: - other = " " + other - return self.sanitize(other, qoutes=False) + """Upgrade a given object to be as safe, and in the same safety context as + the current object + """ + if type(other) == self.__class__: #same type, easy, lets add + return other + elif isinstance(other, Basesafestring): # lets unescape the other 'safe' type, + return self.sanitize(other.unescape(other)) # escape it using our context + return self.sanitize(" " + other, with_quotes=False) @classmethod - def sanitize(cls, value, qoutes=True): + def sanitize(cls, value, with_quotes=True): index = 0 escaped = "" if len(cls.CHARS_ESCAPE_REGEX.findall(value)) == 0: if not str.isdigit(value): - if qoutes: + if with_quotes: return f"'{value}'" return value return value @@ -130,30 +148,54 @@ def sanitize(cls, value, qoutes=True): index = m.span()[1] escaped += value[index:] if not str.isdigit(escaped): - if qoutes: + if with_quotes: return f"'{escaped}'" return escaped return escaped - def escape(cls, sql, values): + def escape(self, sql, values): x = 0 escaped = "" if not isinstance(values, tuple): raise ValueError("Values should be a tuple") - if len(cls.PLACEHOLDERS_REGEX.findall(sql)) != len(values): + + if len(self.PLACEHOLDERS_REGEX.findall(sql)) != len(values): raise ValueError("Number of values does not match number of replacements") - for index, m in enumerate(cls.PLACEHOLDERS_REGEX.finditer(sql)): - escaped += sql[x:m.span()[0]] + cls.sanitize(values[index]) + + for index, m in enumerate(self.PLACEHOLDERS_REGEX.finditer(sql)): + escaped += sql[x:m.span()[0]] + self.sanitize(values[index]) x = m.span()[1] escaped += sql[x:] return SQLSAFE(escaped) - - + + def unescape(self, value): + if not isinstance(value, SQLSAFE): + raise ValueError(f"The value needs to be an instance of the SQLSAFE class and not of type: {type(value)}") + x = 0 + escaped = "" + for index, m in enumerate(self.CHARS_UNESCAPE_REGEX.finditer(value)): + escaped += value[x:m.span()[0]] + target = value[m.span()[0]:m.span()[1]] + escaped += self.CHARS_UNESCAPE_DICT.get(target) + x = m.span()[1] + escaped += value[x:] + return SQLSAFE(escaped) + + # what follows are the actual useable classes that are safe in specific contexts +class Unsafestring(Basesafestring): + """This class removes any escaping done""" + + def escape(self, data): + return data + + def unescape(self, data): + return data + + class HTMLsafestring(Basesafestring): """This class signals that the content is HTML safe""" - - + def escape(self, data): return html.escape(data) @@ -166,18 +208,44 @@ class JSONsafestring(Basesafestring): Most of this will be handled by just feeding regular python objects into json.dumps, but for some outputs this might be handy. Eg, when outputting - partial json into dynamic generated javascript files""" + partial json into dynamic generated javascript files + """ + + def __new__(cls, data, **kwargs): + if isinstance(data, str): + data = cls.escape(cls, str(data)) if 'unsafe' in kwargs else data + else: + data = json.dumps(data, cls=JsonEncoder) + return super().__new__(cls, data) def escape(self, data): - if not isinstance(data, str): - raise TypeError - return json.dumps(data) + return json.dumps(data, cls=JsonEncoder) def unescape(self, data): if not isinstance(data, str): raise TypeError - data = json.loads(data) - return data + return json.loads(data) + +class JsonEncoder(json.JSONEncoder): + def default (self, o): + if isinstance(o, datetime.datetime): + return o.strftime('%F %T') + if isinstance(o, datetime.date): + return o.strftime('%F') + if isinstance(o, datetime.time): + return o.strftime('%T') + if isinstance(o, uuid.UUID): + return str(o) + if hasattr(o, "__json__"): + return str(o.__json__()) + if hasattr(o, "__html__"): + return str(o.__html__()) + if hasattr(o, "__dict__"): + return o.__dict__ + try: + return super().default(o) + except TypeError: + return str(o) class URLqueryargumentsafestring(Basesafestring): @@ -194,7 +262,8 @@ def unescape(self, data): class URLsafestring(Basesafestring): """This class signals that the content is URL safe, for use in http headers - like redirects, but also calls to wget or the like""" + like redirects, but also calls to wget or the like + """ def escape(self, data): """Drops everything that does not fit in a url @@ -216,10 +285,11 @@ def unescape(self, data): class EmailAddresssafestring(Basesafestring): - """This class signals that the content is safe Email address + """This class signals that the content is a safe Email address - ITs usefull when sending out emails or constructing email headers - Email Header injection is subverted.""" + Its usefull when sending out emails or constructing email headers + Email Header injection is subverted. + """ def escape(self, data): """Drops everything that does not fit in an email address""" @@ -234,3 +304,17 @@ def escape(self, data): def unescape(self, data): """Can't unremove non address elements so we'll just return the string""" return data + + +class EmailHeadersafestring(Basesafestring): + """This class signals that the content is a safe Email header + + Its usefull when sending out emails or constructing email headers.""" + + def escape(self, data): + """Drops everything that does not fit in a email header""" + return data.replace("\n", "").replace("\r", "") + + def unescape(self, data): + """Can't unremove non header elements so we'll just return the string""" + return data diff --git a/uweb3/ext_lib/underdark/libs/safestring/test.py b/uweb3/libs/safestring/test.py similarity index 61% rename from uweb3/ext_lib/underdark/libs/safestring/test.py rename to uweb3/libs/safestring/test.py index d0f36b8a..b7f9db3d 100755 --- a/uweb3/ext_lib/underdark/libs/safestring/test.py +++ b/uweb3/libs/safestring/test.py @@ -7,7 +7,7 @@ import unittest #custom modules -from uweb3.ext_lib.underdark.libs.safestring import URLsafestring, SQLSAFE, HTMLsafestring, URLqueryargumentsafestring, JSONsafestring, EmailAddresssafestring, Basesafestring +from uweb3.libs.safestring import URLsafestring, SQLSAFE, HTMLsafestring, URLqueryargumentsafestring, JSONsafestring, EmailAddresssafestring, Basesafestring class BasesafestringMethods(unittest.TestCase): def test_creation_str(self): @@ -70,6 +70,20 @@ def test_format_keyword(self): testdata = HTMLsafestring('foo {kw} test').format(kw='') self.assertEqual(testdata, 'foo <b> test') + def test_join(self): + """Tests to join two already safe list items""" + testdata = HTMLsafestring('').join((HTMLsafestring(''), + HTMLsafestring(''))) + self.assertEqual(testdata, '') + self.assertIsInstance(testdata, HTMLsafestring) + + def test_join_unsafe(self): + """Test a join over possibly insafe and safe strings combined""" + testdata = HTMLsafestring('').join(('', + HTMLsafestring(''))) + self.assertEqual(testdata, '<b>') + self.assertIsInstance(testdata, HTMLsafestring) + class TestJSonStringMethods(unittest.TestCase): def test_addition(self): @@ -100,6 +114,20 @@ def test_unsafe_init(self): self.assertEqual(testdata, 'jan@underdark.nl') class TestSQLSAFEMethods(unittest.TestCase): + def test_user_supplied_safe_value(self): + user_supplied_safe_object = SQLSAFE("SELECT * FROM users WHERE username = 'username\t'") + self.assertEqual(user_supplied_safe_object, "SELECT * FROM users WHERE username = 'username\t'") + self.assertIsInstance(user_supplied_safe_object, SQLSAFE) + + def test_escaping_wrong_values_type(self): + with self.assertRaises(ValueError): + self.assertRaises(SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=["username'"], unsafe=True)) + + def test_escaping_uneven_replacements_and_values(self): + with self.assertRaises(ValueError): + self.assertRaises(SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=["username'", "test"], unsafe=True)) + self.assertRaises(SQLSAFE("""SELECT * FROM users WHERE username = ? AND name=?""", values=["username'"], unsafe=True)) + def test_escaping(self): testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) self.assertEqual(testdata, "SELECT * FROM users WHERE username = 'username\\''") @@ -107,7 +135,6 @@ def test_escaping(self): self.assertEqual(testdata, "SELECT * FROM users WHERE username = 'username\\\"'") testdata = SQLSAFE("""SELECT * FROM users WHERE username = ? AND ? """, values=('username"', "password"), unsafe=True) - def test_concatenation(self): testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) other = "AND firstname='test'" @@ -116,5 +143,30 @@ def test_concatenation(self): other = "AND firstname='test'" self.assertEqual(testdata + other, "SELECT * FROM users WHERE username = 'username\\\"' AND firstname=\\'test\\'") + def test_unescape_wrong_type(self): + """Validate if the string we are trying to unescape is part of an SQLSAFE instance""" + testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) + with self.assertRaises(ValueError): + self.assertRaises(testdata.unescape('whatever')) + + def test_correct_escape_character(self): + """Validate that all characters are escaped as expected""" + self.assertEqual(SQLSAFE.sanitize('\0', with_quotes=False), '\\0') + self.assertEqual(SQLSAFE.sanitize('\b', with_quotes=False), '\\b') + self.assertEqual(SQLSAFE.sanitize('\t', with_quotes=False), '\\t') + self.assertEqual(SQLSAFE.sanitize('\n', with_quotes=False), '\\n') + self.assertEqual(SQLSAFE.sanitize('\r', with_quotes=False), '\\r') + self.assertEqual(SQLSAFE.sanitize('\x1a', with_quotes=False), '\\Z') + self.assertEqual(SQLSAFE.sanitize('"', with_quotes=False), '\\"') + self.assertEqual(SQLSAFE.sanitize('\'', with_quotes=False), '\\\'') + self.assertEqual(SQLSAFE.sanitize('\\', with_quotes=False), '\\\\') + + def test_unescape(self): + """Validate that the string is converted back to the original after escaping and unescaping""" + testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username\t \t",), unsafe=True) + self.assertEqual(testdata, "SELECT * FROM users WHERE username = 'username\\t \\t'") + self.assertEqual(testdata.unescape(testdata), "SELECT * FROM users WHERE username = 'username\t \t'") + + if __name__ == '__main__': unittest.main() diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/__init__.py b/uweb3/libs/sqltalk/__init__.py similarity index 80% rename from uweb3/ext_lib/underdark/libs/sqltalk/__init__.py rename to uweb3/libs/sqltalk/__init__.py index 2981d79f..cf45f629 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/__init__.py +++ b/uweb3/libs/sqltalk/__init__.py @@ -1,11 +1,11 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """Easy use SQL abstraction module. Returns a custom QueryResult object that holds the result, query, used character set and various other small statistics. The QueryResult object also support pivoting and subselects -Currently only implements a MySQL abstraction module with a stripped down +Currently implements a MySQL and sqlite abstraction module with a stripped down version of MySQLdb internally. example usage: diff --git a/uweb3/libs/sqltalk/mysql/__init__.py b/uweb3/libs/sqltalk/mysql/__init__.py new file mode 100644 index 00000000..d471c984 --- /dev/null +++ b/uweb3/libs/sqltalk/mysql/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +"""SQLTalk MySQL interface package. + +Functions: + Connect: Connects to a MySQL server and returns a connection object. + Refer to the documentation enclosed in the connections module for + argument information. +""" +__author__ = 'Jan Klopper ' +__version__ = '0.10' + +# Application specific modules +from . import connection + +def Connect(*args, **kwargs): + """Factory function for connection.Connection.""" + return connection.Connection(*args, **kwargs) diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/connection.py b/uweb3/libs/sqltalk/mysql/connection.py similarity index 83% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/connection.py rename to uweb3/libs/sqltalk/mysql/connection.py index e9f059b7..c6520bb0 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/connection.py +++ b/uweb3/libs/sqltalk/mysql/connection.py @@ -1,10 +1,11 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """This module implements the Connection class, which sets up a connection to a MySQL database. From this connection, cursor objects can be created, which use the escaping and character encoding facilities offered by the connection. """ -__author__ = 'Elmer de Looff ' -__version__ = '0.16' +__author__ = ('Elmer de Looff ', + 'Jan Klopper ') +__version__ = '0.17' # Standard modules import pymysql @@ -13,8 +14,8 @@ import weakref # Application specific modules -from . import constants -from . import converters +from pymysql import constants +from pymysql import converters from . import cursor from .. import sqlresult @@ -118,7 +119,7 @@ def StringDecoder(string): encoders = {} converts = {} - conversions = converters.CONVERSIONS + conversions = converters.conversions self.string_decoder = _GetStringDecoder() # if use_unicode: @@ -151,7 +152,6 @@ def StringDecoder(string): # self.encoders[unicode] = self.unicode_literal = _GetUnicodeLiteral() self.converter = conversions - self.server_version = tuple(map(int, self.get_server_info().split('.')[:2])) if sql_mode: self.SetSqlMode(sql_mode) @@ -159,11 +159,11 @@ def StringDecoder(string): self.transactional = bool(self.server_capabilities & constants.CLIENT.TRANSACTIONS) - self._autocommit = None + self.autocommit_mode = None if autocommit is not None: - self.autocommit = autocommit + self.autocommit_mode = autocommit else: - self.autocommit = not self.transactional + self.autocommit_mode = not self.transactional def __enter__(self): """Refreshes the connection and returns a cursor, starting a transaction.""" @@ -181,30 +181,38 @@ def __exit__(self, exc_type, exc_value, _exc_traceback): self.ResetTransactionTimer() if exc_type: self.rollback() - self.logger.exception( - 'The transaction was rolled back after an exception.\n' - 'Server: %s\nQueries in transaction (last one triggered):\n\n%s', - self.get_host_info(), - '\n\n'.join(self.queries)) + if self.debug: + self.logger.exception( + 'The transaction was rolled back after an exception.\n' + 'Server: %s\nQueries in transaction (last one triggered):\n\n%s', + self.get_host_info(), + '\n\n'.join(self.queries)) else: self.commit() - self.logger.debug( - 'Transaction committed (server: %r).', self.get_host_info()) + if self.debug: + self.logger.debug( + 'Transaction committed (server: %r).', self.get_host_info()) self.lock.release() def CurrentDatabase(self): """Return the name of the currently used database""" return self.Query('SELECT DATABASE()')[0] - def EscapeField(self, field): - """Returns a SQL escaped field or table name.""" + def EscapeField(self, field, multiple=False): + """Returns a SQL escaped field or table name. + + Set multiple = True if field is a tuple of names to be escaped. + If multiple = False, and a tuple is encountered `field` as `name` will be + returned where the second part of the tuple is the `name` part. + """ if not field: return '' - elif isinstance(field, str): + if isinstance(field, str): fields = '.'.join('`%s`' % f.replace('`', '``') for f in field.split('.')) return fields.replace('`*`', '*') - else: - return map(self.EscapeField, field) + elif not multiple and isinstance(field, tuple): + return '%s as %s' % (self.EscapeField(field[0]), self.EscapeField(field[1])) + return map(self.EscapeField, field) def EscapeValues(self, obj): """Escapes any object passed in following the encoders dictionary. @@ -212,28 +220,35 @@ def EscapeValues(self, obj): Sequences and mappings will only have their contents escaped. All strings will be encoded to the connection's character set. """ + if isinstance(obj, tuple) or isinstance(obj, list): + return list(map(lambda x: self.escape(x, self.encoders), obj)) return self.escape(obj, self.encoders) def Info(self): """Returns a dictionary of MySQL server info and current active database. Returns - dictionary: keys: 'db', 'charset', 'server' + dictionary: keys: 'db', 'charset', 'server', 'debug', 'autocommit', + 'querycount', 'transactioncount' """ - # TODO(Elmer): Make this return more useful information and statistics return {'db': self.CurrentDatabase(), - 'charset': self.charset, - 'server': self.ServerInfo()} - - def Query(self, query_string): + 'charset': self._GetCharacterSet(), + 'server': self.ServerInfo(), + 'debug': self.debug, + 'autocommit': self.autocommit_mode, + 'querycount': self.counter_queries, + 'transactioncount': self.counter_transactions} + + def Query(self, query_string, cur=None): self.counter_queries += 1 if isinstance(query_string, str): query_string = query_string.encode(self.charset) - cur = cursor.Cursor(self) + if not cur: + cur = cursor.Cursor(self) cur.execute(query_string) stored_result = cur.fetchall() if stored_result: - fields = stored_result[0].keys() + fields = list(stored_result[0]) else: fields = [] return sqlresult.ResultSet( @@ -250,16 +265,11 @@ def ServerInfo(self): def SetSqlMode(self, sql_mode): """Set the connection sql_mode. See MySQL documentation for legal values.""" - if self.server_version < (4, 1): - raise self.NotSupportedError('server is too old to set sql_mode') self.Query('SET SESSION sql_mode=%s' % self.EscapeValues(sql_mode)) def ShowWarnings(self): """Return detailed information about warnings as a sequence of tuples of - (Level, Code, Message). This is only supported in MySQL-4.1 and up. - If your server is an earlier version, an empty sequence is returned.""" - if self.server_version < (4, 1): - return () + (Level, Code, Message).""" return self.Query('SHOW WARNINGS') def StartTransactionTimer(self, delay=60): @@ -283,7 +293,7 @@ def ResetTransactionTimer(self): def _GetAutocommitState(self): """This returns the current setting for autocommiting transactions.""" - return self._autocommit + return self.autocommit_mode def _GetCharacterSet(self): """This configures the character set used by this connection. @@ -296,9 +306,12 @@ def _SetAutocommitState(self, state): """This sets the autocommit mode on the connection. This is False by default if the database supports transactions.""" - self.ping(state) + try: + self.ping(reconnect=True) + except: + self.connect(sock=None) super(Connection, self).autocommit(state) - self._autocommit = state + self.autocommit_mode = state def _SetCharacterSet(self, charset): """This sets the character set, refer to _GetCharacterSet for doc.""" @@ -306,13 +319,14 @@ def _SetCharacterSet(self, charset): super(Connection, self).set_charset(charset) self._charset = charset + # Error classes taken from PyMySQL Error = pymysql.Error InterfaceError = pymysql.InterfaceError DatabaseError = pymysql.DatabaseError DataError = pymysql.DataError OperationalError = pymysql.OperationalError - IntegrityError = pymysql.IntegrityError + IntegrityError = pymysql.err.IntegrityError InternalError = pymysql.InternalError ProgrammingError = pymysql.ProgrammingError NotSupportedError = pymysql.NotSupportedError diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/cursor.py b/uweb3/libs/sqltalk/mysql/cursor.py similarity index 90% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/cursor.py rename to uweb3/libs/sqltalk/mysql/cursor.py index 8cc119a0..341fbbde 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/cursor.py +++ b/uweb3/libs/sqltalk/mysql/cursor.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python """SQLTalk MySQL Cursor class.""" __author__ = 'Elmer de Looff ' __version__ = '0.13' @@ -13,7 +13,7 @@ class ReturnObject(tuple): def __new__(cls, connection, results): """Creates the immutable tuple.""" - return super(ReturnObject, cls).__new__(cls, tuple(results)) + return super().__new__(cls, tuple(results)) def __init__(self, connection, results): """Adds the required attributes.""" @@ -54,10 +54,7 @@ def _Execute(self, query): # of other escaping things to start working properly. # Refer to MySQLdb.cursor code (~line 151) to see how this works. self._LogQuery(query) - self.execute(query.strip()) - result = ReturnObject(self.connection, self.fetchall()) - self._ProcessWarnings(result) - return result + return self.connection.Query(query.strip(), self) def _LogQuery(self, query): connection = self.connection @@ -80,8 +77,7 @@ def _StringFields(fields, field_escape): return '*' elif isinstance(fields, str): return field_escape(fields) - else: - return ', '.join(field_escape(fields)) + return ', '.join(field_escape(fields, True)) @staticmethod def _StringGroup(group, field_escape): @@ -89,7 +85,7 @@ def _StringGroup(group, field_escape): return '' elif isinstance(group, str): return 'GROUP BY ' + field_escape(group) - return 'GROUP BY ' + ', '.join(field_escape(group)) + return 'GROUP BY ' + ', '.join(field_escape(group, True)) @staticmethod def _StringLimit(limit, offset): @@ -115,8 +111,7 @@ def _StringOrder(order, field_escape): def _StringTable(table, field_escape): if isinstance(table, str): return field_escape(table) - else: - return ', '.join(field_escape(table)) + return ', '.join(field_escape(table, True)) def Delete(self, table, conditions, order=None, limit=None, offset=0, escape=True): @@ -206,7 +201,8 @@ def Insert(self, table, values, escape=True): return self._Execute(query) def Select(self, table, fields=None, conditions=None, order=None, - group=None, limit=None, offset=0, escape=True, totalcount=False): + group=None, limit=None, offset=0, escape=True, totalcount=False, + distinct=False): """Select fields from table that match the conditions, ordered and limited. Arguments: @@ -214,6 +210,9 @@ def Select(self, table, fields=None, conditions=None, order=None, fields: string/list/tuple (optional). Fields to select. Default '*'. As string, single field name. (autoquoted) As list/tuple, one field name per element. (autoquoted) + If the fielname itself is supplied as a tuple, + `field` as `name' will be returned where name is the second + item in the tuple. (autoquoted) conditions: string/list/tuple (optional). SQL 'where' statement. Literal as string. AND'd if list/tuple. THESE WILL NOT BE ESCAPED FOR YOU, EVER. @@ -232,24 +231,40 @@ def Select(self, table, fields=None, conditions=None, order=None, totalcount: boolean. If this is set to True, queries with a LIMIT applied will have the full number of matching rows on the affected_rows attribute of the resultset. + distinct: bool (optional). Performs a DISTINCT query if set to True. Returns: sqlresult.ResultSet object. """ - field_escape = self.connection.EscapeField if escape else lambda x: x - result = self._Execute('SELECT %s %s FROM %s WHERE %s %s %s %s' % ( + field_escape = self.connection.EscapeField if escape else self.NoEscapeField + result = self._Execute('SELECT %s %s %s FROM %s WHERE %s %s %s %s' % ( 'SQL_CALC_FOUND_ROWS' if totalcount and limit is not None else '', + 'DISTINCT' if distinct else '', self._StringFields(fields, field_escape), self._StringTable(table, field_escape), self._StringConditions(conditions, field_escape), self._StringGroup(group, field_escape), self._StringOrder(order, field_escape), self._StringLimit(limit, offset))) - if totalcount and limit is not None: result.affected = self._Execute('SELECT FOUND_ROWS()')[0][0] return result + def NoEscapeField(self, field, multiple=False): + """Returns a SQL unescaped field or table name. + + Set multiple = True if field is a tuple of names to be returned. + If multiple = False, and a tuple is encountered `field` as `name` will be + returned where the second part of the tuple is the `name` part. + """ + if not field: + return '' + if isinstance(field, str): + return field + elif not multiple and isinstance(field, tuple): + return '%s as %s' % (field[0], field[1]) + return map(self.NoEscapeField, field) + def SelectTables(self, contains=None, exact=False): """Returns table names from the current database. diff --git a/uweb3/libs/sqltalk/sqlite/__init__.py b/uweb3/libs/sqltalk/sqlite/__init__.py new file mode 100644 index 00000000..dc904251 --- /dev/null +++ b/uweb3/libs/sqltalk/sqlite/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +"""SQLTalk SQLite interface package.""" + +# Standard modules +import sqlite3 + +# Application specific modules +from . import connection + +VERSION_INFO = tuple(map(int, sqlite3.version.split('.'))) +SQLITE_VERSION_INFO = tuple(map(int, sqlite3.sqlite_version.split('.'))) + + +def Connect(*args, **kwds): + """Factory function for connection.Connection.""" + kwds['detect_types'] = sqlite3.PARSE_DECLTYPES + return connection.Connection(*args, **kwds) + + +def ThreadConnect(*args, **kwds): + """Factory function for connection.ThreadedConnection.""" + kwds['detect_types'] = sqlite3.PARSE_DECLTYPES + return connection.ThreadedConnection(*args, **kwds) + + +DataError = sqlite3.DataError +DatabaseError = sqlite3.DatabaseError +Error = sqlite3.Error +IntegrityError = sqlite3.IntegrityError +InterfaceError = sqlite3.InterfaceError +InternalError = sqlite3.InternalError +NotSupportedError = sqlite3.NotSupportedError +OperationalError = sqlite3.OperationalError diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py b/uweb3/libs/sqltalk/sqlite/connection.py similarity index 83% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py rename to uweb3/libs/sqltalk/sqlite/connection.py index 9611af50..94598a48 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py +++ b/uweb3/libs/sqltalk/sqlite/connection.py @@ -1,29 +1,27 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """This module implements the Connection class, which sets up a connection to an SQLite database. From this connection, cursor objects can be created, which use the escaping and character encoding facilities offered by the connection. """ -from __future__ import with_statement - __author__ = 'Elmer de Looff ' __version__ = '0.3' # Standard modules -import _sqlite3 +import sqlite3 import logging import os -import Queue +import queue import threading # Application specific modules -import converters -import cursor +from . import converters +from . import cursor COMMIT = '----COMMIT' ROLLBACK = '----ROLLBACK' NAMED_TYPE_SELECT = 'SELECT `name` FROM `sqlite_master` where `type`=?' -class Connection(_sqlite3.Connection): +class Connection(sqlite3.Connection): def __init__(self, *args, **kwds): db_name = os.path.splitext(os.path.split(args[0])[1])[0] self.logger = logging.getLogger('sqlite_%s' % db_name) @@ -33,7 +31,7 @@ def __init__(self, *args, **kwds): self.logger.setLevel(logging.WARNING) if kwds.pop('disable_log', False): self.logger.disable_logger = True - _sqlite3.Connection.__init__(self, *args, **kwds) + sqlite3.Connection.__init__(self, *args, **kwds) def __enter__(self): """Starts a transaction.""" @@ -50,22 +48,22 @@ def __exit__(self, exc_type, _exc_value, _exc_traceback): self.logger.debug('Transaction committed.') def commit(self): - _sqlite3.Connection.commit(self) - - def execute(self, query, args=()): - return _sqlite3.Connection.execute(self, query, args) - - def executemany(self, query, args=()): - return _sqlite3.Connection.executemany(self, query, args) + sqlite3.Connection.commit(self) def rollback(self): - _sqlite3.Connection.rollback(self) + sqlite3.Connection.rollback(self) @staticmethod def EscapeField(field): """Returns a SQL escaped field or table name.""" return '.'.join('`%s`' % f.replace('`', '``') for f in field.split('.')) + def EscapeValues(self, obj): + """We do not escape here, we simple return the value and allow the query + engine to escape using parameters. + """ + return obj + def ShowTables(self): result = self.execute(NAMED_TYPE_SELECT, ('table',)).fetchall() return [row[0] for row in result] @@ -86,7 +84,7 @@ def __init__(self, *args, **kwds): self.sqlite_args = args self.sqlite_kwds = kwds - self.queries = Queue.Queue(1) + self.queries = queue.queue(1) self.transaction_lock = threading.RLock() self.daemon = True self.start() @@ -111,13 +109,13 @@ def commit(self): def execute(self, query, args=()): with self.transaction_lock: - response = Queue.Queue() + response = queue.queue() self.queries.put((query, args, response, False)) return self._ProcessResponse(response) def executemany(self, query, args=()): with self.transaction_lock: - response = Queue.Queue() + response = queue.queue() self.queries.put((query, args, response, True)) return self._ProcessResponse(response) @@ -139,7 +137,7 @@ def run(self): response.put(SqliteResult(result.fetchall(), result.description, result.rowcount, result.lastrowid)) del execute, result - except Exception, error: + except Exception as error: response.put(error) del error @@ -166,7 +164,7 @@ def ShowTables(self): return [row[0] for row in result] -class SqliteResult(object): +class SqliteResult: def __init__(self, result, description, rowcount, lastrowid): self.result = result self.description = description @@ -178,11 +176,11 @@ def fetchall(self): #FIXME(Elmer): This needs defining in one place, not in each and every file. -DataError = _sqlite3.DataError -DatabaseError = _sqlite3.DatabaseError -Error = _sqlite3.Error -IntegrityError = _sqlite3.IntegrityError -InterfaceError = _sqlite3.InterfaceError -InternalError = _sqlite3.InternalError -NotSupportedError = _sqlite3.NotSupportedError -OperationalError = _sqlite3.OperationalError +DataError = sqlite3.DataError +DatabaseError = sqlite3.DatabaseError +Error = sqlite3.Error +IntegrityError = sqlite3.IntegrityError +InterfaceError = sqlite3.InterfaceError +InternalError = sqlite3.InternalError +NotSupportedError = sqlite3.NotSupportedError +OperationalError = sqlite3.OperationalError diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/converters.py b/uweb3/libs/sqltalk/sqlite/converters.py similarity index 91% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/converters.py rename to uweb3/libs/sqltalk/sqlite/converters.py index f2f64e4b..8bf0187d 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/converters.py +++ b/uweb3/libs/sqltalk/sqlite/converters.py @@ -1,4 +1,4 @@ -import _sqlite3 +import sqlite3 import datetime import pytz import time @@ -100,8 +100,8 @@ def ConvertTimestamp(date_obj): return INTERPRET_AS_UTC(datetime.datetime(*time_tuple)) -_sqlite3.register_adapter(datetime.date, AdaptDate) -_sqlite3.register_adapter(datetime.datetime, AdaptDatetime) -_sqlite3.register_adapter(time.struct_time, AdaptTimeStruct) -_sqlite3.register_converter('DATE', ConvertDate) -_sqlite3.register_converter('TIMESTAMP', ConvertTimestamp) +sqlite3.register_adapter(datetime.date, AdaptDate) +sqlite3.register_adapter(datetime.datetime, AdaptDatetime) +sqlite3.register_adapter(time.struct_time, AdaptTimeStruct) +sqlite3.register_converter('DATE', ConvertDate) +sqlite3.register_converter('TIMESTAMP', ConvertTimestamp) diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/cursor.py b/uweb3/libs/sqltalk/sqlite/cursor.py similarity index 89% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/cursor.py rename to uweb3/libs/sqltalk/sqlite/cursor.py index 45facb00..4b72b4aa 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/cursor.py +++ b/uweb3/libs/sqltalk/sqlite/cursor.py @@ -1,33 +1,36 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """SQLTalk SQLite Cursor class.""" __author__ = 'Elmer de Looff ' __version__ = '0.4' # Custom modules -from underdark.libs.sqltalk import sqlresult +from .. import sqlresult -class Cursor(object): +class Cursor: def __init__(self, connection): self.connection = connection + self.cursor = connection.cursor() def Execute(self, query, args=(), many=False): try: if many: - result = self.connection.executemany(query, args) + result = self.cursor.executemany(query, args) else: - result = self.connection.execute(query, args) + result = self.cursor.execute(query, args) except Exception: self.connection.logger.exception('Exception during query execution') raise + fieldnames = [field[0] for field in result.description] return sqlresult.ResultSet( affected=result.rowcount, charset='utf-8', - fields=result.description, + fields=fieldnames, insertid=result.lastrowid, query=(query, tuple(args)), - result=result.fetchall()) + result=[dict(zip(fieldnames, row)) for row in result.fetchall()], + ) def Insert(self, table, values): if not values: @@ -70,14 +73,14 @@ def Select(self, table, fields=None, conditions=None, order=None, group=None, Returns: sqlresult.ResultSet object. """ - if isinstance(table, basestring): + if isinstance(table, str): table = self.connection.EscapeField(table) else: table = ', '.join(map(self.connection.EscapeField, table)) if fields is None: fields = '*' - elif isinstance(fields, basestring): + elif isinstance(fields, str): fields = self.connection.EscapeField(fields) else: fields = ', '.join(map(self.connection.EscapeField, fields)) @@ -91,7 +94,7 @@ def Select(self, table, fields=None, conditions=None, order=None, group=None, if order is not None: orders = [] for rule in order: - if isinstance(rule, basestring): + if isinstance(rule, str): orders.append(self.connection.EscapeField(rule)) else: orders.append('%s %s' % diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlresult.py b/uweb3/libs/sqltalk/sqlresult.py similarity index 91% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlresult.py rename to uweb3/libs/sqltalk/sqlresult.py index 5e61d9b4..165c2821 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlresult.py +++ b/uweb3/libs/sqltalk/sqlresult.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """SQL result abstraction module. Classes: @@ -10,11 +10,11 @@ FieldError: Field- index or name does not exist. NotSupportedError: Operation is not supported """ -__author__ = 'Elmer de Looff ' -__version__ = '1.3' +__author__ = ('Elmer de Looff ', + 'Jan Klopper ') +__version__ = '1.4' # Standard modules -import itertools import operator GET_FIELD_NAME = operator.itemgetter(0) @@ -116,7 +116,7 @@ def itervalues(self): return iter(self._values) def iteritems(self): - return itertools.izip(self._fields, self._values) + return zip(self._fields, self._values) def keys(self): return self._fields[:] @@ -181,8 +181,7 @@ class ResultSet(object): @ charset - str Character set used for this connection. @ fields - tuple - Fields in the ResultSet. Each field is a tuple of 7 elements as specified - by the Python DB API (v2). + Fields in the ResultSet. @ insertid - int Auto-increment ID that was generated upon the last insert. @ query - str @@ -204,9 +203,8 @@ def __init__(self, query='', charset='', result=None, fields=None, Number of affected rows from this operation. % charset: str ~~ '' Character set used by the connection that executed this operation. - % fields: tuple of tuples of strings ~~ None - Description of fields involved in this operation. Tuples of strings as - per the Python DB API (v2). + % fields: tuple of strings ~~ None + Description of fields involved in this operation. % insertid: int ~~ 0 Auto-increment ID that was generated upon the last insert. % query ~~ '' @@ -222,11 +220,10 @@ def __init__(self, query='', charset='', result=None, fields=None, if result: self.fields = fields - self._fieldnames = fieldnames = map(GET_FIELD_NAME, fields) - self.result = [row_class(fieldnames, row) for row in result] + self.raw = result + self.result = [row_class(fields, row.values()) for row in result] else: self.fields = () - self._fieldnames = [] self.result = [] def __eq__(self, other): @@ -266,7 +263,7 @@ def __getitem__(self, item): except TypeError: # The item type is incorrect, try grabbing a column for this fieldname. try: - index = self._fieldnames.index(item) + index = self._fields.index(item) return tuple(row[index] for row in self.result) except ValueError: raise FieldError('Bad field name: %r.' % item) @@ -285,7 +282,7 @@ def __nonzero__(self): def __repr__(self): """Returns a string representation of the ResultSet.""" - return '%s instance: %d rows%s' % ( + return '%s instance: %d row%s' % ( self.__class__.__name__, len(self.result), 's'[len(self.result) == 1:]) def FilterRowsByFields(self, *fields): @@ -302,7 +299,7 @@ def FilterRowsByFields(self, *fields): ResultRow: Each ResultRow contains only the filtered fields. """ try: - indices = tuple(self._fieldnames.index(field) for field in fields) + indices = tuple(self._fields.index(field) for field in fields) except ValueError: raise FieldError('Bad fieldnames in filter request.') for row in self: @@ -310,7 +307,7 @@ def FilterRowsByFields(self, *fields): def PopField(self, field): try: - self._fieldnames.remove(field) + self._fields.remove(field) except ValueError: raise FieldError('Fieldname %r does not occur in the ResultSet.' % field) return [row.pop(field) for row in self] @@ -321,4 +318,4 @@ def PopRow(self, row_index): @property def fieldnames(self): """Returns a tuple of the fieldnames that are in this ResultSet.""" - return tuple(self._fieldnames) + return tuple(self._fields) diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlresult_test.py b/uweb3/libs/sqltalk/sqlresult_test.py similarity index 98% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlresult_test.py rename to uweb3/libs/sqltalk/sqlresult_test.py index ea131263..12943823 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlresult_test.py +++ b/uweb3/libs/sqltalk/sqlresult_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """Testsuite for the SQL Result abstraction module.""" __author__ = 'Elmer de Looff ' __version__ = '0.6' @@ -22,7 +22,7 @@ import unittest # Unittest target -import sqlresult +from . import sqlresult class ResultRowBasicOperation(unittest.TestCase): @@ -186,8 +186,8 @@ class ResultSetBasicOperation(unittest.TestCase): def setUp(self): """Set up a persistent test environment.""" self.fields = ('first', 'second', 'third', 'fourth') - self.result = tuple(tuple(2 ** i for i in xrange(j, j + 4)) - for j in xrange(0, 13, 4)) + self.result = tuple(tuple(2 ** i for i in range(j, j + 4)) + for j in range(0, 13, 4)) def testFalseWhenEmpty(self): """ResultSet is boolean False when there's all but a result.""" diff --git a/uweb3/libs/uploadlimiter.py b/uweb3/libs/uploadlimiter.py new file mode 100644 index 00000000..ff94e124 --- /dev/null +++ b/uweb3/libs/uploadlimiter.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +"""Module to validate uploaded files against some config vars if present""" + +__author__ = 'Jan Klopper ' +__version__ = '0.1' + +import magic + +class UploadLimiter: + def __init__(self, options=None, size=None, filetypes=None): + self.options = options + self.size = int(self.options.get('upload', {}).get('size', size) if options else size) + self.filetypes = None + filetypes = self.options.get('upload', {}).get('filetypes', filetypes) if options else filetypes + if filetypes: + filetypes = filetypes.lower().replace(' ', ',').split(',') + self.filetypes = [filetype.strip() for filetype in filetypes if filetype.strip()] + + def ValidFileType(self, content_type): + if content_type.lower() not in self.filetypes: + for filetype in self.filetypes: + if content_type.startswith(filetype): + return True + raise ContentTypeUploadException('%s is not an allowed file type.' % content_type) + return True + + def Validate(self, file): + if self.size and len(file) > self.size: + raise FilesizeUploadException('File is too big: %db > %db' % (len(file), self.size)) + + if self.filetypes: + content_type = magic.from_buffer(file, mime=True) + if not content_type: + content_type = 'text/plain' + return self.ValidFileType(content_type) + return True + + +class UploadException(Exception): + """There was an exception while uploading""" + +class FilesizeUploadException(UploadException): + """There was an exception while uploading due to filesize""" + +class ContentTypeUploadException(UploadException): + """There was an exception while uploading due to an invalid ContentType""" diff --git a/uweb3/model.py b/uweb3/model.py index 3f74920a..e00a528c 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -1,32 +1,26 @@ -#!/usr/bin/python +#!/usr/bin/python3 """uWeb3 model base classes.""" # Standard modules -import os +import base64 +import configparser import datetime -import simplejson +import os import sys import hashlib -import pickle +import json import secrets -import configparser -from sqlalchemy import Column, Integer, String -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.orm import sessionmaker, reconstructor -from sqlalchemy.orm.session import object_session -from sqlalchemy.inspection import inspect -from contextlib import contextmanager -from itertools import chain class Error(Exception): """Superclass used for inheritance and external exception handling.""" - class DatabaseError(Error): """Superclass for errors returned by the database backend.""" +class CurrentlyWorking(Error): + """Caching error""" class BadFieldError(DatabaseError): """A field in the record could not be written to the database.""" @@ -34,43 +28,66 @@ class BadFieldError(DatabaseError): class AlreadyExistError(Error): """The resource already exists, and cannot be created twice.""" - class NotExistError(Error): """The requested or provided resource doesn't exist or isn't accessible.""" - class PermissionError(Error): """The entity has insufficient rights to access the resource.""" + class SettingsManager(object): - def __init__(self, filename=None, executing_path=None): + def __init__(self, filename=None, path=None): """Creates a ini file with the child class name Arguments: - % filename: str - Name of the file without the extension + % filename: str, optional + Name of the file, optionally without the extension will default to .ini + If not filename is given the class.__name__ will be used to look for the config file in the path + % path: str, Optional + Path to the config file, will be used if filename is relative, eg does not start with '/' """ self.options = None - self.FILENAME = f"{self.__class__.__name__[:1].lower() + self.__class__.__name__[1:]}.ini" + extension = '' if filename and filename.endswith(('.ini', '.conf')) else '.ini' if filename: - self.FILENAME = f"{filename[:1].lower() + filename[1:]}.ini" + self.filename = f"{filename[:1].lower() + filename[1:] + extension}" + else: + self.filename = self.TableName() + extension - self.FILE_LOCATION = os.path.join(executing_path, self.FILENAME) + if path and not filename.startswith('/'): + self.file_location = os.path.join(path, self.filename) + else: + self.file_location = self.filename self.__CheckPermissions() + if not os.path.isfile(self.file_location): + os.mknod(self.file_location) - if not os.path.isfile(self.FILE_LOCATION): - os.mknod(self.FILE_LOCATION) - + self.mtime = None self.config = configparser.ConfigParser() self.Read() + @classmethod + def TableName(cls): + """Returns the 'database' table name for the SettingsManager class. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + We stick to the same naming scheme as for more table like connectors even + though we use files instead of tables in this class. + """ + if cls._TABLE: + return cls._TABLE + name = cls.__name__ + return name[0].lower() + name[1:] + def __CheckPermissions(self): """Checks if SettingsManager can read/write to file.""" - if not os.access(self.FILE_LOCATION, os.R_OK): - raise PermissionError(f"SettingsManager missing permissions to read file: {self.FILE_LOCATION}") - if not os.access(self.FILE_LOCATION, os.W_OK): - raise PermissionError(f"SettingsManager missing permissions to write to file: {self.FILE_LOCATION}") + if not os.path.isfile(self.file_location): + return True + if not os.access(self.file_location, os.R_OK): + raise PermissionError(f"SettingsManager missing permissions to read file: {self.file_location}") + if not os.access(self.file_location, os.W_OK): + raise PermissionError(f"SettingsManager missing permissions to write to file: {self.file_location}") def Create(self, section, key, value): """Creates a section or/and key = value @@ -92,14 +109,22 @@ def Create(self, section, key, value): raise ValueError("key already exists") self.config.set(section, key, value) - - with open(self.FILE_LOCATION, 'w') as configfile: - self.config.write(configfile) - self.Read() + self._Write(False) + self.mtime = None def Read(self): - self.config.read(self.FILE_LOCATION) - self.options = self.config._sections + """Reads the config file and populates the options member + It uses the mtime to see if any re-reading is required + + Returns True if changes where detected, False if no re-read was needed.""" + if not self.mtime: + curtime = os.path.getmtime(self.file_location) + if self.mtime and self.mtime == curtime: + return False + self.config.read(self.file_location) + self.options = self.config._sections + self.mtime = curtime + return True def Update(self, section, key, value): """Updates ini file @@ -116,12 +141,10 @@ def Update(self, section, key, value): if not self.options.get(section): self.config.add_section(section) self.config.set(section, key, value) + self._Write() + self.mtime = None - with open(self.FILE_LOCATION, 'w') as configfile: - self.config.write(configfile) - self.Read() - - def Delete(self, section, key, delete_section=False): + def Delete(self, section, key=None): """Delete sections/keys from the INI file Be aware, deleting a section that is not empty will remove all keys from that given section @@ -129,43 +152,93 @@ def Delete(self, section, key, delete_section=False): Arguments: @ section: str Name of the section - @ key: str + @ key: None / str Name of the key you want to remove - % delete_section: boolean - If set to true it will delete the supplied section + If set to None (default) it will delete the supplied section Raises: configparser.NoSectionError """ - self.config.remove_option(section, key) - if delete_section: + if key: + self.config.remove_option(section, key) + if not key: self.config.remove_section(section) - with open(self.FILE_LOCATION, 'w') as configfile: + self._Write() + self.mtime = None + return True + + def _Write(self, reread=True): + """Internal function to store the current config to file""" + with open(self.file_location, 'w') as configfile: self.config.write(configfile) - self.Read() + if reread: + return self.Read() + return True class SecureCookie(object): - def __init__(self): - self.req = self.secure_cookie_connection[0] - self.cookies = self.secure_cookie_connection[1] - self.cookie_salt = self.secure_cookie_connection[2] - self.cookiejar = self.__GetSessionCookies() - - def __GetSessionCookies(self): - cookiejar = {} - for key, value in self.cookies.items(): - if value: - isValid, value = self.__ValidateCookieHash(value) - if isValid: - cookiejar[key] = value - return cookiejar - - def Create(self, name, data, **attrs): + """The secureCookie class works just like other data abstraction classes, + except that it stores its data in client side cookies that are signed with a + server side secret to avoid tampering by the end-user. + + Subclass this class with your own class to create signed cookie objects. The + name for your class will reflect the name for the cookie in the cookie. + """ + + HASHTYPE = 'ripemd160' + _TABLE = None + _CONNECTOR = 'signedCookie' + + def __init__(self, connection): + """Create a new SecureCookie instance.""" + self.connection = connection + self.request, self.cookies, self.cookie_salt = self.connection + self.debug = self.connection.debug + self._rawcookie = None + if self.debug: + print('current cookies (unvalidated) for request:', self.cookies) + + def __str__(self): + """Returns the cookie's value if it was valid and untampered with.""" + return str(self.rawcookie) + + @classmethod + def TableName(cls): + """Returns the 'database' table name for the SecureCookie class. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + We stick to the same naming scheme as for more table like connectors even + though we use cookies instead of tables in this class. + """ + if cls._TABLE: + return cls._TABLE + name = cls.__name__ + return name[0].lower() + name[1:] + + @property + def rawcookie(self): + """Reads the request cookie, checks if it was signed correctly and return + the value, or returns False""" + if not self._rawcookie is None: + return self._rawcookie + name = self.TableName() + if name in self.cookies and self.cookies[name]: + isValid, value = self.__ValidateCookieHash(self.cookies[name]) + if isValid: + self._rawcookie = value + return value + if self.debug: + print('Secure cookie "%s" was tampered with and thus invalid. content was: %s ' % (name, self.cookies[name])) + if self.debug: + print('Secure cookie "%s" was not present.' % name) + self._rawcookie = '' + return self._rawcookie + + @classmethod + def Create(cls, connection, data, **attrs): """Creates a secure cookie Arguments: - @ name: str - Name of the cookie @ data: dict Needs to have a key called __name with value of how you want to name the 'table' % only_return_hash: boolean @@ -199,30 +272,23 @@ def Create(self, name, data, **attrs): Raises: ValueError: When cookie with name already exists """ - if not attrs.get('update') and self.cookiejar.get(name): - raise ValueError("Cookie with name already exists") - if attrs.get('update'): - self.cookiejar[name] = data + cls.connection = connection + cls.request, cls.cookies, cls.cookie_salt = connection + name = cls.TableName() + cls._rawcookie = data - hashed = self.__CreateCookieHash(data) - if not attrs.get('only_return_hash'): - #Delete all these settings to prevent them from injecting in a cookie - if attrs.get('update'): - del attrs['update'] - if attrs.get('only_return_hash'): - del attrs['only_return_hash'] - self.req.AddCookie(name, hashed, **attrs) - else: - return hashed + hashed = cls.__CreateCookieHash(cls, data) + cls.cookies[name] = hashed + cls.request.AddCookie(name, hashed, **attrs) + return cls - def Update(self, name, data, **attrs): + def Update(self, data, **attrs): """"Updates a secure cookie - Keep in mind that the actual cookie is updated on the next request. After calling - this method it will update the session attribute to the new value however. + Keep in mind that the actual cookie's value is avilable from the next + request. After calling this method it will update the cookie attribute to + the new value however. Arguments: - @ name: str - Name of the cookie @ data: dict Needs to have a key called __name with value of how you want to name the 'table' % only_return_hash: boolean @@ -256,32 +322,27 @@ def Update(self, name, data, **attrs): Raises: ValueError: When no cookie with given name found """ - if not self.cookiejar.get(name): - raise ValueError("No cookie with name `{}` found".format(name)) - - attrs['update'] = True - self.Create(name, data, **attrs) - + name = self.TableName() + if not self.rawcookie: + raise ValueError("No valid cookie with name `{}` found".format(name)) + self._rawcookie = data + hashed = self.__CreateCookieHash(data) + self.cookies[name] = hashed + self.request.AddCookie(name, hashed, **attrs) - def Delete(self, name): + def Delete(self): """Deletes cookie based on name The cookie is no longer in the session after calling this method - - Arguments: - % name: str - Deletes cookie by name """ - self.req.DeleteCookie(name) - if self.cookiejar.get(name): - self.cookiejar.pop(name) + name = self.TableName() + self.request.DeleteCookie(name) + self._rawcookie = None def __CreateCookieHash(self, data): - hex_string = pickle.dumps(data).hex() - - hashed = (hex_string + self.cookie_salt).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return '{}+{}'.format(h.hexdigest(), hex_string) + data = str(json.dumps(data)) + h = hashlib.new(self.HASHTYPE) + h.update((data + self.cookie_salt).encode('utf-8')) + return '{}+{}'.format(h.hexdigest(), self._encode(data)) def __ValidateCookieHash(self, cookie): """Takes a cookie and validates it @@ -292,15 +353,35 @@ def __ValidateCookieHash(self, cookie): if not cookie: return None try: - data = cookie.rsplit('+', 1)[1] - data = pickle.loads(bytes.fromhex(data)) + data = json.loads(self._decode(cookie.rsplit('+', 1)[1])) except Exception: + print('Cookie contents could not be loaded as Json') return (False, None) - if cookie != self.__CreateCookieHash(data): - return (False, None) + if cookie == self.__CreateCookieHash(data): + return (True, data) + print('Cookie contents could not be verified as hash is different') + return (False, None) + + @staticmethod + def _encode(data): + """Encode cookie values per RFC 6265 + http://www.ietf.org/rfc/rfc6265.txt + + We elect to only encode the control chars for the cookie spec, and not the + whole cookie content. + """ + return data.replace('%', "%25").replace('"', "%22").replace(",", "%27").replace('{', "%7B").replace('}', "%7D").replace('=', "%3D") + + @staticmethod + def _decode(data): + """decode cookie values per RFC 6265 + http://www.ietf.org/rfc/rfc6265.txt - return (True, data) + We elect to only decode the control chars for the cookie spec, and not the + whole cookie content. + """ + return data.replace("%22", '"').replace("%27", ",").replace("%7B", '{').replace("%7D", '}').replace("%3D", '=').replace("%25", '%') # Record classes have many methods, this is not an actual problem. # pylint: disable=R0904 @@ -453,7 +534,10 @@ def _PreCreate(self, _cursor): Typically you would verify values of the Record in this step, or transform the data for database-safe insertion. If the data is transformed here, this transformation should be reversed in `_PostCreate()`. + + Returning False from this method will halt the creation of the record. """ + return True def _PreSave(self, _cursor): """Hook that runs before saving (updating) a Record in the database. @@ -461,7 +545,10 @@ def _PreSave(self, _cursor): Typically you would verify values of the Record in this step, or transform the data for database-safe insertion. If the data is transformed here, this transformation should be reversed in `_PostSave()`. + + Returning False from this method will halt the updating of the record. """ + return True def _PostInit(self): """Hook that runs after initializing a Record instance. @@ -485,6 +572,15 @@ def _PostSave(self, _cursor): Any transforms that were performed on the data should be reversed here. """ + def _PreDelete(self): + """Hook that runs before deleting a Record in the database. + Returning False from this method will halt the deletion of the record. + """ + return True + + def _PostDelete(self): + """Hook that runs after deleting a Record in the database.""" + # ############################################################################ # Base record functionality methods, to be implemented by subclasses. # Some methods have a generic implementation, but may need customization, @@ -523,9 +619,12 @@ def Delete(self): For deleting an unloaded object, use the classmethod `DeletePrimary`. """ + if not self._PreDelete(): + return False self.DeletePrimary(self.connection, self.key) self._record.clear() self.clear() + self._PostDelete() @classmethod def FromPrimary(cls, connection, pkey_value): @@ -585,33 +684,31 @@ def _LoadAsForeign(cls, connection, relation_value, method=None): method = cls._LOAD_METHOD return getattr(cls, method)(connection, relation_value) - # ############################################################################ # Functions for tracking table and primary key values # def _Changes(self): """Returns the differences of the current state vs the last stored state.""" sql_record = self._DataRecord() - changes = {} - for key, value in sql_record.items(): - if self._record.get(key) != value: - changes[key] = value - return changes + return { + key: value + for key, value in sql_record.items() if self._record.get(key) != value + } def _DataRecord(self): """Returns a dictionary of the record's database values For any Record object present, its primary key value (`Record.key`) is used. """ - sql_record = {} - for key, value in super(BaseRecord, self).items(): - sql_record[key] = self._ValueOrPrimary(value) - return sql_record + return {key: self._ValueOrPrimary(value) for key, value in super().items()} @staticmethod def _ValueOrPrimary(value): """Returns the value, or its primary key value if it's a Record.""" while isinstance(value, BaseRecord): - value = value.key + if hasattr(value, '_RECORD_KEY') and value._RECORD_KEY: + value = value[value._RECORD_KEY] + else: + value = value.key return value @classmethod @@ -664,6 +761,8 @@ def key(self, value): class Record(BaseRecord): """Extensions to the Record abstraction for relational database use.""" _FOREIGN_RELATIONS = {} + _CONNECTOR = 'mysql' + SEARCHABLE_COLUMNS = [] # ############################################################################ # Methods enabling auto-loading @@ -772,15 +871,11 @@ def GetRecordClass(cls): if foreign_cls is None: return value - elif type(foreign_cls) is dict: + if type(foreign_cls) is dict: cls = GetRecordClass(foreign_cls['class']) loader = foreign_cls.get('loader') - value = cls._LoadAsForeign(self.connection, value, method=loader) - return value - else: - value = GetRecordClass(foreign_cls)._LoadAsForeign(self.connection, value) - self[field] = value - return value + return cls._LoadAsForeign(self.connection, value, method=loader) + return GetRecordClass(foreign_cls)._LoadAsForeign(self.connection, value) # ############################################################################ # Override basic dict methods so that autoload mechanisms function on them. @@ -834,7 +929,8 @@ def values(self): # @classmethod def _FromParent(cls, parent, relation_field=None, conditions=None, - limit=None, offset=None, order=None): + limit=None, offset=None, order=None, + yield_unlimited_total_first=False): """Returns all `cls` objects that are a child of the given parent. This utilized the parent's _Children method, with either this class' @@ -844,8 +940,8 @@ def _FromParent(cls, parent, relation_field=None, conditions=None, @ parent: Record The parent for who children should be found in this class % relation_field: str ~~ cls.TableName() - The fieldname in this class' table which relates to the parent's - primary key. If not given, parent.TableName() will be used. + The fieldname in this class' table which relates to the parent's primary + key. If not given, parent.TableName() will be used. % conditions: str / iterable ~~ None The extra condition(s) that should be applied when querying for records. % limit: int ~~ None @@ -855,10 +951,13 @@ def _FromParent(cls, parent, relation_field=None, conditions=None, Specifies the offset at which the yielded items should start. Combined with limit this enables proper pagination. % order: iterable of str/2-tuple - Defines the fields on which the output should be ordered. This should - be a list of strings or 2-tuples. The string or first item indicates - the field, the second argument defines descending order + Defines the fields on which the output should be ordered. This should be + a list of strings or 2-tuples. The string or first item indicates the + field, the second argument defines descending order (desc. if True). + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. """ if not isinstance(parent, Record): raise TypeError('parent argument should be a Record type.') @@ -870,12 +969,20 @@ def _FromParent(cls, parent, relation_field=None, conditions=None, qry_conditions.append(conditions) else: qry_conditions.extend(conditions) + + firstrow = yield_unlimited_total_first # set a flag to skip the linking of + # the first row to our parent, as that will be the full record cound instead + # of a record for record in cls.List(parent.connection, conditions=qry_conditions, - limit=limit, offset=offset, order=order): - record[relation_field] = parent.copy() + limit=limit, offset=offset, order=order, + yield_unlimited_total_first=yield_unlimited_total_first): + if not firstrow: + record[relation_field] = parent.copy() + firstrow = False yield record - def _Children(self, child_class, relation_field=None, conditions=None): + def _Children(self, child_class, relation_field=None, conditions=None, + limit=None, offset=None, order=None, yield_unlimited_total_first=False): """Returns all `child_class` objects related to this record. The table for the given `child_class` will be queried for all fields where @@ -887,16 +994,31 @@ def _Children(self, child_class, relation_field=None, conditions=None): @ child_class: type (Record subclass) The child class whose objects should be found. % relation_field: str ~~ self.TableName() - The fieldname in the `child_class` table which relates that table to - the table for this record. + The fieldname in the `child_class` table which relates that table to the + table for this record. % conditions: str / iterable ~~ The extra condition(s) that should be applied when querying for records. + % limit: int ~~ None + Specifies a maximum number of items to be yielded. The limit happens on + the database side, limiting the query results. + % offset: int ~~ None + Specifies the offset at which the yielded items should start. Combined + with limit this enables proper pagination. + % order: iterable of str/2-tuple + Defines the fields on which the output should be ordered. This should be + a list of strings or 2-tuples. The string or first item indicates the + field, the second argument defines descending order (desc. if True). + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. """ # Delegating to let child class handle its own querying. These are methods # for development, and are private only to prevent name collisions. # pylint: disable=W0212 return child_class._FromParent( - self, relation_field=relation_field, conditions=conditions) + self, relation_field=relation_field, conditions=conditions, + limit=limit, offset=offset, order=order, + yield_unlimited_total_first=yield_unlimited_total_first) def _DeleteChildren(self, child_class, relation_field=None): """Deletes all `child_class` objects related to this record. @@ -908,8 +1030,8 @@ def _DeleteChildren(self, child_class, relation_field=None): @ child_class: type (Record subclass) The child class whose objects should be deleted. % relation_field: str ~~ self.TableName() - The fieldname in the `child_class` table which relates that table to - the table for this record. + The fieldname in the `child_class` table which relates that table to the + table for this record. """ relation_field = relation_field or self.TableName() with self.connection as cursor: @@ -919,7 +1041,7 @@ def _DeleteChildren(self, child_class, relation_field=None): @classmethod def _PrimaryKeyCondition(cls, connection, value): - """Returns the MySQL primary key condition to be used.""" + """Returns the primary key condition to be used.""" if isinstance(cls._PRIMARY_KEY, tuple): if not isinstance(value, tuple): raise TypeError( @@ -929,9 +1051,10 @@ def _PrimaryKeyCondition(cls, connection, value): values = tuple(map(cls._ValueOrPrimary, value)) return ' AND '.join('`%s` = %s' % (field, value) for field, value in zip(cls._PRIMARY_KEY, connection.EscapeValues(values))) - else: - return '`%s` = %s' % (cls._PRIMARY_KEY, - connection.EscapeValues(cls._ValueOrPrimary(value))) + return '`%s`.`%s` = %s' % ( + cls.TableName(), + cls._PRIMARY_KEY, + connection.EscapeValues(cls._ValueOrPrimary(value))) def _RecordCreate(self, cursor): """Inserts the record's current values in the database as a new record. @@ -939,8 +1062,8 @@ def _RecordCreate(self, cursor): Upon success, the record's primary key is set to the result's insertid """ try: - # Compound key case values = self._DataRecord() + # Compound key case if isinstance(self._PRIMARY_KEY, tuple): auto_inc_field = set(self._PRIMARY_KEY) - set(values) if auto_inc_field: @@ -954,13 +1077,13 @@ def _RecordCreate(self, cursor): except cursor.OperationalError as err_obj: if err_obj[0] == 1054: raise BadFieldError(err_obj[1]) - raise + raise DatabaseError(err_obj) def _RecordUpdate(self, cursor): """Updates the existing database entry with the record's current values. - The constraint with which the record is updated is the name and value of - the Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) + The constraint with which the record is updated is the name and value of the + Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) """ try: if isinstance(self._PRIMARY_KEY, tuple): @@ -968,13 +1091,15 @@ def _RecordUpdate(self, cursor): else: primary = self._record[self._PRIMARY_KEY] cursor.Update( - table=self.TableName(), values=self._Changes(), + table=self.TableName(), + values=self._Changes(), conditions=self._PrimaryKeyCondition(self.connection, primary)) except KeyError: raise Error('Cannot update record without pre-existing primary key.') except cursor.OperationalError as err_obj: if err_obj[0] == 1054: raise BadFieldError(err_obj[1]) + raise DatabaseError(err_obj) def _SaveForeign(self, cursor): """Recursively saves all nested Record instances.""" @@ -990,8 +1115,8 @@ def _SaveForeign(self, cursor): def _SaveSelf(self, cursor): """Updates the existing database entry with the record's current values. - The constraint with which the record is updated is the name and value of - the Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) + The constraint with which the record is updated is the name and value of the + Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) """ self._PreSave(cursor) difference = self._Changes() @@ -1033,7 +1158,8 @@ def FromPrimary(cls, connection, pkey_value): @classmethod def List(cls, connection, conditions=None, limit=None, offset=None, - order=None, yield_unlimited_total_first=False): + order=None, yield_unlimited_total_first=False, search=None, + tables=None, escape=True, fields=None, distinct=False): """Yields a Record object for every table entry. Arguments: @@ -1049,24 +1175,64 @@ def List(cls, connection, conditions=None, limit=None, offset=None, Specifies the offset at which the yielded items should start. Combined with limit this enables proper pagination. % order: iterable of str/2-tuple - Defines the fields on which the output should be ordered. This should - be a list of strings or 2-tuples. The string or first item indicates the + Defines the fields on which the output should be ordered. This should be + a list of strings or 2-tuples. The string or first item indicates the field, the second argument defines descending order (desc. if True). % yield_unlimited_total_first: bool ~~ False Instead of yielding only Record objects, the first item returned is the number of results from the query if it had been executed without limit. + % search: str + Specifies what string should be searched for in the default searchable + database columns. + % tables: str / iterable ~~ None + Specifies what tables should be searched queried + % escape: bool ~~ True + Are conditions escaped? + % fields: str / iterable ~~ * + Specifies what fields should be returned + % distinct: bool (optional). + Performs a DISTINCT query if set to True. Yields: Record: Database record abstraction class. """ + if not tables: + tables = [cls.TableName()] + group = None + if fields is None: + fields = '%s.*' % cls.TableName() + if search: + group = '%s.%s' % (cls.TableName(), (cls.RecordKey() if getattr(cls, "RecordKey", None) else cls._PRIMARY_KEY)) + tables, searchconditions = cls._GetSearchQuery(connection, tables, search) + if conditions: + if type(conditions) == list: + conditions.extend(searchconditions) + else: + searchconditions.append(conditions) + conditions = searchconditions + else: + conditions = searchconditions + cacheable = False + if not offset or offset < 0: + offset = 0 + if hasattr(cls, '_addToCache'): + #TODO dont cache partial / multi-table objects + cacheable = True + connection.modelcache['_stats']['queries'].append('%s Record.List' % cls.TableName()) with connection as cursor: - records = cursor.Select( - table=cls.TableName(), conditions=conditions, limit=limit, - offset=offset, order=order, totalcount=yield_unlimited_total_first) + records = cursor.Select(fields=fields, + table=tables, conditions=conditions, + limit=limit, offset=offset, order=order, + totalcount=yield_unlimited_total_first, + escape=escape, group=group, distinct=distinct) if yield_unlimited_total_first: yield records.affected - for record in records: - yield cls(connection, record) + records = [cls(connection, record) for record in list(records)] + + yield from records + + if cacheable: + list(cls._cacheListPreseed(records)) # SQL Records have foreign relations, saving needs an extra argument for this. # pylint: disable=W0221 @@ -1090,6 +1256,64 @@ def Save(self, save_foreign=False): return self # pylint: enable=W0221 + @classmethod + def _GetSearchQuery(cls, connection, tables, search): + """Extracts table information from the searchable columns list.""" + conditions = [] + like = 'like "%%%s%%"' % connection.EscapeValues(search.strip())[1:-1] + searchconditions = [] + thistable = cls.TableName() + for column in cls.SEARCHABLE_COLUMNS: + if '.' not in column: + searchconditions.append('`%s`.`%s` %s' % (thistable, column, like)) + continue + classname, column = column.split('.', 1) + othertable = cls._SUBTYPES[classname].TableName() + if (othertable != thistable and + othertable not in tables): + fkey = cls._FOREIGN_RELATIONS.get(classname, False) + key = othertable._PRIMARY_KEY + if fkey and fkey.get('LookupKey', False): + key = fkey.get('LookupKey') + elif getattr(table, "RecordKey", None): + key = othertable.RecordKey() + # add the cross table join + #TODO use referenced field, instead of just the othertable name + conditions.append('`%s`.`%s` = `%s`.`%s' % (thistable, + othertable, + othertable, + key)) + tables.append(othertable) + searchconditions.append('`%s`.`%s` %s' % (othertable, + column, like)) + + conditions.append('(%s)' % ' or '.join(searchconditions)) + return tables, conditions + + def __json__(self, complete=True, recursive=True): + """Returns a dictionary representation of the Record. + + Arguments: + % complete: bool ~~ False + Whether the foreign references on the object should all be resolved before + converting the Record to a dictionary. Either way, existing resolved + references will be represented as complete dictionaries. + + Returns: + dict: dictionary representation of the record. + """ + record_dict = {} + record = self if complete else dict(record) + for key, value in record.items(): + if isinstance(value, BaseRecord): + if complete and recursive: + record_dict[key] = value.__json__(complete=True, recursive=True) + else: + record_dict[key] = dict(value) + else: + record_dict[key] = value + return record_dict + class VersionedRecord(Record): """Basic class for database table/record abstraction.""" @@ -1144,33 +1368,104 @@ def FromIdentifier(cls, connection, identifier): return cls(connection, record[0]) @classmethod - def List(cls, connection, conditions=None): + def List(cls, connection, conditions=None, limit=None, offset=None, + order=None, yield_unlimited_total_first=False, search=None, + tables=None, escape=True, fields=None): """Yields the latest Record for each versioned entry in the table. Arguments: - @ connection: sqltalk.connection + @ connection: object Database connection to use. + % conditions: str / iterable ~~ None + Optional query portion that will be used to limit the list of results. + If multiple conditions are provided, they are joined on an 'AND' string. + % limit: int ~~ None + Specifies a maximum number of items to be yielded. The limit happens on + the database side, limiting the query results. + % offset: int ~~ None + Specifies the offset at which the yielded items should start. Combined + with limit this enables proper pagination. + % order: iterable of str/2-tuple + Defines the fields on which the output should be ordered. This should + be a list of strings or 2-tuples. The string or first item indicates the + field, the second argument defines descending order (desc. if True). + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. + % search: str + Specifies what string should be searched for in the default searchable + database columns. + % tables: str / iterable ~~ None + Specifies what tables should be searched queried + % escape: bool ~~ True + Are conditions escaped? + % fields: str / iterable ~~ * + Specifies what fields should be returned Yields: Record: The Record with the newest version for each versioned entry. """ - if isinstance(conditions, (list, tuple)): - conditions = ' AND '.join(conditions) + if not tables: + tables = [cls.TableName()] + if fields: + if fields != '*': + if isinstance(fields, str): + fields = connection.EscapeField(fields) + else: + fields = ', '.join(connection.EscapeField(fields)) + else: + fields = "%s.*" % cls.TableName() + if search: + search = search.strip() + tables, searchconditions = cls._GetSearchQuery(connection, tables, search) + if conditions: + if type(conditions) == list: + conditions.extend(searchconditions) + else: + searchconditions.append(conditions) + conditions = searchconditions + else: + conditions = searchconditions + field_escape = connection.EscapeField if escape else lambda x: x + if yield_unlimited_total_first and limit is not None: + totalcount = 'SQL_CALC_FOUND_ROWS' + else: + totalcount = '' + cacheable = False + if hasattr(cls, '_addToCache'): + #TODO dont cache partial / multi-table objects + cacheable = True + connection.modelcache['_stats']['queries'].append('%s VersionedRecord.List' % cls.TableName()) with connection as cursor: records = cursor.Execute(""" - SELECT `%(table)s`.* - FROM `%(table)s` + SELECT %(totalcount)s %(fields)s + FROM %(tables)s JOIN (SELECT MAX(`%(primary)s`) AS `max` FROM `%(table)s` GROUP BY `%(record_key)s`) AS `versions` ON (`%(table)s`.`%(primary)s` = `versions`.`max`) WHERE %(conditions)s - """ % {'primary': cls._PRIMARY_KEY, + %(order)s + %(limit)s + """ % {'totalcount': totalcount, + 'primary': cls._PRIMARY_KEY, 'record_key': cls.RecordKey(), + 'fields': fields, 'table': cls.TableName(), - 'conditions': conditions or '1'}) - for record in records: - yield cls(connection, record) + 'tables': cursor._StringTable(tables, field_escape), + 'conditions': cursor._StringConditions(conditions, + field_escape), + 'order': cursor._StringOrder(order, field_escape), + 'limit': cursor._StringLimit(limit, offset)}) + if yield_unlimited_total_first and limit is not None: + with connection as cursor: + records.affected = cursor._Execute('SELECT FOUND_ROWS()')[0][0] + yield records.affected + # turn sqltalk rows into model + records = [cls(connection, record) for record in list(records)] + yield from records + if cacheable: + list(cls._cacheListPreseed(records)) @classmethod def Versions(cls, connection, identifier, conditions='1'): @@ -1227,11 +1522,15 @@ def _PreSave(self, cursor): assume an AutoIncrement primary key field. """ super(VersionedRecord, self)._PreSave(cursor) - self.key = None + difference = self._Changes() + if difference: + self.key = None def _RecordUpdate(self, cursor): """All updates are handled as new inserts for the same Record Key.""" - self._RecordCreate(cursor) + difference = self._Changes() + if difference: + self._RecordCreate(cursor) # Pylint falsely believes this property is overwritten by its setter later on. # pylint: disable=E0202 @@ -1257,6 +1556,7 @@ def identifier(self, value): class MongoRecord(BaseRecord): """Abstraction of MongoDB collection records.""" _PRIMARY_KEY = '_id' + _CONNECTOR = 'mongo' @classmethod def Collection(cls, connection): @@ -1310,423 +1610,6 @@ def _StoreRecord(self): self.key = self.Collection(self.connection).save(self._DataRecord()) -class AlchemyBaseRecord(object): - def __init__(self, session, record): - self.session = session - self._BuildClassFromRecord(record) - - def _BuildClassFromRecord(self, record): - if isinstance(record, dict): - for key, value in record.items(): - if not key in self.__table__.columns.keys(): - raise AttributeError(f"Key '{key}' not specified in class '{self.__class__.__name__}'") - setattr(self, key, value) - if self.session: - try: - self.session.add(self) - except: - self.session.rollback() - raise - else: - self.session.commit() - - def __hash__(self): - """Returns the hashed value of the key.""" - return hash(self.key) - - def __del__(self): - """Cleans up the connection at the end of its life cycle""" - self.session.close() - - def __repr__(self): - s = {} - for key in self.__table__.columns.keys(): - value = getattr(self, key) - if value: - s[key] = value - return f'{type(self).__name__}({s})' - - def __eq__(self, other): - if type(self) != type(other): - return False # Types must be the same. - elif not (self.key == other.key is not None): - return False # Records should have the same non-None primary key value. - elif len(self) != len(other): - return False # Records must contain the same number of objects. - for key in self.__table__.columns.keys(): - value = getattr(self, key) - other_value = getattr(other, key) - if isinstance(self, AlchemyBaseRecord) != isinstance(other, AlchemyBaseRecord): - # Only one of the two is a BaseRecord instance - if (isinstance(self, AlchemyBaseRecord) and value.key != other_value or - isinstance(other, AlchemyBaseRecord) and other_value.key != value): - return False - elif value != other_value: - return False - return True - - def __ne__(self, other): - """Returns the proper inverse of __eq__.""" - # Without this, the non-equal checks used in __eq__ will not work, - # and the `!=` operator would not be the logical inverse of `==`. - return not self == other - - def __len__(self): - return len(dict((col, getattr(self, col)) for col in self.__table__.columns.keys() if getattr(self, col))) - - def __int__(self): - """Returns the integer key value of the Record. - - For record objects where the primary key value is not (always) an integer, - this function will raise an error in the situations where it is not. - """ - key_val = self.key - if not isinstance(key_val, (int)): - # We should not truncate floating point numbers. - # Nor turn strings of numbers into an integer. - raise ValueError('The primary key is not an integral number.') - return key_val - - def copy(self): - """Returns a shallow copy of the Record that is a new functional Record.""" - import copy - return copy.copy(self) - - def deepcopy(self): - import copy - return copy.deepcopy(self) - - def __gt__(self, other): - """Index of this record is greater than the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key > other.key - return NotImplemented - - def __ge__(self, other): - """Index of this record is greater than, or equal to, the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key >= other.key - return NotImplemented - - def __lt__(self, other): - """Index of this record is smaller than the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key < other.key - return NotImplemented - - def __le__(self, other): - """Index of this record is smaller than, or equal to, the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key <= other.key - return NotImplemented - - def __getitem__(self, field): - return getattr(self, field) - - def iteritems(self): - """Yields all field+value pairs in the Record. - - This automatically loads in relationships. - """ - return chain(((key, getattr(self, key)) for key in self.__table__.columns.keys()), - ((child[0], getattr(self, child[0])) for child in inspect(type(self)).relationships.items())) - - def itervalues(self): - """Yields all values in the Record, loads relationships""" - return chain((getattr(self, key) for key in self.__table__.columns.keys()), - (getattr(self, child[0]) for child in inspect(type(self)).relationships.items())) - - def items(self): - """Returns a list of field+value pairs in the Record. - - This automatically loads in relationships. - """ - return list(self.iteritems()) - - def values(self): - """Returns a list of values in the Record, loading foreign references.""" - return list(self.itervalues()) - - @property - def key(self): - return getattr(self, inspect(type(self)).primary_key[0].name) - - @classmethod - def TableName(cls): - """Returns the database table name for the Record class.""" - return cls.__tablename__ - - @classmethod - def _AlchemyRecordToDict(cls, record): - """Turns the values of a given class into a dictionary. Doesn't trigger - automatic loading of child classes. - - Arguments: - @ record: cls - AlchemyBaseRecord class that is retrieved from a database query - Returns - dict: dictionary with all table columns and values - None: when record is empty - """ - if not isinstance(record, type(None)): - return dict((col, getattr(record, col)) for col in record.__table__.columns.keys()) - return None - - @reconstructor - def reconstruct(self): - """This is called instead of __init__ when the result comes from the database""" - self.session = object_session(self) - - @classmethod - def _PrimaryKeyCondition(cls, target): - """Returns the name of the primary key of given class - - Arguments: - @ target: cls - Class that you want to know the primary key name from - """ - return getattr(cls, inspect(cls).primary_key[0].name) - -class AlchemyRecord(AlchemyBaseRecord): - """ """ - @classmethod - def FromPrimary(cls, session, p_key): - """Finds record based on given class and supplied primary key. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ p_key: integer - primary_key of the object to delete - Returns - cls - None - """ - try: - record = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).first() - except: - session.rollback() - raise - else: - if not record: - raise NotExistError(f"Record with primary key {p_key} does not exist") - return record - - @classmethod - def DeletePrimary(cls, session, p_key): - """Deletes record base on primary key from given class. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ p_key: integer - primary_key of the object to delete - - Returns: - isdeleted: boolean - """ - try: - isdeleted = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).delete() - except: - session.rollback() - raise - else: - session.commit() - return isdeleted - - @classmethod - def Create(cls, session, record): - """Creates a new instance and commits it to the database - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ record: dict - Dictionary with all key:value pairs that are required for the db record - Returns: - cls - """ - return cls(session, record) - - @classmethod - def List(cls, session, conditions=None, limit=None, offset=None, - order=None, yield_unlimited_total_first=False): - """Yields a Record object for every table entry. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - % conditions: list - Optional query portion that will be used to limit the list of results. - If multiple conditions are provided, they are joined on an 'AND' string. - For example: conditions=[User.id <= 10, User.id >=] - % limit: int ~~ None - Specifies a maximum number of items to be yielded. The limit happens on - the database side, limiting the query results. - % offset: int ~~ None - Specifies the offset at which the yielded items should start. Combined - with limit this enables proper pagination. - % order: tuple of operants - For example the User class has 3 fields; id, username, password. We can pass - the field we want to order on to the tuple like so; - (User.id.asc(), User.username.desc()) - % yield_unlimited_total_first: bool ~~ False - Instead of yielding only Record objects, the first item returned is the - number of results from the query if it had been executed without limit. - - Returns: - integer: integer with length of results. - list: List of classes from request type - """ - try: - query = session.query(cls) - if conditions: - for condition in conditions: - query = query.filter(condition) - if order: - for item in order: - query = query.order_by(item) - if limit: - query = query.limit(limit) - if offset: - query = query.offset(offset) - result = query.all() - except: - session.rollback() - raise - else: - if yield_unlimited_total_first: - return len(result) - return result - - @classmethod - def Update(cls, session, conditions, values): - """Update table based on conditions. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ conditions: list|tuple - for example: [User.id > 2, User.id < 100] - @ values: dict - for example: {User.username: 'value'} - """ - try: - query = session.query(cls) - for condition in conditions: - query = query.filter(condition) - query = query.update(values) - except: - session.rollback() - raise - else: - session.commit() - - def Delete(self): - """Delete current instance from the database""" - try: - isdeleted = self.session.query(type(self)).filter(self._PrimaryKeyCondition(self) == self.key).delete() - except: - self.session.rollback() - raise - else: - self.session.commit() - return isdeleted - - def Save(self): - """Saves any changes made in the current record. Sqlalchemy automatically detects - these changes and only updates the changed values. If no values are present - no query will be commited.""" - self.session.commit() - -class Smorgasbord(object): - """A connection tracker for uWeb3 Record classes. - - The idea is that you can set up a Smorgasbord with various different - connection types (Mongo and relational), and have the smorgasbord provide the - correct connection for the caller's needs. MongoReceord would be given the - MongoDB connection as expected, and all other users will be given a relational - database connection. - - This is highly beta and debugging is going to be at the very least interesting - because of __getattribute__ overriding that is necessary for this type of - behavior. - """ - CONNECTION_TYPES = 'mongo', 'relational' - - def __init__(self, connections=None): - self.connections = {} if connections is None else connections - - def AddConnection(self, connection, con_type): - """Adds a connection and its type to the Smorgasbord. - - The connection type should be one of the strings defined in the class - constant `CONNECTION_TYPES`. - """ - if con_type not in self.CONNECTION_TYPES: - raise ValueError('Unknown connection type %r' % con_type) - self.connections[con_type] = connection - - def RelevantConnection(self): - """Returns the relevant database connection dependant on the caller model. - - If the caller model cannot be determined, the 'relational' database - connection is returned as a fallback method. - """ - # Figure out caller type or instance - # pylint: disable=W0212 - caller_locals = sys._getframe(2).f_locals - # pylint: enable=W0212 - if 'self' in caller_locals: - caller_cls = type(caller_locals['self']) - else: - caller_cls = caller_locals.get('cls', type) - # Decide the type of connection to return for this caller - if issubclass(caller_cls, MongoRecord): - con_type = 'mongo' - else: - con_type = 'relational' # This is the default connection to return. - try: - return self.connections[con_type] - except KeyError: - raise TypeError('There is no connection for type %r' % con_type) - - def __enter__(self): - """Proxies the transaction to the underlying relevant connection. - - This is not quite as transparent a passthrough as using __getattribute__, - but it necessary due to performance optimizations done in Python2.7 - """ - return self.RelevantConnection().__enter__() - - def __exit__(self, *args): - """Proxies the transaction to the underlying relevant connection. - - This is not quite as transparent a passthrough as using __getattribute__, - but it necessary due to performance optimizations done in Python2.7 - """ - return self.RelevantConnection().__exit__(*args) - - def __getattribute__(self, attribute): - try: - # Pray to God we haven't overloaded anything from our connection classes. - return super(Smorgasbord, self).__getattribute__(attribute) - except AttributeError: - return getattr(self.RelevantConnection(), attribute) - - def RecordTableNames(): """Yields Record subclasses that have been defined outside this module. @@ -1745,8 +1628,7 @@ def GetSubTypes(cls, seen=None): if sub not in seen: seen.add(sub) yield sub - for sub in GetSubTypes(sub, seen): - yield sub + yield from GetSubTypes(sub, seen) for cls in GetSubTypes(BaseRecord): # Do not yield subclasses defined in this module @@ -1754,59 +1636,59 @@ def GetSubTypes(cls, seen=None): yield cls.TableName(), cls -def RecordToDict(record, complete=False, recursive=False): - """Returns a dictionary representation of the Record. - - Arguments: - @ record: Record - A record object that should be turned to a dictionary - % complete: bool ~~ False - Whether the foreign references on the object should all be resolved before - converting the Record to a dictionary. Either way, existing resolved - references will be represented as complete dictionaries. - % recursive: bool ~~ False - When this and `complete` are set True, foreign references will recursively - be resolved, resulting in the entire tree to be expanded before it is - converted to a dictionary. - - Returns: - dict: dictionary representation of the record. - """ - record_dict = {} - record = record if complete else dict(record) - for key, value in record.items(): - if isinstance(value, Record): - if complete and recursive: - record_dict[key] = RecordToDict(value, complete=True, recursive=True) - else: - record_dict[key] = dict(value) - else: - record_dict[key] = value - return record_dict +import functools +class CachedPage(object): + """Abstraction class for the cached Pages table in the database.""" -def MakeJson(record, complete=False, recursive=False, indent=None): - """Returns a JSON object string of the given `record`. + MAXAGE = 61 - The record may be a regular Python dictionary, in which case it will be - converted to JSON, with a few additional conversions for date and time types. + @classmethod + def Clean(cls, connection, maxage=None): + """Deletes all cached pages that are older than MAXAGE. - If the record is a Record subclass, it is first passed through the - RecordToDict() function. The arguments `complete` and `recursive` function - similarly to the arguments on that function. + An optional 'maxage' integer can be specified instead of MAXAGE. + """ + with connection as cursor: + cursor.Execute("""delete + from + %s + where + TIME_TO_SEC(TIMEDIFF(UTC_TIMESTAMP(), created)) > %d + """ % ( + cls.TableName(), + (cls.MAXAGE if maxage is None else maxage) + )) - Returns: - str: JSON representation of the given record dictionary. - """ - def _Encode(obj): - if isinstance(obj, datetime.datetime): - return obj.strftime('%F %T') - if isinstance(obj, datetime.date): - return obj.strftime('%F') - if isinstance(obj, datetime.time): - return obj.strftime('%T') - - if isinstance(record, BaseRecord): - record = RecordToDict(record, complete=complete, recursive=recursive) - return simplejson.dumps( - record, default=_Encode, sort_keys=True, indent=indent) + @classmethod + def FromSignature(cls, connection, maxage, name, modulename, args, kwargs): + """Returns a cached page from the given signature.""" + with connection as cursor: + cache = cursor.Execute("""select + data, + TIME_TO_SEC(TIMEDIFF(UTC_TIMESTAMP(), created)) as age, + creating + from + %s + where + TIME_TO_SEC(TIMEDIFF(UTC_TIMESTAMP(), created)) < %d AND + name = %s AND + modulename = %s AND + args = %s AND + kwargs = %s + order by created desc + limit 1 + """ % ( + cls.TableName(), + (cls.MAXAGE if maxage is None else maxage), + connection.EscapeValues(name), + connection.EscapeValues(modulename), + connection.EscapeValues(args), + connection.EscapeValues(kwargs))) + + if cache: + if cache[0]['creating'] is not None: + raise CurrentlyWorking(cache[0]['age']) + return cls(connection, cache[0]) + else: + raise cls.NotExistError('No cached data found') diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index d35f4b41..c0c71a29 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -3,29 +3,30 @@ import datetime import logging +import magic import mimetypes import os import pyclbr import sys import threading import time +import hashlib +import glob from base64 import b64encode from pymysql import Error as pymysqlerr -import uweb3 -from uweb3.model import SecureCookie +import uweb3 +from ..connections import ConnectionManager from .. import response, templateparser -from .new_login import Users RFC_1123_DATE = '%a, %d %b %Y %T GMT' - class ReloadModules(Exception): """Signals the handler that it should reload the pageclass""" -class CacheStorage(object): +class CacheStorage: """A (semi) persistent storage for the PageMaker.""" def __init__(self): super(CacheStorage, self).__init__() @@ -142,22 +143,164 @@ def update(self, data=None, **kwargs): if kwargs: self.update(kwargs) -class BasePageMaker(object): - """Provides the base pagemaker methods for all the html generators.""" + +class XSRFToken: + def __init__(self, seed, remote_addr): + self.seed = seed + self.remote_addr = remote_addr + self.unix_today = time.mktime(datetime.datetime.now().date().timetuple()) + + def generate_token(self): + """Generate an XSRF token + + XSRF token is generated based on the unix timestamp from today, + a randomly generated seed and the IP addres from the request + """ + hashed = (str(self.unix_today) + self.seed + self.remote_addr).encode('utf-8') + h = hashlib.new('ripemd160') + h.update(hashed) + return h.hexdigest() + + +class Base: # Constant for persistent storage accross requests. This will be accessible # by all threads of the same application (in the same Python process). PERSISTENT = CacheStorage() # Base paths for templates and public data. These are used in the PageMaker # classmethods that set up paths specific for that pagemaker. - PUBLIC_DIR = 'static' TEMPLATE_DIR = 'templates' - _registery = [] - # Default Static() handler cache durations, per MIMEtype, in days - CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7}) + def __init__(self): + self.persistent = self.PERSISTENT + # clean up any request tags in the template parser, We do this in the init + # because due to crashes we might not have triggered any __del__ or similar + # end of request code. + if '__parser' in self.persistent: + self.persistent.Get('__parser').ClearRequestTags() + + def _PostInit(self): + pass + + @property + def parser(self): + """Provides a templateparser.Parser instance. - def __init__(self, req, config=None, secure_cookie_secret=None, executing_path=None): - """sets up the template parser and database connections + If the config file specificied a [templates] section and a `path` is + assigned in there, this path will be used. + Otherwise, the `TEMPLATE_DIR` will be used to load templates from. + """ + if '__parser' not in self.persistent: + self.persistent.Set('__parser', templateparser.Parser( + self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) + parser = self.persistent.Get('__parser') + parser.dictoutput = self.req.noparse + return parser + + +class WebsocketPageMaker(Base): + """Pagemaker for the websocket routes. + This is different from the BasePageMaker as we choose to not have a database connection + in our WebSocketPageMaker. + + This class lacks pretty much all functionality that the BasePageMaker has. + """ + + #TODO: Add request to pagemaker? + def Connect(self, sid, env): + """This is the connect event, + sets the req variable that contains the request headers. + """ + print(f"User connected with SocketID {sid}: ") + self.req = env + + +class XSRFMixin: + """Provides XSRF protection by enabling setting xsrf token cookies, checking + them and setting a flag based on their value + + A seperate decorator can then be used to clear the POST/GET/PUT variables if + needed in specific pagemaker functions depending on that page's security + context. + """ + XSRFCOOKIE = 'xsrf' + XSRF_seed = str(os.urandom(32)) + + def validatexsrf(self): + """Sets the invalid_xsrf_token flag to true or false""" + self.invalid_xsrf_token = False + if self.req.method != 'GET': # GET calls will be ignored, but will set a cookie + self.invalid_xsrf_token = True + try: + user_supplied_xsrf_token = getattr(self, self.req.method.lower()).getfirst(self.XSRFCOOKIE) + self.invalid_xsrf_token = (self.cookies.get(self.XSRFCOOKIE) != user_supplied_xsrf_token) + except Exception: + # any error in looking up the cookie of the supplied post vars will result in a invalid xsrf token flag + pass + # If no cookie is present, set it. + self._Set_XSRF_cookie() + + def _Set_XSRF_cookie(self): + """This creates a new XSRF token for this client, which is IP bound, and + stores it in a cookie. + """ + xsrf_cookie = self.cookies.get(self.XSRFCOOKIE, False) + if not xsrf_cookie: + xsrf_cookie = XSRFToken(self.XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token() + self.req.AddCookie(self.XSRFCOOKIE, xsrf_cookie, path="/", httponly=True) + return xsrf_cookie + + def XSRFInvalidToken(self): + """Returns an error message regarding an incorrect XSRF token.""" + errorpage = templateparser.FileTemplate(os.path.join( + os.path.dirname(__file__), 'http_403.html')) + error = """Your browser did not send us the correct token, any token at all, or a timed out token. + Because of this we cannot allow you to perform this action at this time. Please refresh the previous page and try again.""" + + return uweb3.Response(content=errorpage.Parse(error=error), + httpcode=403, headers=self.req.response.headers) + + def _Get_XSRF(self): + """Easy access to the XSRF token""" + try: + return self.cookies[self.XSRFCOOKIE] + except KeyError: + return self._Set_XSRF_cookie() + + +class LoginMixin: + """This mixin provides a few methods that help with handling logins, sessions + and related database/cookie interaction""" + + def _ReadSession(self): + return NotImplemented + + @property + def user(self): + """Returns the current user""" + if not hasattr(self, '_user') or not self._user: + try: + self._user = self._ReadSession() + except ValueError: + self._user = False + return self._user + + +class BasePageMaker(Base): + """Provides the base pagemaker methods for all the html generators.""" + + PUBLIC_DIR = 'static' + # Default Static() handler cache durations, per MIMEtype, in days + CACHE_DURATION = MimeTypeDict( + {'text': 7, + 'image': 30, + 'application': 7, + 'text/css': 7}) + + def __init__(self, + req, + config=None, + executing_path=None): + """sets up the template parser and database connections. Arguments: @ req: request.Request @@ -165,119 +308,140 @@ def __init__(self, req, config=None, secure_cookie_secret=None, executing_path=N % config: dict ~~ None Configuration for the pagemaker, with database connection information and other settings. This will be available through `self.options`. + % executing_path: str/path + This is the path to the uWeb3 routing file. """ + super(BasePageMaker, self).__init__() self.__SetupPaths(executing_path) self.req = req self.cookies = req.vars['cookie'] self.get = req.vars['get'] - self.post = req.vars['post'] - self.options = config or {} - self.persistent = self.PERSISTENT - self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) - # self.user = self._GetLoggedInUser() - - def _PostRequest(self, response): - if response.status == '500 Internal Server Error': - if not hasattr(self, 'connection_error'): #this is set when we try and create a connection but it failed - self.connection_error = False - if hasattr(self, 'connection'): - if self.connection.open: - self.connection.close() - self.persistent.Del("__mysql") - return response - - def XSRFInvalidToken(self, command): - """Returns an error message regarding an incorrect XSRF token.""" - page_data = self.parser.Parse('403.html', error=command, - **self.CommonBlocks('Invalid XSRF token')) - return uweb3.Response(content=page_data, httpcode=403) - - # def _GetLoggedInUser(self): - # """Checks if user is logged in based on cookie""" - # scookie = SecureCookie(self.secure_cookie_connection) - # if not scookie.cookiejar.get('login'): - # return None - # try: - # user = scookie.cookiejar.get('login') - # except Exception: - # self.req.DeleteCookie('login') - # return None - # if not user: - # return None - # return Users(None, user) + self.post = req.vars['post'] if 'post' in req.vars else {} + self.put = req.vars['put'] if 'put' in req.vars else {} + self.delete = req.vars['delete'] if 'delete' in req.vars else {} + self.files = req.vars['files'] if 'files' in req.vars else {} + self.config = config or None + self.options = config.options if config else {} + self.debug = DebuggerMixin in self.__class__.__mro__ + try: + self.connection = self.persistent.Get('connection') + except KeyError: + self.persistent.Set('connection', ConnectionManager(self.config, self.options, self.debug)) + self.connection = self.persistent.Get('connection') + + def __str__(self): + return str(type(self)) @classmethod - def LoadModules(cls, default_routes='routes', excluded_files=('__init__', '.pyc')): + def LoadModules(cls, routes='routes/*.py'): """Loops over all .py files apart from some exceptions in target directory Looks for classes that contain pagemaker + + Arguments: + % default_routes: str + Location to your route files. Defaults to routes/*.py + Supports glob style syntax, non recursive. """ bases = [] - routes = os.path.join(os.getcwd(), default_routes) - for path, dirnames, filenames in os.walk(routes): - for filename in filenames: - name, ext = os.path.splitext(filename) - if name not in excluded_files and ext not in excluded_files: - f = os.path.relpath(os.path.join(os.getcwd(), default_routes, filename[:-3])).replace('/', '.') - example_data = pyclbr.readmodule_ex(f) - for name, data in example_data.items(): - if hasattr(data, 'super'): - if 'PageMaker' in data.super[0]: - module = __import__(f, fromlist=[name]) - bases.append(getattr(module, name)) + for file in glob.glob(routes): + module = os.path.relpath(os.path.join(os.getcwd(), file[:-3])).replace('/', '.') + classlist = pyclbr.readmodule_ex(module) + for name, data in classlist.items(): + if hasattr(data, 'super') and 'PageMaker' in data.super[0]: + module = __import__(file, fromlist=[name]) + bases.append(getattr(module, name)) return bases def _PostInit(self): """Method that gets called for derived classes of BasePageMaker.""" + def _ConnectionRollback(self): + """Roll back all connections, this method can be overwritten by the user""" + self.connection.Rollback() + @classmethod def __SetupPaths(cls, executing_path): """This sets up the correct paths for the PageMaker subclasses. From the passed in `cls`, it retrieves the filename. Of that path, the - directory is used as the working directory. Then, the module constants - PUBLIC_DIR and TEMPLATE_DIR are used to define class constants from. + directory is used as the working directory. Then, the module constant + TEMPLATE_DIR is used to define class constants from. """ - # Unfortunately, mod_python does not always support retrieving the caller - # filename using sys.modules. In those cases we need to query the stack. - # pylint: disable=W0212 - try: - local_file = os.path.abspath(sys.modules[cls.__module__].__file__) - except KeyError: - # This happens for old-style mod_python solutions: The pages file is - # imported through the mechanics of mod_pythoif '__mysql' not in self.persistent: (not package imports) and - # isn't known in sys modules. We use the CPython implementation details - # to get the correct executing file. - frame = sys._getframe() - initial = frame.f_code.co_filename - # pylint: enable=W0212 - while initial == frame.f_code.co_filename: - if not frame.f_back: - break # This happens during exception handling of DebuggingPageMaker - frame = frame.f_back - local_file = frame.f_code.co_filename + local_file = os.path.abspath(sys.modules[cls.__module__].__file__) cls.LOCAL_DIR = cls_dir = executing_path - cls.PUBLIC_DIR = os.path.join(cls_dir, cls.PUBLIC_DIR) - cls.TEMPLATE_DIR = os.path.join(cls_dir, cls.TEMPLATE_DIR) + cls.PUBLIC_DIR = os.path.realpath(os.path.join(cls_dir, cls.PUBLIC_DIR)) + cls.TEMPLATE_DIR = os.path.realpath(os.path.join(cls_dir, cls.TEMPLATE_DIR)) - @property - def parser(self): - """Provides a templateparser.Parser instance. + def Static(self, rel_path, content_type=None): + """Provides a handler for static content. - If the config file specificied a [templates] section and a `path` is - assigned in there, this path will be used. - Otherwise, the `TEMPLATE_DIR` will be used to load templates from. + The requested `path` is truncated against a root (removing any uplevels), + and then added to the working dir + PUBLIC_DIR. If the request file exists, + then the requested file is retrieved, if needed mimetype guessed, and + returned to the client performing the request. + + Should the requested file not exist, a 404 page is returned instead. + + Arguments: + @ rel_path: str + The filename relative to the working directory of the webserver. + @ content_type: str + The content_type we will send to the client, If None it will be guessed + based on the extention or by the contents of the file (in that order). + If no guess can be made, text/plain will be used. + + Returns: + Page: contains the content and mimetype of the requested file, or a 404 + page if the file was not available on the local path. """ - if '__parser' not in self.persistent: - self.persistent.Set('__parser', templateparser.Parser( - self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) - return self.persistent.Get('__parser') + + abs_path = os.path.realpath(os.path.join(self.PUBLIC_DIR, rel_path.lstrip('/'))) + if self.debug: + print('Serving static file:', abs_path) + + if os.path.commonprefix((abs_path, self.PUBLIC_DIR)) != self.PUBLIC_DIR: + return self._StaticNotFound(rel_path) + try: + if not content_type: + content_type, _encoding = mimetypes.guess_type(abs_path) + if not content_type: + content_type = magic.from_file(abs_path, mime=True) + if not content_type: + content_type = 'text/plain' + mtime = os.path.getmtime(abs_path) + length = os.path.getsize(abs_path) + readtype = 'r' if content_type.startswith('text/') else 'rb' + + with open(abs_path, readtype) as staticfile: + cache_days = self.CACHE_DURATION.get(content_type, 0) + expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) + return response.Response(content=staticfile.read(), + content_type=content_type, + headers={'Expires': expires.strftime(RFC_1123_DATE), + 'cache-control': 'max-age=%d' % + (cache_days*24*60*60), + 'last-modified': time.ctime(mtime), + 'content-length': length}) + except IOError: + return self._StaticNotFound(rel_path) + + def _StaticNotFound(self, _path): + message = 'This is not the path you\'re looking for. No such file %r' % ( + self.req.env['PATH_INFO']) + return response.Response(message, content_type='text/plain', httpcode=404) + + def _NotFound(self, _path): + message = 'This is not the path you\'re looking for. No such path %r' % ( + self.req.env['PATH_INFO']) + return response.Response(message, content_type='text/html', httpcode=404) def InternalServerError(self, exc_type, exc_value, traceback): """Returns a plain text notification about an internal server error.""" + self.req.errorlogger.error( + 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r', + self.req.path, exc_info=(exc_type, exc_value, traceback)) error = 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r' % ( self.req.path) - self.req.registry.logger.error( - error, exc_info=(exc_type, exc_value, traceback)) return response.Response( content=error, content_type='text/plain', httpcode=500) @@ -286,37 +450,20 @@ def Reload(): """Raises `ReloadModules`, telling the Handler() to reload its pageclass.""" raise ReloadModules('Reloading ... ') - def _GetXSRF(self): - if 'xsrf' in self.cookies: - return self.cookies['xsrf'] - return None - - def CommonBlocks(self, title, page_id=None, scripts=None): - """Returns a dictionary with the header and footer in it.""" - if not page_id: - page_id = title.replace(' ', '_').lower() - - return {'header': self.parser.Parse( - 'header.html', title=title, page_id=page_id, user=self.user - ), - 'footer': self.parser.Parse( - 'footer.html', year=time.strftime('%Y'), user=self.user, - page_id=page_id, scripts=scripts - ), - 'page_id': page_id, - 'xsrftoken': self._GetXSRF(), - } - - -class DebuggerMixin(object): + def CloseRequestConnections(self): + """Method that gets called after each request to close 'request' based + connections like signedcookieStores""" + self.connection.PostRequest() + + +class DebuggerMixin: """Replaces the default handler for Internal Server Errors. This one prints a host of debugging and request information, though it still lacks interactive functions. """ CACHE_DURATION = MimeTypeDict({}) - ERROR_TEMPLATE = templateparser.FileTemplate(os.path.join( - os.path.dirname(__file__), 'http_500.html')) + ERROR_TEMPLATE = 'http_500.html' def _ParseStackFrames(self, stack): """Generates list items for traceback information. @@ -366,7 +513,7 @@ def _SourceLines(filename, line_num, context=3): def InternalServerError(self, exc_type, exc_value, traceback): """Returns a HTTP 500 response with detailed failure analysis.""" - self.req.registry.logger.error( + self.req.errorlogger.error( 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r', self.req.path, exc_info=(exc_type, exc_value, traceback)) exception_data = { @@ -377,12 +524,15 @@ def InternalServerError(self, exc_type, exc_value, traceback): 'error_for_error': False, 'exc': {'type': exc_type, 'value': exc_value, 'traceback': self._ParseStackFrames(traceback)}} + + error_template = templateparser.FileTemplate(os.path.join( + os.path.dirname(__file__), self.ERROR_TEMPLATE)) try: return response.Response( - self.ERROR_TEMPLATE.Parse(**exception_data), httpcode=500) + error_template.Parse(**exception_data), httpcode=500) except Exception: exc_type, exc_value, traceback = sys.exc_info() - self.req.registry.logger.critical( + self.req.errorlogger.error( 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF ERROR PAGE', exc_info=(exc_type, exc_value, traceback)) exception_data['error_for_error'] = True @@ -390,134 +540,104 @@ def InternalServerError(self, exc_type, exc_value, traceback): exception_data['exc'] = {'type': exc_type, 'value': exc_value, 'traceback': self._ParseStackFrames(traceback)} return response.Response( - self.ERROR_TEMPLATE.Parse(**exception_data), httpcode=500) + error_template.Parse(**exception_data), httpcode=500) -class MongoMixin(object): - """Adds MongoDB support to PageMaker.""" - @property - def mongo(self): - """Returns a MongoDB database connection.""" - if '__mongo' not in self.persistent: - import pymongo - mongo_config = self.options.get('mongo', {}) - connection = pymongo.connection.Connection( - host=mongo_config.get('host'), - port=mongo_config.get('port')) - if 'database' in mongo_config: - self.persistent.Set('__mongo', connection[mongo_config['database']]) - else: - self.persistent.Set('__mongo', connection) - return self.persistent.Get('__mongo') - - -class SqlAlchemyMixin(object): - """Adds MysqlAlchemy connection to PageMaker.""" - @property - def engine(self): - if '__sql_alchemy' not in self.persistent: - from sqlalchemy import create_engine - mysql_config = self.options['mysql'] - engine = create_engine('mysql://{username}:{password}@{host}/{database}'.format( - username=mysql_config.get('user'), - password=mysql_config.get('password'), - host=mysql_config.get('host', 'localhost'), - database=mysql_config.get('database')), pool_size=5, max_overflow=0) - self.persistent.Set('__sql_alchemy', engine) - return self.persistent.Get('__sql_alchemy') +class CSPMixin: + """Provides CSP header output. - @property - def session(self): - from sqlalchemy.orm import sessionmaker - Session = sessionmaker() - Session.configure(bind=self.engine, expire_on_commit=False) - return Session() - -class MysqlMixin(object): - """Adds MySQL support to PageMaker.""" - @property - def connection(self): - """Returns a MySQL database connection.""" - try: - if '__mysql' not in self.persistent: - from underdark.libs.sqltalk import mysql - mysql_config = self.options['mysql'] - self.persistent.Set('__mysql', mysql.Connect( - host=mysql_config.get('host', 'localhost'), - user=mysql_config.get('user'), - passwd=mysql_config.get('password'), - db=mysql_config.get('database'), - charset=mysql_config.get('charset', 'utf8'), - debug=DebuggerMixin in self.__class__.__mro__)) - return self.persistent.Get('__mysql') - except Exception as e: - self.connection_error = True - raise e - - - -class SqliteMixin(object): - """Adds SQLite support to PageMaker.""" - @property - def connection(self): - """Returns an SQLite database connection.""" - if '__sqlite' not in self.persistent: - from underdark.libs.sqltalk import sqlite - self.persistent.Set('__sqlite', sqlite.Connect( - self.options['sqlite']['database'])) - return self.persistent.Get('__sqlite') - -class SmorgasbordMixin(object): - """Provides multiple-database connectivity. - - This enables a developer to use a single 'connection' property (`bord`) which - can be used for regular relation database and MongoDB access. The caller will - be given the relation database connection, unless Smorgasbord is aware of - the caller's needs for another database connection. + https://content-security-policy.com/ """ - class Connections(dict): - """Connection autoloading class for Smorgasbord.""" - def __init__(self, pagemaker): - super(SmorgasbordMixin.Connections, self).__init__() - self.pagemaker = pagemaker - - def __getitem__(self, key): - """Returns the requested database connection type. - - If the database connection type isn't locally available, it is retrieved - using one of the _Load* methods. - """ - try: - return super(SmorgasbordMixin.Connections, self).__getitem__(key) - except KeyError: - return self.setdefault(key, getattr(self, '_Load%s' % key.title())()) + _csp = { + "default-src": ("'none'",), + "object-src": ("'none'",), + "script-src": ("'none'",), + "style-src": ("'none'",), + "form-action": ("'none'",), + "connect-src": ("'none'",), + "img-src": ("'none'",), + "font-src": ("'none'",), + "frame-ancestors": ("'none'",), + "base-uri": ("'none'",) + } + + def _SetCsp(self, resourcetype="default-src", urls=("'self'", ), append=True): + """Add a new CSP url to the csp headers for the given resourcetype. + + resourcetype is any of the CSP resource types as defined in: + https://content-security-policy.com/#directive + defaults to: default-src + + urls should be one or more of: + https://content-security-policy.com/#source_list + default to 'self' + string or tuple/list is allowed + + By default this appends to the already present list of sources for the given + resourcetype - def _LoadMongo(self): - """Returns the PageMaker's MongoDB connection.""" - return self.pagemaker.mongo + """ + if isinstance(urls, str): + urls = [urls, ] + else: + urls = list(urls) if type(urls) == tuple else urls + if resourcetype not in self._csp: + self._csp[resourcetype] = [] + if self._csp[resourcetype] == "'none'" or not append: + self._csp[resourcetype] = urls + return + self._csp[resourcetype].extend(urls) + + def _CSPFromConfig(self, config): + """sets the CSP headers from a Dictionary + Dict keys should be resourcetypes, values should be lists of urls + + resourcetype is any of the CSP resource types as defined in: + https://content-security-policy.com/#directive + + urls are in the form: + https://content-security-policy.com/#source_list + """ + self._csp = config - def _LoadRelational(self): - """Returns the PageMaker's relational database connection.""" - return self.pagemaker.connection + def _CSPheaders(self): + """Adds the constructed CSP header to the request""" + csp = '; '.join( + "%s %s" % (key, ' '.join(value)) for key, value in self._csp.items()) + self.req.AddHeader('Content-Security-Policy', csp) - @property - def bord(self): - """Returns a Smorgasbord of autoloading database connections.""" - if '__bord' not in self.persistent: - from .. import model - self.persistent.Set('__bord', model.Smorgasbord( - connections=SmorgasbordMixin.Connections(self))) - return self.persistent.Get('__bord') +class SparseAsyncPages(BasePageMaker): + """This mixin provides the template download functionality for client side + parsing based on the sparse json output functionality in the templateparser. + + The client is to download the template, (and cache them themselves, and do + replacements on their own based on the sparse page output they received. + """ + def SparseTemplateProvider(self, content_hash, path): + """This provides the client with the raw template as used by the server side + templateparser.""" + try: + template = self.parser[path] + except IOError: + return response.Response("This template does not exists", + content_type='text/plain', httpcode=404) + if template._template_hash != content_hash: + return response.Response("This template does not exists anymore. %s %s" % (template._template_hash, content_hash), + content_type='text/plain', httpcode=404) + return response.Response(template, content_type='text/plain') + + def SparseRenderedProvider(self): + return response.Response(templateparser.FileTemplate(os.path.join( + os.path.dirname(__file__), 'sparserenderer.js')), + content_type='application/javascript') # ############################################################################## # Classes for public use (wildcard import) # -class SqAlchemyPageMaker(SqlAlchemyMixin, BasePageMaker): - """The basic PageMaker class, providing MySQL support.""" +class PageMaker(XSRFMixin, BasePageMaker): + """The basic PageMaker class, providing XSRF support.""" -class PageMaker(MysqlMixin, BasePageMaker): - """The basic PageMaker class, providing MySQL support.""" class DebuggingPageMaker(DebuggerMixin, PageMaker): """The same basic PageMaker, with added debugging on HTTP 500.""" diff --git a/uweb3/pagemaker/admin.py b/uweb3/pagemaker/admin.py deleted file mode 100644 index 5794dc06..00000000 --- a/uweb3/pagemaker/admin.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/python -"""uWeb3 PageMaker Mixins for admin purposes.""" - -# Standard modules -import datetime -import decimal -import inspect -import os - -# Package modules -from .. import model -from .. import templateparser - -NOT_ALLOWED_METHODS = dir({}) + ['key', 'identifier'] - -FIELDTYPES = {'datetime': datetime.datetime, - 'decimal': decimal.Decimal} - -class AdminMixin(object): - """Provides an admin interface based on the available models""" - - def _Admin(self, url): - self.parser.RegisterFunction('classname', lambda cls: type(cls).__name__) - - if not self.ADMIN_MODEL: - return 'Setup ADMIN_MODEL first' - indextemplate = templateparser.FileTemplate( - os.path.join(os.path.dirname(__file__), 'admin', 'index.html')) - - urlparts = (url or '').split('/') - table = None - method = 'List' - methods = None - results = None - columns = None - basepath = self.__BasePath() - resultshtml = [] - columns = None - edithtml = None - message = None - docs = None - if len(urlparts) > 2: - if urlparts[1] == 'table': - table = urlparts[2] - methods = self.__AdminTablesMethods(table) - docs = self.__GetClassDocs(table) - if len(urlparts) > 3: - method = urlparts[3] - if method == 'edit': - edithtml = self.__EditRecord(table, urlparts[4]) - elif method == 'delete': - key = self.post.getfirst('key') - if self.__DeleteRecord(table, key): - message = '%s with key %s deleted.' %(table, key) - else: - message = 'Could not find %s with key %s.' %(table, key) - elif method == 'save': - message = self.__SaveRecord(table, self.post.getfirst('key')) - else: - (columns, results) = self.__AdminTablesMethodsResults(urlparts[2], - method) - - resulttemplate = templateparser.FileTemplate( - os.path.join(os.path.dirname(__file__), 'admin', 'record.html')) - - for result in results: - resultshtml.append(resulttemplate.Parse(result=result['result'], - key=result['key'], - table=table, - basepath=basepath, - fieldtypes=FIELDTYPES)) - elif urlparts[1] == 'method': - table = urlparts[2] - methods = self.__AdminTablesMethods(table) - docs = self.__GetDocs(table, urlparts[3]) - return indextemplate.Parse(basepath=basepath, - tables=self.__AdminTables(), - table=table, - columns=columns, - method=method, - methods=methods, - results=resultshtml, - edit=edithtml, - message=message, - docs=docs) - - def __GetDocs(self, table, method): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - methodobj = getattr(table, method) - if methodobj.__doc__: - return inspect.cleandoc(methodobj.__doc__) - try: - while table: - table = table.__bases__[0] - methodobj = getattr(table, method) - if methodobj.__doc__: - return inspect.cleandoc(methodobj.__doc__) - except AttributeError: - pass - return 'No documentation avaiable' - - def __GetClassDocs(self, table): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - if table.__doc__: - return inspect.cleandoc(table.__doc__) - try: - while table: - table = table.__bases__[0] - if table.__doc__: - return inspect.cleandoc(table.__doc__) - except AttributeError: - pass - return 'No documentation avaiable' - - def __EditRecord(self, table, key): - self.parser.RegisterFunction('classname', lambda cls: type(cls).__name__) - edittemplate = templateparser.FileTemplate( - os.path.join(os.path.dirname(__file__), 'admin', 'edit.html')) - fields = self.__EditRecordFields(table, key) - if not fields: - return 'Could not load record with %s' % key - return edittemplate.Parse(table=table, - key=key, - basepath=self.__BasePath(), - fields=fields, - fieldtypes=FIELDTYPES) - - def __SaveRecord(self, table, key): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - try: - obj = table.FromPrimary(self.connection, key) - except model.NotExistError: - return 'Could not load record with %s' % key - for item in obj.keys(): - if (isinstance(obj[item], int) or - isinstance(obj[item], long)): - obj[item] = int(self.post.getfirst(item, 0)) - elif (isinstance(obj[item], float) or - isinstance(obj[item], decimal.Decimal)): - obj[item] = float(self.post.getfirst(item, 0)) - elif isinstance(obj[item], basestring): - obj[item] = self.post.getfirst(item, '') - elif isinstance(obj[item], datetime.datetime): - obj[item] = self.post.getfirst(item, '') - else: - obj[item] = int(self.post.getfirst(item, 0)) - try: - obj.Save() - except Exception, error: - return error - return 'Changes saved' - return 'Invalid table' - - def __DeleteRecord(self, table, key): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - try: - obj = table.FromPrimary(self.connection, key) - obj.Delete() - return True - except model.NotExistError: - return False - return False - - def __BasePath(self): - return self.req.path.split('/')[1] - - def __EditRecordFields(self, table, key): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - try: - return table.FromPrimary(self.connection, key) - except model.NotExistError: - return False - return False - - def __CheckTable(self, table): - """Verfies the given name is that of a model.BaseRecord subclass.""" - tableclass = getattr(self.ADMIN_MODEL, table) - return type(tableclass) == type and issubclass(tableclass, model.Record) - - def __AdminTables(self): - tables = [] - for table in dir(self.ADMIN_MODEL): - if self.__CheckTable(table): - tables.append(table) - return tables - - def __AdminTablesMethods(self, table): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - methods = [] - for method in dir(table): - if (not method.startswith('_') - and method not in NOT_ALLOWED_METHODS - and callable(getattr(table, method))): - methods.append(method) - return methods - return False - - def __AdminTablesMethodsResults(self, tablename, methodname='List'): - if self.__CheckTable(tablename): - table = getattr(self.ADMIN_MODEL, tablename) - method = getattr(table, methodname) - results = method(self.connection) - resultslist = [] - for result in results: - resultslist.append({'result': result.values(), - 'key': result.key}) - if resultslist: - return result.keys(), resultslist - return (), () diff --git a/uweb3/pagemaker/admin/edit.html b/uweb3/pagemaker/admin/edit.html deleted file mode 100644 index 639a24b1..00000000 --- a/uweb3/pagemaker/admin/edit.html +++ /dev/null @@ -1,23 +0,0 @@ -

Editing: [table], key: [key]

-
- -
    -{{ for key, value in [fields|items] }} -
  • - {{ if isinstance([value], basestring) }} - - {{ elif isinstance([value], int) or isinstance([value], long) or isinstance([value], float) or isinstance([value], [fieldtypes:decimal]) }} - - {{ elif isinstance([value], [fieldtypes:datetime]) }} - - {{ elif not [value] }} - - {{ else }} - - [value:key] ([value|classname]) - {{ endif }} -
  • -{{ endfor }} -
  • -
-
diff --git a/uweb3/pagemaker/admin/index.html b/uweb3/pagemaker/admin/index.html deleted file mode 100644 index db05bf0a..00000000 --- a/uweb3/pagemaker/admin/index.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - µWeb, Admin interface {{if [table]}}[table]{{ endif }} {{if [method]}} - [method]{{ endif }} - - - -
- {{ if [results] }} -

Results for [table] / [method]

- - - - - {{ for column in [columns]}} - - {{ endfor }} - - - - - - {{ for result in [results] }} - [result|raw] - {{ endfor }} - -
[column|raw]EditDelete
- {{ elif [edit]}} - [edit|raw] - {{ elif [message]}} - [message] - {{ endif }} - {{ if [docs]}} -

Documentation {{ if not [results] and not [edit]}} for [method] on [table] {{ endif }}

-
[docs]
- {{ endif }} -
- - - diff --git a/uweb3/pagemaker/admin/record.html b/uweb3/pagemaker/admin/record.html deleted file mode 100644 index 66fbe460..00000000 --- a/uweb3/pagemaker/admin/record.html +++ /dev/null @@ -1,21 +0,0 @@ - - - {{ for column in [result] }} - {{ if isinstance([column], basestring) }} - [column] - {{ elif isinstance([column], int) or isinstance([column], long) or isinstance([column], float) or isinstance([column], [fieldtypes:decimal]) }} - [column] - {{ elif isinstance([column], [fieldtypes:datetime]) }} - [column] - {{ elif not [column] }} - None - {{ else }} - [column:key] - {{ endif }} - {{ endfor }} - Edit -
- - -
- diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 8397fd1e..fd432b22 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -1,148 +1,178 @@ """This file holds all the decorators we use in this project.""" +import codecs +from datetime import datetime +import hashlib +import pickle +import pytz +import json +import time + import uweb3 -import _mysql_exceptions +from uweb3 import model +from uweb3.request import IndexedFieldStorage def loggedin(f): - """Decorator that checks if the user requesting the page is logged in.""" - def wrapper(*args, **kwargs): - try: - args[0].user = args[0]._CurrentUser() - except (uweb.model.NotExistError, args[0].NoSessionError): - path = '/login' - if args[0].req.env['PATH_INFO'].strip() != '': - path = '%s/%s' % (path, args[0].req.env['PATH_INFO'].strip()) - return uweb.Redirect(path) - return f(*args, **kwargs) - return wrapper + """Decorator that checks if the user requesting the page is logged in based on set cookie.""" + def wrapper(*args, **kwargs): + if not args[0].user: + return args[0].RequestLogin() + return f(*args, **kwargs) + return wrapper def checkxsrf(f): - """Decorator that checks the user's XSRF. + """Decorator that checks the user's XSRF. + The function will compare the XSRF in the user's cookie and in the + (post) request. + """ + def _clear_form_data(pagemaker): + method = pagemaker.req.method.lower() + # Set an attribute in the pagemaker that holds the form data on an invalid XSRF validation + pagemaker.invalid_xsrf_data = getattr(pagemaker, method) + # Remove the form data from the PageMaker + setattr(pagemaker, method, IndexedFieldStorage()) + # Remove the form data from the Request class + pagemaker.req.vars[method] = IndexedFieldStorage() + if 'files' in pagemaker.req.vars: + pagemaker.req.vars['files'] = {} + return pagemaker + + def wrapper(*args, **kwargs): + if args[0].req.method != "GET": + if args[0].invalid_xsrf_token: + _clear_form_data(args[0]) + return args[0].XSRFInvalidToken() + return f(*args, **kwargs) + return wrapper - The function will compare the XSRF in the user's cookie and in the - (post) request. - """ +def Cached(maxage=None, verbose=False, handler=None, *t_args, **t_kwargs): + """Decorator that wraps checks the cache table for a cached page. + The function will see if we have a recent cached output for this call, + or if one is being created as we speak. + Use by adding the decorator module and flagging a pagemaker function with + it. + from pages import decorators + @decorators.Cached(60) + def mypage() + + Arguments: + #TODO: Make handler an argument instead of a kwd since it is required? + @ handler: class CustomClass(model.Record, model.CachedPage) + This is some sort of custom mixin class that we use to store our cached page in the database + % maxage: int(60) + Cache time in seconds. + % verbose: bool(False) + Insert html comment with cache information. + Raises: + KeyError + """ + def cache_decorator(f): def wrapper(*args, **kwargs): - if args[0].incorrect_xsrf_token: - args[0].post.list = [] - return args[0].XSRFInvalidToken( - 'Your XSRF token was incorrect, please try again.') - return f(*args, **kwargs) + if not handler: + raise KeyError("A handler is required for storing this page into the database.") + create = False + name = f.__name__ + modulename = f.__module__ + handler.Clean(args[0].connection, maxage) + requesttime = time.time() + time.clock() + sleep = 0.3 + maxsleepinterval = 2 + try: # see if we have a cached version thats not too old + data = handler.FromSignature(args[0].connection, + maxage, + name, modulename, + json.dumps(args[1:]), + json.dumps(kwargs)) + if verbose: + data = '%s' % ( + pickle.loads(codecs.decode(data['data'].encode(), "base64")), + data['age']) + else: + data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) + except model.CurrentlyWorking: # we dont have anything fresh enough, but someones working on it + age = 0 + while age < maxage: # as long as there's no output, we should try periodically until we have waited too long + time.sleep(sleep) + age = (time.time() - requesttime) + try: + data = handler.FromSignature(args[0].connection, + maxage, + name, modulename, + json.dumps(args[1:]), + json.dumps(kwargs)) + break + except Exception: + sleep = min(sleep*2, maxsleepinterval) + try: + data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) + if verbose: + data += '' % age + except NameError: + create = True + except uweb3.model.NotExistError: # we don't have anything fresh enough, lets create + create = True + if create: + try: + now = str(pytz.utc.localize(datetime.utcnow()))[0:19] + # create the db row for this call, let other processes know we are working on it. + cache = handler.Create(args[0].connection, { + 'name': name, + 'modulename': modulename, + 'args': json.dumps(args[1:]), + 'kwargs': json.dumps(kwargs), + 'creating': now, + 'created': now + }) + data = f(*args, **kwargs) + cache['data'] = codecs.encode(pickle.dumps(data), "base64").decode() + # update the created time to now, as we are done. + cache['created'] = str(pytz.utc.localize(datetime.utcnow()))[0:19] + cache['creating'] = None + cache.Save() + if verbose: + data += '' + except Exception: #This is probably a pymysql Error. or db collision, whilst unfortunate, we wont break the page on this + pass + return data return wrapper + return cache_decorator -def validapikey(f): - """Decorator that checks if the user requesting the page is using a valid api key.""" +def ContentType(content_type): + """Decorator that wraps and returns sets the contentType.""" + def content_type_decorator(f): def wrapper(*args, **kwargs): - if not args[0].apikey: - return args[0].NoSessionError('Your API key was incorrect, please try again.') - return f(*args, **kwargs) + pageresult = f(*args, **kwargs) or {} + if not isinstance(pageresult, uweb3.Response): + return uweb3.Response(pageresult, + content_type=content_type) + if isinstance(pageresult, uweb3.Response): + pageresult.content_type = content_type + args[0].req.content_type = content_type + return pageresult return wrapper + return content_type_decorator -import simplejson -import pytz -from datetime import datetime -import time -import pickle -from underdarkcustomers import model -def Cached(maxage=None, verbose=False, *t_args, **t_kwargs): - """Decorator that wraps checks the cache table for a cached page. - - The function will see if we have a recent cached output for this call, - or if one is being created as we speak. - - Use by adding the decorator module and flagging a pagemaker function with - it. - - from pages import decorators - @decorators.Cached(60) - def mypage() - - Arguments: - maxage: int(60), cache time in seconds. - verbose: bool(false), insert html comment with cache information. - - """ - def cache_decorator(f): - def wrapper(*args, **kwargs): - create = False - name = f.__name__ - modulename = f.__module__ - model.CachedPage.Clean(args[0].connection, maxage) - requesttime = time.time() - time.clock() - sleep = 0.3 - try: # see if we have a cached version thats not too old - data = model.CachedPage.FromSignature(args[0].connection, - maxage, - name, modulename, - simplejson.dumps(args[1:]), - simplejson.dumps(kwargs)) - if verbose: - data = '%s' % ( - pickle.loads(str(data['data'])), - data['age']) - else: - data = pickle.loads(str(data['data'])) - except model.CurrentlyWorking: # we dont have anything fresh enough, but someones working on it - age = 0 - while age < maxage: # as long as there's no output, we should try periodically until we have waited too long - time.sleep(sleep) - age = (time.time() - requesttime) - try: - data = model.CachedPage.FromSignature(args[0].connection, - maxage, - name, modulename, - simplejson.dumps(args[1:]), - simplejson.dumps(kwargs)) - break - except Exception: - sleep = min(sleep*2, 2) - try: - if verbose: - data = '%s' % ( - pickle.loads(str(data['data'])), - age) - else: - data = pickle.loads(str(data['data'])) - except NameError: - create = True - except uweb.model.NotExistError: # we don't have anything fresh enough, lets create - create = True - if create: - try: - cache = model.CachedPage.Create(args[0].connection, { - 'name': name, - 'modulename': modulename, - 'args': simplejson.dumps(args[1:]), - 'kwargs': simplejson.dumps(kwargs), - 'creating': str(pytz.utc.localize(datetime.utcnow()))[0:19], - 'created': str(pytz.utc.localize(datetime.utcnow()))[0:19] - }) - data = f(*args, **kwargs) - cache['data'] = pickle.dumps(data) - cache['created'] = str(pytz.utc.localize(datetime.utcnow()))[0:19] - cache['creating'] = None - cache.Save() - if verbose: - data = '%s' % data - except _mysql_exceptions.OperationalError: - pass - return data - return wrapper - return cache_decorator +def CSP(resourcetype, urls, append=True): + """Decorator that injects a new CSP allowed source into the current csp output.""" + def csp_decorator(f): + def wrapper(*args, **kwargs): + args[0]._SetCsp(resourcetype, urls, append) + return f(*args, **kwargs) or {} + return wrapper + return csp_decorator def TemplateParser(template, *t_args, **t_kwargs): - """Decorator that wraps and returns the output. + """Decorator that wraps and returns the output. - The output is wrapped in a templateparser call if its not already something - that we prepared for direct output to the client. - """ - def template_decorator(f): - def wrapper(*args, **kwargs): - pageresult = f(*args, **kwargs) or {} - if not isinstance(pageresult, (str, uweb.Response, uweb.Redirect)): - pageresult.update(args[0].CommonBlocks(*t_args, **t_kwargs)) - return args[0].parser.Parse(template, **pageresult) - return pageresult - return wrapper - return template_decorator + The output is wrapped in a templateparser call if its not already something + that we prepared for direct output to the client. + """ + def template_decorator(f): + def wrapper(*args, **kwargs): + pageresult = f(*args, **kwargs) or {} + if not isinstance(pageresult, (str, uweb3.Response, uweb3.Redirect)): + return args[0].parser.Parse(template, **pageresult) + return pageresult + return wrapper + return template_decorator diff --git a/uweb3/pagemaker/http_403.html b/uweb3/pagemaker/http_403.html new file mode 100644 index 00000000..c96a76dd --- /dev/null +++ b/uweb3/pagemaker/http_403.html @@ -0,0 +1,27 @@ + + + + Your session has timed our. µWeb3 403 Error. + + + + +
+

Error, your session has timed out (HTTP 403)

+
+
+
+ {{if [error] }} +

[error]

+ {{ endif }} +
+
+ + diff --git a/uweb3/pagemaker/http_500.html b/uweb3/pagemaker/http_500.html index d7a3a92b..c48a949c 100644 --- a/uweb3/pagemaker/http_500.html +++ b/uweb3/pagemaker/http_500.html @@ -1,93 +1,94 @@ - - + + - Well, that's embarrassing - + Well, that's embarrassing µWeb3 500 Error. + +

Internal Server Error (HTTP 500)

{{ if [error_for_error] }}

Error page for error page

@@ -97,107 +98,123 @@

Error page for error page

An error occurred on the server during the processing of your request

Here's what we know went wrong, though we still have to figure out why:

{{ endif }} -

[exc:type]

-

[exc:value]

-

Traceback (most recent call first)

-
    - {{ for frame in [exc:traceback] }} -
  1. -
      -
    • File: "[frame:file]"
    • -
    • Scope: [frame:scope]
    • +
+
+
+
+

[exc:type]

+

[exc:value]

+
+

Traceback (most recent call first)

+
    + {{ for frame in [exc:traceback] }}
  1. - - - - {{ for filename, line_no in [frame:source] }} - - {{ endfor }} - -
    Source code
    [filename][line_no]
    +
      +
    • File: "[frame:file]"
    • +
    • Scope: [frame:scope]
    • +
    • + + + + {{ for filename, line_no in [frame:source] }} + + {{ endfor }} + +
      Source code
      [filename][line_no]
      +
    • + {{ if not [error_for_error] }} +
    • + + + + {{ for name, value in [frame:locals|items|sorted] }} + + {{ endfor }} + +
      Frame locals
      [name]{{ if type([value]) == str}}"[value]"{{else}}[value]{{endif}}
      +
    • + {{ endif }} +
  2. - {{ if not [error_for_error] }} -
  3. - - - - {{ for name, value in [frame:locals|items|sorted] }} - - {{ endfor }} - -
    Frame locals
    [name][value]
    -
  4. - {{ endif }} - - - {{ endfor }} -
- {{ if [error_for_error] }} -

Original error (that the error page broke on)

-

[orig_exc:type]

-

[orig_exc:value]

-

Traceback (most recent call first)

-
    - {{ for frame in [orig_exc:traceback] }} -
  1. -
      -
    • File: "[frame:file]"
    • -
    • Scope: [frame:scope]
    • + {{ endfor }} +
+
+ {{ if [error_for_error] }} +
+
+

Original error (that the error page broke on)

+ +

[orig_exc:type]

+

[orig_exc:value]

+
+ +

Traceback (most recent call first)

+
    + {{ for frame in [orig_exc:traceback] }}
  1. - - - - {{ for filename, line_no in [frame:source] }} - - {{ endfor }} - -
    Source code
    [filename][line_no]
    +
      +
    • File: "[frame:file]"
    • +
    • Scope: [frame:scope]
    • +
    • + + + + {{ for filename, line_no in [frame:source] }} + + {{ endfor }} + +
      Source code
      [filename][line_no]
      +
    • +
  2. - - - {{ endfor }} -
- {{ endif }} -

Environment information

- {{ if [cookies] }} - - - - {{ for name, value in [cookies|items|sorted] }} - - {{ endfor }} - -
Cookies
[name][value]
- {{ endif }} - {{ if [query_args] }} - - - - {{ for name, value in [query_args|items|sorted] }} - - {{ endfor }} - -
Query arguments (GET)
[name][value]
- {{ endif }} - {{ if [post_data] }} - - - - {{ for name, value in [post_data|items|sorted] }} - {{ endfor }} - -
POST data
[name][value]
- {{ endif }} - {{ if [environ] }} - - - - {{ for name, value in [environ|items|sorted] }} - - {{ endfor }} - -
Full environment
[name][value]
- {{ endif }} + +
+ {{ endif }} + +
+

Environment information

+ {{ if [cookies] }} + + + + {{ for name, value in [cookies|items|sorted] }} + + {{ endfor }} + +
Cookies
[name][value]
+ {{ endif }} + {{ if [query_args] }} + + + + {{ for name, value in [query_args|items|sorted] }} + + {{ endfor }} + +
Query arguments (GET)
[name][value]
+ {{ endif }} + {{ if [post_data] }} + + + + {{ for name, value in [post_data|items|sorted] }} + + {{ endfor }} + +
POST data
[name][value]
+ {{ endif }} + {{ if [environ] }} + + + + {{ for name, value in [environ|items|sorted] }} + + {{ endfor }} + +
Full environment
[name][value]
+ {{ endif }} +
+
diff --git a/uweb3/pagemaker/login.py b/uweb3/pagemaker/login.py deleted file mode 100644 index bd1f82e0..00000000 --- a/uweb3/pagemaker/login.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/python -"""uWeb3 PageMaker Mixins for login/authentication purposes. - -Contains both the Underdark Login Framework and OpenID implementations -""" - -# Standard modules -import binascii -import hashlib -import os -import base64 - -# Third-party modules -import simplejson - -# Package modules -from uweb3.model import SecureCookie -from . import login_openid -from .. import model -from .. import response - -OPENID_PROVIDERS = {'google': 'https://www.google.com/accounts/o8/id', - 'yahoo': 'http://yahoo.com/', - 'myopenid': 'http://myopenid.com/'} - - -# ############################################################################## -# Record classes for Underdark Login Framework -# -class Challenge(model.Record): - """Abstraction for the `challenge` table.""" - _PRIMARY_KEY = 'user', 'remote' - CHALLENGE_BYTES = 16 - - @classmethod - def ChallengeBytes(cls): - """Returns the configured number of random bytes for a challenge.""" - return os.urandom(cls.CHALLENGE_BYTES) - - @classmethod - def MakeChallenge(cls, connection, remote, user): - """Makes a new, or retrieves an existing challenge for a given IP + user.""" - record = {'remote': remote, 'user': user, 'challenge': cls.ChallengeBytes()} - try: - return super(Challenge, cls).Create(connection, record) - except connection.IntegrityError: - return cls.FromPrimary(connection, (user, remote)) - - -class User(model.Record): - """Abstraction for the `user` table.""" - SALT_BYTES = 8 - - @classmethod - def FromName(cls, connection, username): - """Returns a User object based on the given username.""" - with connection as cursor: - safe_name = connection.EscapeValues(username) - user = cursor.Select( - table=cls.TableName(), - conditions='name=%s' % safe_name) - if not user: - raise cls.NotExistError('No user with name %r' % username) - return cls(connection, user[0]) - - @classmethod - def HashPassword(cls, password, salt=None): - if not salt: - salt = cls.SaltBytes() - if (len(salt) * 3) / 4 - salt.decode('utf-8').count('=', -2) != cls.SALT_BYTES: - raise ValueError('Salt is of incorrect length. Expected %d, got: %d' % ( - cls.SALT_BYTES, len(salt))) - m = hashlib.sha256() - m.update(password.encode("utf-8") + binascii.hexlify(salt)) - password = m.hexdigest() - return { 'password': password, 'salt': salt } - - @classmethod - def SaltBytes(cls): - """Returns the configured number of random bytes for the salt.""" - random_bytes = os.urandom(cls.SALT_BYTES) - return base64.b64encode(random_bytes).decode('utf-8').encode('utf-8') #we do this to cast this byte to utf-8 - - def UpdatePassword(self, plaintext): - """Stores a new password hash and salt, from the given plaintext.""" - self.update(self.HashPassword(plaintext)) - self.Save() - - def VerifyChallenge(self, attempt, challenge): - """Verifies the password hash against the stored hash. - - Both the password hash (attempt) and the challenge should be provided - as raw bytes. - """ - password = binascii.hexlify(self['password']) - actual_pass = hashlib.sha256(password + binascii.hexlify(challenge)).digest() - return attempt == actual_pass - - def VerifyPlaintext(self, plaintext): - """Verifies a given plaintext password.""" - salted = self.HashPassword(plaintext, self['salt'].encode('utf-8'))['password'] - return salted == self['password'] - - -# ############################################################################## -# Actual Pagemaker mixin class -# -class LoginMixin(SecureCookie): - """Provides the Login Framework for uWeb3.""" - ULF_CHALLENGE = Challenge - ULF_USER = User - - def ValidateLogin(self): - user = self.ULF_USER.FromName( - self.connection, self.post.getfirst('username')) - if user.VerifyPlaintext(str(self.post.getfirst('password', ''))): - return self._Login_Success(user) - return self._Login_Failure() - - def _Login_Success(self, user): - """Renders the response to the user upon authentication failure.""" - raise NotImplementedError - - def _ULF_Success(self, secure): - """Renders the response to the user upon authentication success.""" - raise NotImplementedError - - -class OpenIdMixin(object): - """A class that provides rudimentary OpenID authentication. - - At present, it does not support any means of Attribute Exchange (AX) or other - account information requests (sReg). However, it does provide the base - necessities for verifying that whoever logs in is still the same person as the - one that was previously registered. - """ - def _OpenIdInitiate(self, provider=None): - """Verifies the supplied OpenID URL and resolves a login through it.""" - if provider: - try: - openid_url = OPENID_PROVIDERS[provider.lower()] - except KeyError: - return self.OpenIdProviderError('Invalid OpenID provider %r' % provider) - else: - openid_url = self.post.getfirst('openid_provider') - - consumer = login_openid.OpenId(self.req) - # set the realm that we want to ask to user to verify to - trustroot = 'http://%s' % self.req.env['HTTP_HOST'] - # set the return url that handles the validation - returnurl = trustroot + '/OpenIDValidate' - - try: - return consumer.Verify(openid_url, trustroot, returnurl) - except login_openid.InvalidOpenIdUrl as error: - return self.OpenIdProviderBadLink(error) - except login_openid.InvalidOpenIdService as error: - return self.OpenIdProviderError(error) - - def _OpenIdValidate(self): - """Handles the return url that openId uses to send the user to""" - try: - auth_dict = login_openid.OpenId(self.req).doProcess() - except login_openid.VerificationFailed as error: - return self.OpenIdAuthFailure(error) - except login_openid.VerificationCanceled as error: - return self.OpenIdAuthCancel(error) - return self.OpenIdAuthSuccess(auth_dict) - - def OpenIdProviderBadLink(self, err_obj): - """Handles the case where the OpenID provider link is faulty.""" - raise NotImplementedError - - def OpenIdProviderError(self, err_obj): - """Handles the case where the OpenID provider responds out of spec.""" - raise NotImplementedError - - def OpenIdAuthCancel(self, err_obj): - """Handles the case where the client cancels OpenID authentication.""" - raise NotImplementedError - - def OpenIdAuthFailure(self, err_obj): - """Handles the case where the provided authentication is invalid.""" - raise NotImplementedError - - def OpenIdAuthSuccess(self, auth_dict): - """Handles the case where the OpenID authentication was successful. - - Implementers should at the very least override this method as this is where - you will want to mark people as authenticated, either by cookies or sessions - tracked otherwise. - """ - raise NotImplementedError diff --git a/uweb3/pagemaker/login_openid.py b/uweb3/pagemaker/login_openid.py deleted file mode 100644 index fb80bb4a..00000000 --- a/uweb3/pagemaker/login_openid.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/python3 -"""module to support OpenID login in uWeb3""" - -# Standard modules -import base64 -import os -from openid.consumer import consumer -from openid.extensions import pape -from openid.extensions import sreg - -# Package modules -from . import response - - -class Error(Exception): - """An OpenID error has occured""" - - -class InvalidOpenIdUrl(Error): - """The supplied openIDurl is invalid""" - - -class InvalidOpenIdService(Error): - """The supplied openID Service is invalid""" - - -class VerificationFailed(Error): - """The verification for the user failed""" - - -class VerificationCanceled(Error): - """The verification for the user was canceled""" - - -class OpenId(object): - """Provides OpenId verification and processing of return values""" - def __init__(self, request, cookiename='nw_openid'): - """Sets up the openId class - - Arguments: - @ request: request.Request - The request object. - % cookiename: str ~~ 'nw_openid' - The name of the cookie that holds the OpenID session token. - """ - self.request = request - self.session = {'id': None} - self.cookiename = cookiename - - def getConsumer(self): - """Creates a openId consumer class and returns it""" - #XXX(Elmer): What does having a store change? - # As far as I can tell, this does *not* maintain sessions of any sort. - store = None - return consumer.Consumer(self.getSession(), store) - - def getSession(self): - """Return the existing session or a new session""" - if self.session['id'] is not None: - return self.session - - # Get value of cookie header that was sent - try: - self.session['id'] = self.request.vars['cookies'][self.cookiename].value - except KeyError: - # 20 chars long, 120 bits of entropy - self.session['id'] = base64.urlsafe_b64encode(os.urandom(15)) - - return self.session - - def setSessionCookie(self): - """Sets the session cookie on the request object""" - self.request.AddCookie(self.cookiename, self.session['id']) - - def Verify(self, openid_url, trustroot, returnurl): - """ - Takes the openIdUrl from the user and sets up the request to send the user - to the correct page that will validate our trustroot to receive the data. - - Arguments: - @ openid_url: str - The supplied URL where the OpenID provider lives. - @ trustroot: str - The url of our webservice, will be displayed to the user as th - consuming url - @ returnurl: str - The url that will handle the Process step for the user being returned - to us by the openId supplier - """ - oidconsumer = self.getConsumer() - if openid_url.strip() == '': - raise InvalidOpenIdService() - try: - request = oidconsumer.begin(openid_url) - except consumer.DiscoveryFailure: - raise InvalidOpenIdUrl(openid_url) - if not request: - raise InvalidOpenIdService() - if request.shouldSendRedirect(): - redirect_url = request.redirectURL(trustroot, returnurl) - return response.Redirect(redirect_url) - else: - return request.htmlMarkup(trustroot, returnurl, - form_tag_attrs={'id': 'openid_message'}) - - def doProcess(self): - """Handle the redirect from the OpenID server. - - Returns: - tuple: userId - requested fields - phishing resistant info - canonical user ID - - Raises: - VerificationCanceled if the user canceled the verification - VerificationFailed if the verification failed - """ - oidconsumer = self.getConsumer() - - # Ask the library to check the response that the server sent - # us. Status is a code indicating the response type. info is - # either None or a string containing more information about - # the return type. - url = 'http://%s%s' % ( - self.request.env['HTTP_HOST'], self.request.env['PATH_INFO']) - query_args = dict((key, value[0]) for key, value - in self.request.vars['get'].items()) - info = oidconsumer.complete(query_args, url) - - sreg_resp = None - pape_resp = None - display_identifier = info.getDisplayIdentifier() - - if info.status == consumer.FAILURE and display_identifier: - # In the case of failure, if info is non-None, it is the - # URL that we were verifying. We include it in the error - # message to help the user figure out what happened. - raise VerificationFailed('Verification of %s failed: %s' % ( - display_identifier, info.message)) - - elif info.status == consumer.SUCCESS: - # Success means that the transaction completed without - # error. If info is None, it means that the user cancelled - # the verification. - - # This is a successful verification attempt. If this - # was a real application, we would do our login, - # comment posting, etc. here. - sreg_resp = sreg.SRegResponse.fromSuccessResponse(info) - pape_resp = pape.Response.fromSuccessResponse(info) - # You should authorize i-name users by their canonicalID, - # rather than their more human-friendly identifiers. That - # way their account with you is not compromised if their - # i-name registration expires and is bought by someone else. - return {'ident': display_identifier, - 'sreg': sreg_resp, - 'pape': pape_resp, - 'canonicalID': info.endpoint.canonicalID} - - elif info.status == consumer.CANCEL: - # cancelled - raise VerificationCanceled('Verification canceled') - - elif info.status == consumer.SETUP_NEEDED: - if info.setup_url: - message = 'Setup needed' % info.setup_url - else: - # This means auth didn't succeed, but you're welcome to try - # non-immediate mode. - message = 'Setup needed' - raise VerificationFailed(message) - else: - # Either we don't understand the code or there is no - # openid_url included with the error. Give a generic - # failure message. The library should supply debug - # information in a log. - raise VerificationFailed('Verification failed.') diff --git a/uweb3/pagemaker/new_decorators.py b/uweb3/pagemaker/new_decorators.py deleted file mode 100644 index b00d36ed..00000000 --- a/uweb3/pagemaker/new_decorators.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import time -import datetime -import hashlib - -class XSRF(object): - # secret = str(os.urandom(64)) - secret = "test" - def __init__(self, AddCookie, post): - """Checks if cookie with xsrf key is present. - - If not generates xsrf token and places it in a cookie. - Checks if xsrf token in post is equal to the one in the cookie and returns - True when they do not match and False when they do match for the 'incorrect_xsrf_token' flag. - """ - self.unix_timestamp = time.mktime(datetime.datetime.now().date().timetuple()) - self.AddCookie = AddCookie - self.post = post - - def is_valid_xsrf_token(self, userid): - """Validate given xsrf token based on userid - - Arguments: - @ userid: str/int - - Returns: - IsValid: boolean - """ - token = self.Generate_xsrf_token(userid) - if not self.post.get('xsrf'): - return False - if self.post.get('xsrf') != token: - return False - return True - - def Generate_xsrf_token(self, userid): - hashed = (str(self.unix_timestamp) + self.secret + userid).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return h.hexdigest() - -def loggedin(f): - """Decorator that checks if the user requesting the page is logged in based on set cookie.""" - def wrapper(*args, **kwargs): - if not args[0].user: - return args[0].req.Redirect('/login', http_code=303) - return f(*args, **kwargs) - return wrapper - -def checkxsrf(f): - """Decorator that checks the user's XSRF. - - The function will compare the XSRF in the user's cookie and in the - (post) request. Make sure to have xsrf_enabled = True in the config.ini - """ - def wrapper(*args, **kwargs): - xsrf_cookie = args[0].cookies.get('xsrf') - xsrf = XSRF(args[0].req.AddCookie, args[0].post) - if args[0].req.method == "GET": - if not xsrf_cookie: - #If the cookie doesn't exist generate a token and add it in a cookie - args[0].xsrf = xsrf.Generate_xsrf_token(args[0].user.get('user_id')) - args[0].req.AddCookie('xsrf', args[0].xsrf) - else: - #If the cookie exists but the xsrf is not valid replace the cookie with a valid one - if not xsrf.is_valid_xsrf_token(args[0].user.get('user_id')): - args[0].xsrf = xsrf.Generate_xsrf_token(args[0].user.get('user_id')) - args[0].req.AddCookie('xsrf', args[0].xsrf) - else: - args[0].xsrf = xsrf_cookie - else: - #On a post request check if there is a cookie with xsrf and if the post contains an xsrf input - if not xsrf_cookie: - return args[0].XSRFInvalidToken('XSRF cookie is missing') - if not args[0].post.get('xsrf'): - args[0].post = {} - return args[0].XSRFInvalidToken('XSRF token is missing') - #Validate token - if not xsrf.is_valid_xsrf_token(args[0].user.get('user_id')): - return args[0].XSRFInvalidToken('XSRF token is not valid') - args[0].xsrf = xsrf_cookie - return f(*args, **kwargs) - return wrapper \ No newline at end of file diff --git a/uweb3/pagemaker/new_login.py b/uweb3/pagemaker/new_login.py deleted file mode 100644 index 8ac543f7..00000000 --- a/uweb3/pagemaker/new_login.py +++ /dev/null @@ -1,148 +0,0 @@ -import hashlib - -import bcrypt - -from .. import model - -class UserCookieInvalidError(Exception): - """Superclass for errors returned by the user class.""" - -class Test(model.SettingsManager): - """ """ - -class UserCookie(model.SecureCookie): - """ """ - -class Users(model.Record): - """ """ - salt = "SomeSaltyBoi" - cookie_salt = "SomeSaltyCookie" - - UserCookieInvalidError = UserCookieInvalidError - - @classmethod - def CreateNew(cls, connection, user): - """Creates new user if not existing - - Arguments: - @ connection: sqltalk database connection object - @ user: dict. username and password keys are required. - Returns: - ValueError: if username/password are not set - AlreadyExistsError: if username already in database - Users: Users object when user is created - """ - if not user.get('username'): - raise ValueError('Username required') - if not user.get('password'): - raise ValueError('Password required') - - try: - cls.FromName(connection, user.get('username')) - return cls.AlreadyExistError("User with name '{}' already exists".format(user.get('username'))) - except cls.NotExistError: - user['password'] = cls.__HashPassword(user.get('password')).decode('utf-8') - return cls.Create(connection, user) - - @classmethod - def FromName(cls, connection, username): - """Select a user from the database based on name - Arguments: - @ username: str - Returns: - NotExistError: raised when no user with given username found - Users: Users object with the connection and all relevant user data - """ - from sqlalchemy import Table, MetaData, Column, Integer, String, text - meta = MetaData() - users_table = Table('users', meta, - Column('id', Integer, primary_key=True), - Column('username', String(255)), - Column('password', String(255)), - ) - # result = connection.execute(users_table.select()) - # statement = text("SELECT * FROM users WHERE username = :name") - # user = connection.execute(statement, {'name': username}).fetchone() - with connection as cursor: - safe_name = connection.EscapeValues(username) - user = cursor.Select( - table='users', - conditions='username={}'.format(safe_name)) - if not user: - raise cls.NotExistError('No user with name {}'.format(username)) - return cls(connection, user[0]) - - - @classmethod - def __HashPassword(cls, password): - """Hash password with bcrypt""" - password = password + cls.salt - - return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) - - @classmethod - def ComparePassword(cls, password, hashed): - """Check if passwords match - - Arguments: - @ password: str - @ hashed: str password hash from users database table - Returns: - Boolean: True if match False if not - """ - if not isinstance(hashed, bytes): - hashed = hashed.encode('utf-8') - password = password + cls.salt - return bcrypt.checkpw(password.encode('utf-8'), hashed) - - @classmethod - def CreateValidationCookieHash(cls, data): - """Takes a non nested dictionary and turns it into a secure cookie. - - Required: - @ id: str/int - Returns: - A string that is ready to be placed in a cookie. Hash and data are seperated by a + - """ - if not data.get('id'): - raise ValueError("id is required") - - cookie_dict = {} - string_to_hash = "" - for key in data.keys(): - if not isinstance(data[key], (str, int)): - raise ValueError('{} must be of type str or int'.format(data[key])) - value = str(data[key]) - string_to_hash += value - cookie_dict[key] = value - - hashed = (string_to_hash + cls.cookie_salt).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return '{}+{}'.format(h.hexdigest(), cookie_dict) - - @classmethod - def ValidateUserCookie(cls, cookie): - """Takes a cookie and validates it - Arguments - @ str: A hashed cookie from the `CreateValidationCookieHash` method - """ - from ast import literal_eval - if not cookie: - return None - - try: - data = cookie.rsplit('+', 1)[1] - data = literal_eval(data) - except Exception: - raise cls.UserCookieInvalidError("Invalid cookie") - - user_id = data.get('id', None) - if not user_id: - raise cls.UserCookieInvalidError("Could not get id from cookie") - - if cookie != cls.CreateValidationCookieHash(data): - raise cls.UserCookieInvalidError("Invalid cookie") - - return user_id - \ No newline at end of file diff --git a/uweb3/pagemaker/session.py b/uweb3/pagemaker/session.py deleted file mode 100644 index 679c6c74..00000000 --- a/uweb3/pagemaker/session.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/python -"""uWeb3 PageMaker Mixins for session management purposes.""" - -# Standard modules -import binascii -import os -import datetime -import pytz - -# Package modules -from .. import model - -# ############################################################################## -# Record classes for session management -# -# Model class have many methods. -# pylint:disable=R0904 - -class Session(model.Record): - """Abstraction for the `session` table""" - - _PRIMARY_KEY = 'session' -# pylint:enable=R0904 - -# ############################################################################## -# Pagemaker Mixin class for session management -# -class SessionMixin(object): - """Provides session management for uWeb3""" - - class NoSessionError(Exception): - """Custom exception for user not having a (unexpired) session cookie.""" - - class SecurityError(Exception): - """Custom exception raised for not passing security constraints set on the - session.""" - - class XsrfError(Exception): - """Custom exception raised in case of a detected XSRF attack.""" - - SESSION_TABLE = Session - - def _ULF_DeleteSession(self, cookie_name): - """Destroys a user session with `cookie_name`. Used for logging out.""" - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - try: - binsessid = binascii.unhexlify(sessionid) - self.SESSION_TABLE.DeletePrimary(self.connection, binsessid) - # Set a junk cookie that expires in 1 second. - self.req.AddCookie(cookie_name, 'deleted', path='/', max_age=1, - httponly=True) - except model.NotExistError: - raise self.NoSessionError( - 'There is no session associated with ID %s' % sessionid) - - def _ULF_CheckXsrf(self, cookie_name, field_name="xsrf"): - """Checks if the cookie named `cookie_name` matches the field `field_name` - - Used to check if an XSRF is happening. Returns `True` if an XSRF is - detected. - """ - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - if self.req.env['REQUEST_METHOD'] == "POST": - if sessionid != self.post.getfirst(field_name): - raise self.XsrfError("An XSRF attack was detected for this request.") - else: - if sessionid != self.get.getfirst(field_name): - raise self.XsrfError("An XSRF attack was detected for this request.") - - def _ULF_GetSessionId(self, cookie_name): - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - return sessionid - - def _ULF_GetSession(self, cookie_name): - """Fetches a user ID associated with a session ID set on `cookie_name`.""" - remote = self.req.env['REMOTE_ADDR'] # Get remote IP of user. - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - try: - binsessid = binascii.unhexlify(sessionid) - session = Session.FromPrimary(self.connection, binsessid) - remote = self.req.env['REMOTE_ADDR'] - if (session['expiry'] < datetime.datetime.now(pytz.UTC)): - raise self.NoSessionError('The user session has expired.') - if (session['iplocked'] and remote != session['remote']): - raise self.SecurityError('This session is locked to another IP.') - user = session['user'] - except model.NotExistError: - raise self.NoSessionError( - 'There is no session associated with ID %s' % sessionid) - return user - - def _ULF_SetSession(self, cookie_name, uid, expiry=86400, locktoip=True): - """Sets a user ID to `uid` on cookie `cookie_name`, gives a new cookie to - the user with this cookie name. - - Takes an optional `expiry` argument which defaults to 86400 seconds. - Also takes an optional `locktoip` argument which defaults to True -- - which causes the session to be locked to the user's IP""" - random_id = os.urandom(16) - # The random ID needs to be converted to hex for the cookie. - self.req.AddCookie(cookie_name, binascii.hexlify(random_id), path='/', - max_age=expiry, httponly=True) - now = datetime.datetime.utcnow() - expirationdate = now + datetime.timedelta(seconds=expiry) - self.SESSION_TABLE.Create(self.connection, { - 'session': random_id, 'user': uid, - 'remote': self.req.env['REMOTE_ADDR'], 'expiry': '2021-02-18 11:15:45', - 'iplocked': int(locktoip)}) diff --git a/uweb3/pagemaker/sparserenderer.js b/uweb3/pagemaker/sparserenderer.js new file mode 100644 index 00000000..0db5a46a --- /dev/null +++ b/uweb3/pagemaker/sparserenderer.js @@ -0,0 +1,131 @@ +"use strict" +var uweb_sparserenderer = window.uweb_sparserenderer || { + verbose: true, + templatecache: true, + templates: {}, + HandlePageLoad: function(event){ + if (this.verbose){ + console.log('Attaching event handlers to GET/POST actions for: ', window.location); + } + document.querySelectorAll('a').forEach(function(link) { + if(link.href){ + link.addEventListener('click', this.HandleClickEvent.bind(this)); + } + }, this); + document.querySelectorAll('form').forEach(function(link) { + link.addEventListener('submit', this.HandleSubmitEvent.bind(this)); + }, this); + }, + + HandleHistoryPop: function(event){ + if (this.verbose){ + console.log('History event triggered: ', event.state); + } + this.DoClick(event.state); + }, + + HandleClickEvent: function(event){ + if (this.verbose){ + console.log('User Link event registered on: ', event.target.href); + } + event.preventDefault(); + if(event.target.href){ // ignore links without + this.DoClick(event.target.href); + } + return false; + }, + + DoClick: async function (path){ + // Fetches the next URL from uweb while telling it we can do the parsing of the template locally. + let error; + let response = await fetch(path, { + headers: {'Accept': 'application/json'} + }) + .then(response => response.json()) + .then(result => {this.HandleUrlResponse(result, path)}) + .catch((error) => { + console.error('Error:', error); + }); + }, + + HandleSubmitEvent: function(event){ + if (this.verbose){ + console.log('User Form event registered on: ', event.target.action); + } + event.preventDefault(); + if(event.target.action){ + this.DoSubmit(event.target); + } + return false; + }, + + DoSubmit: async function (form){ + // Fetches the next URL from uweb while telling it we can do the parsing of the template locally. + let error; + const formData = new FormData(form); + const data = [...formData.entries()]; + console.log(formData, data); + const PostString = data + .map(x => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`) + .join('&'); + if (this.verbose){ + console.log('User Form submit data: ', PostString); + } + if (new Array('head', 'get').indexOf(form.method.toLowerCase()) != -1){ + return this.DoClick(form.action + '?' + PostString); + } + let response = await fetch(form.action, { + method: form.method, + headers: {'Accept': 'application/json'}, + body: PostString + }) + .then(response => response.json()) + .then(result => {this.HandleUrlResponse(result, form.action)}) + .catch((error) => { + console.error('Error:', error); + }); + }, + + HandleUrlResponse: async function (response, path){ + window.history.pushState(path, "", path); + let template = await this.GetTemplate(response['template'], response['template_hash']); + Object.keys(response['replacements']).forEach(key => { + template = template.replaceAll(key, response['replacements'][key]); + } + ); + + document.body.innerHTML = template; + // re-init all handlers + this.HandlePageLoad(); + return template; + }, + + GetTemplate: async function (path, hash){ + let url = '/template/'+hash+path; + let error; + if (this.templatecache > 0 && + this.templates[path]){ + if (this.verbose){ + console.log('Cached template hit for '+path+' Saved '+this.templates[path].length+' bytes'); + } + return this.templates[path]; + } + return fetch(url)//, + //{integrity: "sha256-"+hash}) + .then(response => response.text()) + .then(data => { + this.templates[path] = data; + return data; + }) + .catch((error) => { + console.error('Error:', error); + }); + }, + + Load: function(){ + window.addEventListener('load', this.HandlePageLoad.bind(this)); + window.addEventListener('popstate', this.HandleHistoryPop.bind(this)); + } + + +}.Load(); diff --git a/uweb3/request.py b/uweb3/request.py index 17711327..d61c704e 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -1,30 +1,28 @@ -#!/usr/bin/python2.6 -"""uWeb3 request module.""" +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +"""µWeb3 request module.""" # Standard modules import cgi import sys import urllib -from cgi import parse_qs -try: - # python 2 - import cStringIO as stringIO - import Cookie as cookie -except ImportError: - # python 3 - import io as stringIO - import http.cookies as cookie +import io +from urllib.parse import parse_qs, parse_qsl +import io as stringIO +import http.cookies as cookie import re import json + # uWeb modules from . import response -from werkzeug.formparser import parse_form_data -from werkzeug.datastructures import MultiDict +MAX_COOKIE_LENGTH = 4096 +MAX_REQUEST_BODY_SIZE = 20000000 #20MB -class CookieToBigError(Exception): +class CookieTooBigError(Exception): """Error class for cookie when size is bigger than 4096 bytes""" + class Cookie(cookie.SimpleCookie): """Cookie class that uses the most specific value for a cookie name. @@ -53,64 +51,96 @@ def _BaseCookie__set(self, key, real_value, coded_value): dict.__setitem__(self, key, morsel) -class PostDictionary(MultiDict): - """ """ - #TODO: Add basic uweb functions - - def getfirst(self, key, default=None): - """Returns the first item out of the list from the given key - - Arguments: - @ key: str - % default: any - """ - items = dict(self.lists()) - try: - return items[key][0] - except KeyError: - return default - - def getlist(self, key): - """Returns a list with all values that were given for the requested key. - - N.B. If the given key does not exist, an empty list is returned. - """ - items = dict(self.lists()) - try: - return items[key] - except KeyError: - return [] - -class Request(object): - def __init__(self, env, registry): +class Request: + def __init__(self, env, logger, errorlogger): self.env = env self.headers = dict(self.headers_from_env(env)) - self.registry = registry self._out_headers = [] self._out_status = 200 self._response = None + self.charset = "utf-8" self.method = self.env['REQUEST_METHOD'] - # `self.vars` setup, will contain keys 'cookie', 'get' and 'post' - self.vars = {'cookie': dict((name, value.value) for name, value in - Cookie(self.env.get('HTTP_COOKIE')).items()), - 'get': PostDictionary(cgi.parse_qs(self.env.get('QUERY_STRING'))), - 'post': PostDictionary()} - self.env['host'] = self.headers.get('Host', '') - - if self.method == 'POST': - stream, form, files = parse_form_data(self.env) - if self.env['CONTENT_TYPE'] == 'application/json': + self.vars = { + 'cookie': { + name: value.value + for name, value in Cookie(self.env.get('HTTP_COOKIE')).items() + }, + 'get': QueryArgsDict(parse_qs(self.env['QUERY_STRING'])), + } + self.env['host'] = self.headers.get('Host', '').strip().lower() + self.logger = logger + self.errorlogger = errorlogger + self.noparse = self.headers.get('accept', '').lower() == 'application/json' + + if self.method in ('POST', 'PUT', 'DELETE'): + request_body_size = 0 + try: + request_body_size = int(self.env.get('CONTENT_LENGTH', 0)) + except Exception: + pass + request_payload = self.env['wsgi.input'].read(min(request_body_size, MAX_REQUEST_BODY_SIZE)) + self.input = request_payload + self.env['mimetype'] = self.env.get('CONTENT_TYPE', '').split(';')[0] + + if self.env['mimetype'] == 'application/json': try: - request_body_size = int(self.env.get('CONTENT_LENGTH', 0)) - except (ValueError): - request_body_size = 0 - request_body = self.env['wsgi.input'].read(request_body_size) - data = json.loads(request_body) - self.vars['post'] = PostDictionary(MultiDict(data)) + self.vars[self.method.lower()] = json.loads(request_payload) + except (json.JSONDecodeError, ValueError): + pass + elif self.env['mimetype'] == 'multipart/form-data': + boundary = self.env.get('CONTENT_TYPE', '').split(';')[1].strip().split('=')[1] + request_payload = request_payload.split(b'--%s' % boundary.encode(self.charset)) + self.vars['files'] = {} + fields = [] + for item in request_payload: + item = item.lstrip() + if item.startswith(b'Content-Disposition: form-data'): + nl = 0 + prevnl = 0 + itemlength = len(item) + name = filename = ContentType = charset = None + while nl < itemlength: + nl = item.index(b"\n", prevnl+len(b"\n")) + header = item[prevnl:nl] + prevnl = nl + if not header.strip(): + content = item[nl:].strip() + break + directives = header.strip().split(b';') + for directive in directives: + directive = directive.lstrip() + if directive.startswith(b'name='): + name = directive.split(b'=', 1)[1][1:-1].decode(self.charset) + if name == '_charset_': # default charset default case + self.charset = item[nl:].strip() + break + if directive.startswith(b'filename='): + filename = directive.split(b'=', 1)[1][1:-1].decode(self.charset) + if directive.startswith(b'Content-Type='): + ContentType = directive.split(b'=', 1)[1].decode(self.charset).split(";") + if len(ContentType) > 1: + if ContentType[1].startswith('charset'): + charset = ContentType[1].split('=')[1] + if ContentType[0].startswith('content-type'): + contenttype = ContentType[0].split(':')[1].strip() + if charset: + content = content.decode(charset) + elif not ContentType: + try: + content = content.decode(charset or self.charset) + except: + pass + if filename: + self.vars['files'][name] = {'filename': filename, + 'ContentType': ContentType, + 'content': content} + else: + fields.append('%s=%s' % (name, content)) + self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO('&'.join(fields)), + environ={'REQUEST_METHOD': 'POST'}) else: - self.vars['post'] = PostDictionary(form) - for f in files: - self.vars['post'][f] = files.get(f) + self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO(request_payload.decode(self.charset)), + environ={'REQUEST_METHOD': 'POST'}) @property def path(self): @@ -119,13 +149,13 @@ def path(self): @property def response(self): if self._response is None: - self._response = response.Response() + self._response = response.Response(headers=self._out_headers) return self._response - def Redirect(self, location, http_code=307): + def Redirect(self, location, httpcode=307): REDIRECT_PAGE = ('Page moved' - 'Page moved, please follow this link' - '').format(location) + 'Page moved, please follow this link' + '').format(location) headers = {'Location': location} if self.response.headers.get('Set-Cookie'): @@ -133,7 +163,7 @@ def Redirect(self, location, http_code=307): return response.Response( content=REDIRECT_PAGE, content_type=self.response.headers.get('Content-Type', 'text/html'), - httpcode=http_code, + httpcode=httpcode, headers=headers ) @@ -172,14 +202,18 @@ def AddCookie(self, key, value, **attrs): When True, the cookie is only used for http(s) requests, and is not accessible through Javascript (DOM). """ - if isinstance(value, (str)): - if len(value.encode('utf-8')) >= 4096: - raise CookieToBigError("Cookie is larger than 4096 bytes and wont be set") + if isinstance(value, (str)) and len(value.encode('utf-8')) >= MAX_COOKIE_LENGTH: + raise CookieTooBigError("Cookie is larger than %d bytes and wont be set" % MAX_COOKIE_LENGTH) new_cookie = Cookie({key: value}) if 'max_age' in attrs: attrs['max-age'] = attrs.pop('max_age') new_cookie[key].update(attrs) + if 'samesite' not in attrs and 'secure' not in attrs: + try: # only supported from python 3.8 and up + attrs['samesite'] = 'Lax' # set default to LAX for no secure (eg, local) sessions. + except http.cookies.CookieError: + pass self.AddHeader('Set-Cookie', new_cookie[key].OutputString()) def AddHeader(self, name, value): @@ -188,6 +222,8 @@ def AddHeader(self, name, value): self.response.headers['Set-Cookie'] = [value] return self.response.headers['Set-Cookie'].append(value) + return + self.response.AddHeader(name, value) def DeleteCookie(self, name): """Deletes cookie by name @@ -221,9 +257,9 @@ def items(self): def read_urlencoded(self): indexed = {} self.list = [] - for field, value in cgi.parse_qsl(self.fp.read(self.length), - self.keep_blank_values, - self.strict_parsing): + for field, value in parse_qsl(self.fp.read(self.length), + self.keep_blank_values, + self.strict_parsing): if self.FIELD_AS_ARRAY.match(str(field)): field_group, field_key = self.FIELD_AS_ARRAY.match(field).groups() indexed.setdefault(field_group, cgi.MiniFieldStorage(field_group, {})) @@ -233,38 +269,41 @@ def read_urlencoded(self): self.list = list(indexed.values()) + self.list self.skip_lines() + def __repr__(self): + return "{%s}" % ','.join("'%s': '%s'" % (k, v if len(v) > 1 else v[0]) for k, v in self.iteritems()) -class CustomByteLikeObject(object): - def __init__(self, data): - self.data = data - - def read(self, length=None): - if length: - return self.data[0:length] - else: - return self.data + @property + def __dict__(self): + return { + key: value if len(value) > 1 else value[0] + for key, value in self.iteritems() + } - def readline(self, *args): - return self.data -def ParseForm(file_handle, environ, json=False): - """Returns an IndexedFieldStorage object from the POST data and environment. +class QueryArgsDict(dict): + def getfirst(self, key, default=None): + """Returns the first value for the requested key, or a fallback value.""" + try: + return self[key][0] + except KeyError: + return default - This small wrapper is necessary because cgi.FieldStorage assumes that the - provided file handles supports .readline() iteration. File handles as provided - by BaseHTTPServer do not support this, so we need to convert them to proper - stringIO objects first. - """ - #TODO see if we need to encode in utf8 or is ascii is fine based on the headers - # print(file_handle.read(int(environ['CONTENT_LENGTH'])).decode('ascii')) - # data = sys.stdin.read() - if json: - #We already decoded the JSON and turned into a urlquerystring - environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' - files = CustomByteLikeObject(file_handle.encode()) - else: - files = CustomByteLikeObject(file_handle.read(int(environ['CONTENT_LENGTH']))) + def getlist(self, key): + """Returns a list with all values that were given for the requested key. - return IndexedFieldStorage(fp=files, environ=environ, keep_blank_values=1) + N.B. If the given key does not exist, an empty list is returned. + """ + try: + return self[key] + except KeyError: + return [] +def return_real_remote_addr(env): + """Returns the remote ip-address, + if there is a proxy involved it will take the last IP addres from the HTTP_X_FORWARDED_FOR list + """ + try: + return env['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip() + except KeyError: + return env['REMOTE_ADDR'] diff --git a/uweb3/response.py b/uweb3/response.py index 306f15ef..568e99ff 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """uWeb3 response classes.""" # Standard modules @@ -7,8 +7,6 @@ except ImportError: import http.client as httplib -from collections import defaultdict - class Response(object): """Defines a full HTTP response. @@ -20,64 +18,96 @@ class Response(object): def __init__(self, content='', content_type=CONTENT_TYPE, httpcode=200, headers=None, **kwds): - """Initializes a Page object. + """Initializes a Response object. Arguments: @ content: str The content to return to the client. This can be either plain text, html or the contents of a file (images for example). % content_type: str ~~ CONTENT_TYPE ('text/html' by default) - The content type of the response. This should NOT be set in headers. + The Content-Type of the response. This should NOT be set in headers. % httpcode: int ~~ 200 The HTTP response code to attach to the response. % headers: dict ~~ None A dictionary with header names and their associated values. """ - self.charset = kwds.get('charset', 'utf8') - self.text = content + self.charset = kwds.get('charset', 'utf-8') + self.content = content self.httpcode = httpcode + self.log = None self.headers = headers or {} - if ';' not in content_type: + if (';' not in content_type and + (content_type.startswith('text/') or + content_type.startswith('application/json'))): content_type = '{!s}; charset={!s}'.format(content_type, self.charset) self.content_type = content_type # Get and set content-type header @property def content_type(self): - return self.headers['Content-Type'] + """Returns the current Content-Type or None if not set""" + return self.headers.get('Content-Type', None) @content_type.setter def content_type(self, content_type): + """Sets the Content-Type of the response + + Arguments: + @ content_type: str ~~ CONTENT_TYPE + The content type of the response. + """ current = self.headers.get('Content-Type', '') if ';' in current: - content_type = '{!s}; {!s}'.format(content_type, current.split(';', 1)[-1]) + content_type = '{!s}; {!s}'.format(content_type, + current.split(';', 1)[-1]) self.headers['Content-Type'] = content_type + def clean_content_type(self): + """Returns the Content-Type, cleaned from any characters set information.""" + if ';' not in self.headers['Content-Type']: + return self.headers['Content-Type'] + return self.headers['Content-Type'].split(';')[0] + # Get and set body text @property def text(self): + """Returns the content of this response""" return self.content @text.setter def text(self, content): - self.content = str(content) + """Sets the content of this response. + + Arguments: + @ content: str + The content to return to the client. This can be either plain text, html + or the contents of a file (images for example). + """ + self.content = content # Retrieve a header list @property def headerlist(self): + """Returns the current headers as a list of tuples + + each tuple contains the header key, and its value. + """ tuple_list = [] for key, val in self.headers.items(): if key == 'Set-Cookie': for cookie in val: - tuple_list.append((key, cookie.encode('ascii', 'ignore').decode('ascii'))) + tuple_list.append( + (key, cookie.encode('ascii', 'ignore').decode('ascii')) + ) continue if not isinstance(val, str): val = str(val) tuple_list.append((key, val.encode('ascii', 'ignore').decode('ascii'))) return tuple_list - + @property def status(self): + """Returns the current http status code for this response.""" if not self.httpcode: return '%d %s' % (500, httplib.responses[500]) return '%d %s' % (self.httpcode, httplib.responses[self.httpcode]) @@ -88,13 +118,20 @@ def __repr__(self): def __str__(self): return self.content + def SetHeaders(self, headers): + """Instantly set all headers for this Response """ + self.headers = headers + + def AddHeader(self, header, value): + """Adds a header to this response's output list""" + self.headers[header] = value + class Redirect(Response): """A response tailored to do redirects.""" REDIRECT_PAGE = ('Page moved' 'Page moved, please follow this link' '') - #TODO make sure we inject cookies set on the previous response by copying any Set-Cookie headers from them into these headers. def __init__(self, location, httpcode=307): super(Redirect, self).__init__( content=self.REDIRECT_PAGE % location, diff --git a/uweb3/scaffold/access_logging.log b/uweb3/scaffold/access_logging.log deleted file mode 100644 index b43e3662..00000000 --- a/uweb3/scaffold/access_logging.log +++ /dev/null @@ -1,983 +0,0 @@ -127.0.0.1 - - [21/04/2020 10:43:34] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:23] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:26] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:29] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:13:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:15:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:21:53] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:34:31] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:34:33] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:34:34] "POST /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:35:41] "POST /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:35:50] "POST /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:36] "POST /login 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:36] "GET /home 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:49] "GET /home 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:49] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:50] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:38:37] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:38:38] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:38:39] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:00] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:20] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:40:35] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:40:41] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:40:41] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:16] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:18] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:19] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:33] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:43:41] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:19] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:25] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:28] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:57] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:46:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:46:21] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:46:29] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:47:08] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:49:04] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:31:30] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:32:31] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:32:37] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:32:43] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:33:00] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:33:27] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:34:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:34:58] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:38:06] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:42:10] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:24] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:24] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:26] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:40] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:57] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:45:45] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:45:50] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:46:49] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:48:45] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:48:45] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:49:39] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:50:35] "POST /login 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:50:35] "GET /home 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:51:31] "GET /home 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:51:32] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:52:00] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:52:21] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:52:59] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:53:12] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:53:36] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:53:49] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:03] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:09] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:17] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:22] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:28] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:56] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:03] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:04] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:05] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:06] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:17] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:19] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:20] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:21] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:33] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:35] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:33] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:35] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:51] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:55] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:56] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:58:07] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:58:11] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:58:16] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:59:20] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:01:03] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:01:17] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:02:01] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:02:02] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:04:22] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:04:22] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:18] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:42] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:02] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:21] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:24] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:07:43] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:06] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:19] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:36] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:38] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:55] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:09:59] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:42] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:53] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:55] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:12:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:12:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:18] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:04] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:20] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:22] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:55] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:12] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:38] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:47] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:37] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:48] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:17:40] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:17:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:15] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:37] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:37] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:20:15] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:13] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:15] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:32] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:35] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:21] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:29] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:54] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:23:03] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:23:37] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:23:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:03] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:07] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:31] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:32] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:53] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:26:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:26:50] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:26:54] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:36:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:19] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:20] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:24] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:38:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:40:44] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:40:44] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:15:43] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:34:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:34:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:34:52] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:35:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:35:45] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:35:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:11] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:11] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:28] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:31] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:25] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:25] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:26] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 11:15:18] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:56] "GET /static 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:58] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:18:26] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/scripts/ajax.js 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/scripts/uweb-dynamic.js 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/scripts/uweb3-template-parser.js 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:33] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:04] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:07] "GET /static 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:13] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:13] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:14] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:25:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:28:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:28:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:08] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:09] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:09] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:09] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:10] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:11] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:12] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:14] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:19] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:19] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:21] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:23] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:26] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:30] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:31] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:32] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:33] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:34] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:37] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:41] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:46] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:51] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:56] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:01] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:06] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:11] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:16] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:21] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:26] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:30] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:30] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:32] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:32] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:33] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:35] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:39] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:44] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:49] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:54] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:00] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:06] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:12] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:18] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:24] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:30] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:36] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:40] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:40] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:41] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:36:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:37:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:37:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:36] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:37] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:38] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:52] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:54] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:55] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:01:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:02:13] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:02:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:02:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:03:29] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:14:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:57] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:58] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:58] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:58] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:58:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:59:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:59:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:44] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:02:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:09:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:09:59] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:11:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:11:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:12:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:50] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:34:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:34:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:03] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:53] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:53] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:56] "GET /home/kappa 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:58] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:24:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:24:49] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:24:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:25:04] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:25:05] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:27:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:27:01] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:27:03] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:42] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:44] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:45] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:31] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:31] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:32] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:33] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:33] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:37:16] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:37:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:44:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:43] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:43] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:43] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:44] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:46] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:57] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:44:59] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:44:59] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:45:00] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:45:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:45:02] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:47:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 10:17:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:00:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:00:17] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:03:27] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:03:27] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:30] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:30] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:31] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:34] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:35] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:36] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:52] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:23:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:13] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:13] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:25] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:26] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:49] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:06] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:34:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:02] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:03] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:29] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:37:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:37:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:11] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:12] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:14] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:15] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:30] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:32] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:40:41] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:40:43] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:12] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:50] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:52] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:42:06] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:42:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:42:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:20:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:20:46] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:20:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:22] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:24] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:34] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:35] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:35] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:39] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:39] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:39] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:40] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:59] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:29:02] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:56:53] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:01] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:50] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:13] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:13] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 12:02:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 12:03:55] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:57] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:57] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:27] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:28] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:20] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:20] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:21] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:22] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:24] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:09:36] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:20:36] "GET /test/escaping 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:26:52] "GET /test/escaping 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:29:14] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:29:32] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:57:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:58:07] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:58:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:01:46] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:02:15] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:02:34] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:02:55] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:03:01] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:04:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:23] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:24] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:50] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:51] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:15] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:18] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:18] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:19] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:29] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:18:17] "GET /login/q 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:18:18] "GET /login/q 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:18:30] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:20:46] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:20] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:22] "GET /login/ 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:49] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:56] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:22:08] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:03] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:03] "GET /favicon.ico 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:04] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:06] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:06] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:16] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:19] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:26] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:26] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:28] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:31] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:35] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:38] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:38] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:39] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:50] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:50] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:54] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:54] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:56] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:57] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:57] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:58] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:58] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:59] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:03] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:38] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:53] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:28:59] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:29:16] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:29:46] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:09] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:13] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:20] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:40] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:00] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:09] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:21] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:29] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:30] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:11] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:11] "GET /favicon.ico 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:43] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:43] "GET /favicon.ico 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:52] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:59] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:08] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:20] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:26] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:26] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:38] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:34:57] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:34:58] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:35:02] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:25] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:27] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:27] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:28] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:37] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:38] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:38] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:42] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:42] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:47] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:57:59] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:58:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:58:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:59:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:59:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:59:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:00:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:21] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:30] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:35] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:43] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:51] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:10] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:13] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:24] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:36] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:04] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:04] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:40] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:48] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:48] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:49] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:49] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:07:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:07:22] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:07:25] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:11:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:14:46] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:14:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:02] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:18] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:18] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:53] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:58] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:59] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:05] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:09] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:11] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:20] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:22] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:40] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:04] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:17] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:35] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:18:09] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:18:13] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:19:15] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:22] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:22] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:24] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:26] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:29] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:36] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:36] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:01] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:01] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:03] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:04] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:06] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:20] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:20] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:21] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:22] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:24] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:39] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:47] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:48] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:49] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:49] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:56] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:58] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:23:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:14] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:14] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:15] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:15] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:16] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:20] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:26] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:27] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:11] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:40] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:47] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:52] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:52] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:54] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:55] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:55] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:55] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:03] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:24] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:45] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:48] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:51] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:51] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:51] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:58] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:29:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:29:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:18] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:22] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:23] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:01:56] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:02:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:04:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:04:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:03] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:04] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:04] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:23] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:23] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:31] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:31] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:51] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:52] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:53] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:07] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:09] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:09] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:18:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:19:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:19:54] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:08] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:30] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:46] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:21:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:21:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:21:44] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:22:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:09] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:22] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:33] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:26] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:22] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:44] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:12] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:18] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:24] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:53] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:29:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:30:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:31:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:31:24] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:24] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:39:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:39:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:40:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:41:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:41:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:54] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:55] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:02] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:44:12] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:44:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:47:05] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:47:05] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:47:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:56:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:56:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:58:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:58:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:58:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:21] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:22] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:00:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:02:05] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:02:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:06:47] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:06:48] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:06:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:07:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:07:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:20:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:20:51] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:22:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:24:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:25:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:31:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:58:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 16:21:51] "GET /200 404 HTTP/1.0" diff --git a/uweb3/scaffold/base.wsgi b/uweb3/scaffold/base.wsgi deleted file mode 100644 index 3090755c..00000000 --- a/uweb3/scaffold/base.wsgi +++ /dev/null @@ -1,16 +0,0 @@ -"""WSGI script for Apache mod_wsgi - -For more information about running and configuring mod_wsgi, please refer to -documentation at https://code.google.com/p/modwsgi/wiki/DeveloperGuidelines. -""" - -# Add the current directory to site packages, this allows importing of the -# project. For production, installing this into a virtualenv is recommended. -import os -import site -#site.addsitedir('/path/to/virtualenv/site-packages') -site.addsitedir(os.path.dirname(__file__)) - -# Import the project and create a WSGI application object -import base -application = base.main() diff --git a/uweb3/scaffold/base/README.md b/uweb3/scaffold/base/README.md deleted file mode 100644 index 9b4fa758..00000000 --- a/uweb3/scaffold/base/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# µWeb3 application scaffold - -This is an empty µWeb3 base project that serves as the starting point for your -project. The following parts are included and allow for easy extension: - -* µWeb3 request routing and server setup (in `__init__.py`) -* a basic configuration file that is read upon app start (`config.ini`) -* use of PageMaker (in 'pages.py') and example template usage (templates in `templates/`) -* included Apache WSGI configuration and development server runner. - -# How to run - -* Run `serve.py` from the commandline -* Use the included `base.wsgi` script to set up Apache + mod_wsgi diff --git a/uweb3/scaffold/base/__init__.py b/uweb3/scaffold/base/__init__.py deleted file mode 100644 index 3df73e8b..00000000 --- a/uweb3/scaffold/base/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -"""A minimal uWeb3 project scaffold.""" - -# Standard modules -import os - -# Third-party modules -import uweb3 - -# Application -from . import pages - -def main(sio=None): - """Creates a uWeb3 application. - - The application is created from the following components: - - - The presenter class (PageMaker) which implements the request handlers. - - The routes iterable, where each 2-tuple defines a url-pattern and the - name of a presenter method which should handle it. - - The configuration file (ini format) from which settings should be read. - """ - path = os.path.dirname(os.path.abspath(__file__)) - routes = ( - ('/', 'Index'), - #test routes - ('/sqlalchemy', 'Sqlalchemy'), - ('/test', 'Test'), - ('/getrawtemplate.*', 'GetRawTemplate'), - ('/parsed', 'Parsed'), - ('/test/escaping', 'StringEscaping'), - # (sio.on('test', namespace="/namespace"), 'EventHandler'), - # (sio.on('connect'), 'Connect'), - ('/(.*)', 'FourOhFour')) - - return uweb3.uWeb(pages.PageMaker, routes, executing_path=path) diff --git a/uweb3/scaffold/base/config.ini b/uweb3/scaffold/base/config.ini deleted file mode 100644 index d9ea0622..00000000 --- a/uweb3/scaffold/base/config.ini +++ /dev/null @@ -1,18 +0,0 @@ -[development] -# By default, all logging is enabled, the following two -# configuration options can be used to disable them -access_logging = True -error_logging = True -port = 8000 -dev = True -uweb_dev = True - -[mysql] -host = 127.0.0.1 -user = stef -password = 24192419 -database = uweb - -[routing] -disable_automatic_route_detection = False -default_routing = routes diff --git a/uweb3/scaffold/base/pages.py b/uweb3/scaffold/base/pages.py deleted file mode 100644 index cd42f3b8..00000000 --- a/uweb3/scaffold/base/pages.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python -"""Request handlers for the uWeb3 project scaffold""" - -from uweb3 import response -from uweb3.model import SettingsManager -from uweb3 import DebuggingPageMaker - -class PageMaker(DebuggingPageMaker): - """Holds all the request handlers for the application""" - - def Index(self): - """Returns the index template""" - return self.parser.Parse('index.html') - - def FourOhFour(self, path): - """The request could not be fulfilled, this returns a 404.""" - self.req.response.httpcode = 404 - return self.parser.Parse('404.html', path=path) diff --git a/uweb3/scaffold/base/static/css/base.css b/uweb3/scaffold/base/static/css/base.css deleted file mode 100644 index 95740706..00000000 --- a/uweb3/scaffold/base/static/css/base.css +++ /dev/null @@ -1,1198 +0,0 @@ -/* This is the Underdark base CSS. Its structure is based on SMACSS. See -https://smacss.com for more info. */ - -/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Change the default font family in all browsers (opinionated). - * 2. Prevent adjustments of font size after orientation changes in IE and iOS. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove the margin in all browsers (opinionated). - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - * 1. Add the correct display in Edge, IE, and Firefox. - * 2. Add the correct display in IE. - */ - -article, -aside, -details, /* 1 */ -figcaption, -figure, -footer, -header, -main, /* 2 */ -menu, -nav, -section, -summary { /* 1 */ - display: block; -} - -/** - * Add the correct display in IE 9-. - */ - -audio, -canvas, -progress, -video { - display: inline-block; -} - -/** - * Add the correct display in iOS 4-7. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Add the correct display in IE 10-. - * 1. Add the correct display in IE. - */ - -template, /* 1 */ -[hidden] { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * 1. Remove the gray background on active links in IE 10. - * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. - */ - -a { - background-color: transparent; /* 1 */ - -webkit-text-decoration-skip: objects; /* 2 */ -} - -/** - * Remove the outline on focused links when they are also active or hovered - * in all browsers (opinionated). - */ - -a:active, -a:hover { - outline-width: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * 1. Remove the bottom border in Firefox 39-. - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Prevent the duplicate application of `bolder` by the next rule in Safari 6. - */ - -b, -strong { - font-weight: inherit; -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * Add the correct font style in Android 4.3-. - */ - -dfn { - font-style: italic; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Add the correct background and color in IE 9-. - */ - -mark { - background-color: #ff0; - color: #000; -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10-. - */ - -img { - border-style: none; -} - -/** - * Hide the overflow in IE. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct margin in IE 8. - */ - -figure { - margin: 1em 40px; -} - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change font properties to `inherit` in all browsers (opinionated). - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font: inherit; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Restore the font weight unset by the previous rule. - */ - -optgroup { - font-weight: bold; -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` - * controls in Android 4. - * 2. Correct the inability to style clickable types in iOS and Safari. - */ - -button, -html [type="button"], /* 1 */ -[type="reset"], -[type="submit"] { - -webkit-appearance: button; /* 2 */ -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Change the border, margin, and padding in all browsers (opinionated). - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Remove the default vertical scrollbar in IE. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10-. - * 2. Remove the padding in IE 10-. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. - */ - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Correct the text style of placeholders in Chrome, Edge, and Safari. - */ - -::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/*! -Pure v0.6.0 -Copyright 2014 Yahoo! Inc. All rights reserved. -Licensed under the BSD License. -https://github.com/yahoo/pure/blob/master/LICENSE.md -*/ - -/*csslint important:false*/ - -/* ========================================================================== - Pure Base Extras - ========================================================================== */ - -/** - * Extra rules that Pure adds on top of Normalize.css - */ - -/** - * Always hide an element when it has the `hidden` HTML attribute. - */ - -.hidden, -[hidden] { - display: none !important; -} - -button, -[type="button"], -[type="reset"], -[type="submit"], -.button { - /* Structure */ - display: inline-block; - zoom: 1; - line-height: 1.25rem; - white-space: nowrap; - vertical-align: middle; - text-align: center; - -webkit-user-drag: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -/* Firefox: Get rid of the inner focus border */ -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner, -.button::-moz-focus-inner { - padding: 0; - border: 0; -} - -/*csslint outline-none:false*/ - -button, -[type="button"], -[type="reset"], -[type="submit"], -.button { - font-family: inherit; - font-size: 100%; - padding: 0.5em 1em; - color: #444; /* rgba not supported (IE 8) */ - color: rgba(0, 0, 0, 0.80); /* rgba supported */ - border: 1px solid #999; /*IE 6/7/8*/ - border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ - background-color: #E6E6E6; - text-decoration: none; - border-radius: 2px; -} - -button:hover, -button:focus, -[type="button"]:hover, -[type="button"]:focus, -[type="reset"]:hover, -[type="reset"]:focus, -[type="submit"]:hover, -[type="submit"]:focus, -.button.hover, -.button:hover, -.button:focus { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); - background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -button:focus, -[type="button"]:focus, -[type="reset"]:focus, -[type="submit"]:focus, -.button:focus { - outline: 0; -} -button.active, -button:active, -[type="button"]:active, -[type="reset"]:active, -[type="submit"]:active, -.button.active, -.button:active { - box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20) inset; - border-color: #000\9; -} - -button[disabled], -[type="button"][disabled], -[type="reset"][disabled], -[type="submit"][disabled], -.button[disabled], -.button.disabled, -.button.disabled:hover, -.button.disabled:focus, -.button.disabled:active { - border: none; - background-image: none; - opacity: 0.40; - cursor: not-allowed; - box-shadow: none; -} - -/*csslint box-model:false*/ -/* -Box-model set to false because we're setting a height on select elements, which -also have border and padding. This is done because some browsers don't render -the padding. We explicitly set the box-model for select elements to border-box, -so we can ignore the csslint warning. -*/ - -input[type="text"], -input[type="password"], -input[type="email"], -input[type="url"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="datetime"], -input[type="datetime-local"], -input[type="week"], -input[type="number"], -input[type="search"], -input[type="tel"], -input[type="color"], -input:not([type]), -select, -textarea { - padding: 0.4375em 0.6em; - display: inline-block; - border: 1px solid #ccc; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .13); - border-radius: 4px; - vertical-align: middle; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -input[type="text"], -input[type="password"], -input[type="email"], -input[type="url"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="datetime"], -input[type="datetime-local"], -input[type="week"], -input[type="number"], -input[type="search"], -input[type="tel"], -input[type="color"], -input:not([type]), -select { - height: 2.375em; -} - -/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */ -/* May be able to remove this tweak as color inputs become more standardized across browsers. */ -input[type="color"] { - padding: 0.2em 0.5em; -} - -input[type="text"]:focus, -input[type="password"]:focus, -input[type="email"]:focus, -input[type="url"]:focus, -input[type="date"]:focus, -input[type="month"]:focus, -input[type="time"]:focus, -input[type="datetime"]:focus, -input[type="datetime-local"]:focus, -input[type="week"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="tel"]:focus, -input[type="color"]:focus, -select:focus, -textarea:focus { - outline: 0; - border-color: #129FEA; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -input:not([type]):focus { - outline: 0; - border-color: #129FEA; -} - -.checkbox, -.radio { - margin: 0.5em 0; - display: block; -} - -input[type="text"][disabled], -input[type="password"][disabled], -input[type="email"][disabled], -input[type="url"][disabled], -input[type="date"][disabled], -input[type="month"][disabled], -input[type="time"][disabled], -input[type="datetime"][disabled], -input[type="datetime-local"][disabled], -input[type="week"][disabled], -input[type="number"][disabled], -input[type="search"][disabled], -input[type="tel"][disabled], -input[type="color"][disabled], -input[type="file"][disabled], -select[disabled], -textarea[disabled] { - cursor: not-allowed; - color: #cad2d3; -} -input[type="text"][disabled], -input[type="password"][disabled], -input[type="email"][disabled], -input[type="url"][disabled], -input[type="date"][disabled], -input[type="month"][disabled], -input[type="time"][disabled], -input[type="datetime"][disabled], -input[type="datetime-local"][disabled], -input[type="week"][disabled], -input[type="number"][disabled], -input[type="search"][disabled], -input[type="tel"][disabled], -input[type="color"][disabled], -select[disabled], -textarea[disabled] { - background-color: #eaeded; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -input:not([type])[disabled] { - cursor: not-allowed; - background-color: #eaeded; - color: #cad2d3; -} -input[readonly], -select[readonly], -textarea[readonly] { - background-color: #eee; /* menu hover bg color */ - color: #777; /* menu text color */ - border-color: #ccc; -} - -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #e9322d; -} - -select { - padding-top: .375em; - padding-bottom: .375em; - background-color: white; -} -select[multiple] { - height: auto; -} -label { - margin: 0.5em 0 0.2em; -} -fieldset { - margin: 0; - padding: 0.35em 0 0.75em; - border: 0; -} -legend { - display: block; - width: 100%; - padding: 0.3em 0; - margin-bottom: 0.3em; - color: #333; - border-bottom: 1px solid #e5e5e5; -} - -.stacked input[type="text"], -.stacked input[type="password"], -.stacked input[type="email"], -.stacked input[type="url"], -.stacked input[type="date"], -.stacked input[type="month"], -.stacked input[type="time"], -.stacked input[type="datetime"], -.stacked input[type="datetime-local"], -.stacked input[type="week"], -.stacked input[type="number"], -.stacked input[type="search"], -.stacked input[type="tel"], -.stacked input[type="color"], -.stacked input[type="file"], -.stacked select, -.stacked label, -.stacked textarea { - display: block; - margin: 0.25em 0; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -.stacked input:not([type]) { - display: block; - margin: 0.25em 0; -} -.aligned input, -.aligned textarea, -.aligned select, -/* NOTE: help-inline is deprecated. Use .message-inline instead. */ -.aligned .help-inline, -.message-inline { - display: inline-block; - *display: inline; - *zoom: 1; - vertical-align: middle; -} -.aligned textarea { - vertical-align: top; -} - -/* Aligned Forms */ -.aligned .control-group { - margin-bottom: 0.5em; -} -.aligned .control-group label { - text-align: right; - display: inline-block; - vertical-align: middle; - width: 10em; - margin: 0 1em 0 0; -} -.aligned .controls { - margin: 1.5em 0 0 11em; -} - -/* Rounded Inputs */ -input.input-rounded, -.input-rounded { - border-radius: 2em; - padding: 0.5em 1em; -} - -/* Grouped Inputs */ -.group fieldset { - margin-bottom: 10px; -} -.group input, -.group textarea { - display: block; - padding: 10px; - margin: 0 0 -1px; - border-radius: 0; - position: relative; - top: -1px; -} -.group input:focus, -.group textarea:focus { - z-index: 3; -} -.group input:first-child, -.group textarea:first-child { - top: 1px; - border-radius: 4px 4px 0 0; - margin: 0; -} -.group input:first-child:last-child, -.group textarea:first-child:last-child { - top: 1px; - border-radius: 4px; - margin: 0; -} -.group input:last-child, -.group textarea:last-child { - top: -2px; - border-radius: 0 0 4px 4px; - margin: 0; -} -.group button { - margin: 0.35em 0; -} - -.input-1 { - width: 100%; -} -.input-2-3 { - width: 66%; -} -.input-1-2 { - width: 50%; -} -.input-1-3 { - width: 33%; -} -.input-1-4 { - width: 25%; -} - -/* Inline help for forms */ -/* NOTE: help-inline is deprecated. Use .message-inline instead. */ -.help-inline, -.message-inline { - display: inline-block; - padding-left: 0.3em; - color: #666; - vertical-align: middle; - font-size: 0.875em; -} - -/* Block help for forms */ -form p { - color: #666; - font-size: 0.875em; -} - -@media only screen and (max-width: 480px) { - [type="button"], - [type="reset"], - [type="submit"], - form button:not([type]) { - margin: 0.7em 0 0; - } - - input:not([type]), - input[type="text"], - input[type="password"], - input[type="email"], - input[type="url"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="datetime"], - input[type="datetime-local"], - input[type="week"], - input[type="number"], - input[type="search"], - input[type="tel"], - input[type="color"], - select, - textarea, - label { - margin-bottom: 0.3em; - display: block; - } - - .group input:not([type]), - .group input[type="text"], - .group input[type="password"], - .group input[type="email"], - .group input[type="url"], - .group input[type="date"], - .group input[type="month"], - .group input[type="time"], - .group input[type="datetime"], - .group input[type="datetime-local"], - .group input[type="week"], - .group input[type="number"], - .group input[type="search"], - .group input[type="tel"], - .group input[type="color"] { - margin-bottom: 0; - } - - .aligned .control-group label { - margin-bottom: 0.3em; - text-align: left; - display: block; - width: 100%; - } - - .aligned .controls { - margin: 1.5em 0 0 0; - } - - /* NOTE: help-inline is deprecated. Use .message-inline instead. */ - .help-inline, - .message-inline, - .message, - .explanation { - display: block; - font-size: 0.75em; - /* Increased bottom padding to make it group with its related input element. */ - padding: 0.2em 0 0.8em; - } -} - -/*csslint adjoining-classes: false, box-model:false*/ -.menu { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.menu-fixed { - position: fixed; - left: 0; - top: 0; - z-index: 3; -} - -.menu-list, -.menu-item { - position: relative; -} - -.menu-list { - list-style: none; - margin: 0; - padding: 0; -} - -.menu-item { - padding: 0; - margin: 0; - height: 100%; -} - -.menu-link, -.menu-heading { - display: block; - text-decoration: none; - white-space: nowrap; -} - -/* HORIZONTAL MENU */ -.menu-horizontal { - width: 100%; - white-space: nowrap; -} - -.menu-horizontal .menu-list { - display: inline-block; -} - -/* Initial menus should be inline-block so that they are horizontal */ -.menu-horizontal .menu-item, -.menu-horizontal .menu-heading, -.menu-horizontal .menu-separator { - display: inline-block; - *display: inline; - zoom: 1; - vertical-align: middle; -} - -/* Submenus should still be display: block; */ -.menu-item .menu-item { - display: block; -} - -.menu-children { - display: none; - position: absolute; - left: 100%; - top: 0; - margin: 0; - padding: 0; - z-index: 3; -} - -.menu-horizontal .menu-children { - left: 0; - top: auto; - width: inherit; -} - -.menu-allow-hover:hover > .menu-children, -.menu-active > .menu-children { - display: block; - position: absolute; -} - -/* Vertical Menus - show the dropdown arrow */ -.menu-has-children > .menu-link:after { - padding-left: 0.5em; - content: "\25B8"; - font-size: small; -} - -/* Horizontal Menus - show the dropdown arrow */ -.menu-horizontal .menu-has-children > .menu-link:after { - content: "\25BE"; -} - -/* scrollable menus */ -.menu-scrollable { - overflow-y: scroll; - overflow-x: hidden; -} - -.menu-scrollable .menu-list { - display: block; -} - -.menu-horizontal.menu-scrollable .menu-list { - display: inline-block; -} - -.menu-horizontal.menu-scrollable { - white-space: nowrap; - overflow-y: hidden; - overflow-x: auto; - -ms-overflow-style: none; - -webkit-overflow-scrolling: touch; - /* a little extra padding for this style to allow for scrollbars */ - padding: .5em 0; -} - -.menu-horizontal.menu-scrollable::-webkit-scrollbar { - display: none; -} - -/* misc default styling */ - -.menu-separator { - background-color: #ccc; - height: 1px; - margin: .3em 0; -} - -.menu-horizontal .menu-separator { - width: 1px; - height: 1.3em; - margin: 0 .3em ; -} - -.menu-heading { - text-transform: uppercase; - color: #565d64; -} - -.menu-link { - color: #777; -} - -.menu-children { - background-color: #fff; -} - -.menu-link, -.menu-disabled, -.menu-heading { - padding: .5em 1em; -} - -.menu-disabled { - opacity: .5; -} - -.menu-disabled .menu-link:hover { - background-color: transparent; -} - -.menu-active > .menu-link, -.menu-link:hover, -.menu-link:focus { - background-color: #eee; -} - -.menu-selected .menu-link, -.menu-selected .menu-link:visited { - color: #000; -} - -table { - /* Remove spacing between table cells (from Normalize.css) */ - border-collapse: collapse; - border-spacing: 0; - empty-cells: show; - border: 1px solid rgba(0, 0, 0, .1); -} -table + table { - margin-top: 1rem; -} - -caption { - font: italic 85%/1 'Arial', sans-serif; - text-align: center; - padding: 1em 0; -} - -td, -th { - border-left: 1px solid rgba(0, 0, 0, .1);/* inner column border */ - border-width: 0 0 0 1px; - font-size: inherit; - margin: 0; - overflow: visible; /*to make ths where the title is really long work*/ - padding: 0.5em 1em; /* cell padding */ -} - -thead, -tfoot { - text-align: left; - background-color: rgba(0, 0, 0, .1); -} - -/* end Pure */ - -html { - font-size: medium; - line-height: 1.5; -} - -:lang(en) { - quotes: '“' '”' '‘' '’'; -} -:lang(nl) { - quotes: '„' '”' '‚' '’'; -} - -q { - quotes: none; -} -q:before, -blockquote:before { - content: open-quote; -} -q:after, -blockquote:after { - content: close-quote; -} - -mark { - padding: .125em 0; -} - -/* input range */ -input[type="range"] { - width: 100%; /* Specific width is required for Firefox. */ - padding: 0; /* Gecko */ -} - -:not(pre) > code { - color: #000; - padding: .0625em .25em; - border: 1px solid rgba(0, 0, 0, .12); - border-radius: .125em; - background-color: rgba(0, 0, 0, .02); -} diff --git a/uweb3/scaffold/base/static/css/layout.css b/uweb3/scaffold/base/static/css/layout.css deleted file mode 100644 index f3a456bc..00000000 --- a/uweb3/scaffold/base/static/css/layout.css +++ /dev/null @@ -1,1146 +0,0 @@ -/* This is the Underdark layout CSS. Its structure is based on SMACSS. See -https://smacss.com for more info. */ - -/* Colors */ -/* 0, 120, 231 */ - -/* Complimentary */ -/* 154, 96, 0 */ -/* 0, 80, 154 */ - -/* Triad */ -/* 15, 88, 154 */ -/* 235, 35, 23 */ - -/* 167, 180, 18 */ -/* 143, 154, 8 */ - -html { - font-family: Arial, sans-serif; -} -body { - color: #444; - line-height: 1.5; - background-color: #fff; -} -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.25; -} -h1 { - font-size: 1.75em; -} -h2 { - font-size: 1.375em; -} -h3 { - font-size: 1.125em; -} - -blockquote { - font-style: italic; - margin-left: 2.5rem; - margin-right: 2.5rem; -} - -/* title */ -abbr[title], -span[title] { - cursor: help; -} -span[title] { - text-decoration: underline dotted; -} - -/* list */ -ol, -ul { - padding-left: 2.5rem; -} - -/* description list */ -dt { - font-weight: bold; -} -dd { - margin-left: 2.5rem; -} - -/* monospace */ -pre { - color: #000; - padding: 1rem; - border: 1px solid rgba(0, 0, 0, .12); - background-color: rgba(0, 0, 0, .02); - white-space: pre-wrap; -} -table pre { - margin: 0; - padding: 0; - border: 0; - background-color: transparent; -} -table code { - padding: 0; - border: 0; - border-radius: 0; - background-color: transparent; -} -kbd { - text-align: center; - text-transform: uppercase; - padding: .125rem .375rem; - border-bottom: .125rem solid #ccc; - border-radius: .25rem; - background-color: #f2f2f2; -} - -/* figure */ -figure { - display: inline-block; - padding: .5rem; - border: 1px solid #eee; -} -figcaption { - font-size: .875em; - font-style: italic; -} - -/* table */ -table { - width: 100%; -} -table + form, -div.scrollable + form { /* assumes scrollable contains table */ - border-top: 0; -} -td a, -th a { - display: inline-block; /* increase click area */ -} - -table ol, -table ul { - padding-left: 1rem; -} - -form table { - margin: 0 0 .5rem; -} -form + form { - border-top: 0; -} - -/* form single div */ -/* form > div:only-of-type > [type="submit"], -form fieldset > div:only-of-type > [type="submit"] { - margin: 0; -} */ -@media (min-width: 30rem) { - form input ~ input, - form input ~ select, - form select ~ input, - form select ~ select { - margin-left: 1rem; - } - - form div > label ~ button, - form div > label ~ [type="button"], - form div > label ~ [type="reset"], - form div > label ~ [type="submit"], - form div > label ~ .button { - margin: .0625rem 0 .0625rem 1rem; - } - form > div:only-of-type, /* don't put the submit outside the form control block */ - form fieldset > div:only-of-type { - margin: 0; - } - form > div:only-of-type > [type="submit"], - form fieldset > div:only-of-type > [type="submit"] { - min-width: auto; /* set default min-width, otherwise flexbox won't let the button grow when the value text is too long */ - } -} -form > div + [type="submit"], -form fieldset > div + [type="submit"] { - margin-top: .5rem; -} - -/*form > table { - margin-top: .25rem; -}*/ -th, -thead td, -tfoot td { - padding: .5rem 1rem; -} - -/* form */ -form { - padding: 1rem; - border: 1px solid #e5e5e5; - background-color: #f7f7f7; - box-sizing: border-box; -} -td > form, -th > form, -body > header form, -body > footer form { - padding: 0; - border: 0; - background-color: transparent; -} -body > header form { - font-size: .75rem; -} -body > header form.login, -body > header form.search { - margin-top: .5rem; -} - -body > header [type="submit"] { - min-width: 4rem; -} - -td > form, -th > form, -section > footer > form { - display: inline-block; -} - -/* input */ -input, -select, -textarea { - line-height: 1.25; - -webkit-transition: border-color .3s; - transition: border-color .3s; -} - -/* textarea */ -textarea { - max-width: 100%; - height: 10rem; - min-height: 4rem; -} -table textarea, -form > textarea { - vertical-align: top; -} -form fieldset > textarea { - vertical-align: baseline; -} - -/* button */ -button, -[type="button"], -[type="reset"], -[type="submit"], -.button { - color: #000; - min-width: 8rem; - max-width: 100%; -} -a.button:hover { - text-decoration: none; -} - -/* button in table */ -table button, -table [type="button"], -table [type="reset"], -table [type="submit"], -table .button { - font-size: .875em; - min-width: 4rem; - line-height: 1rem; - padding: .25rem .5rem; - vertical-align: top; -} - -/* form > button:not(:only-child), -form > [type="button"]:not(:only-child), -form > [type="reset"]:not(:only-child), -form > [type="submit"]:not(:only-child) { - margin-top: .5rem; -} -form > button:only-child, -form > [type="button"]:only-child, -form > [type="reset"]:only-child, -form > [type="submit"]:only-child { - margin-top: 0; -} */ - -form > div, -form fieldset > div { - margin: 0 0 .5rem; -} - -form div > input[type="number"] + span, -form div > input[type="range"] + span { - min-width: 24%; -} - -/* containers */ -body > header > div, -body > footer > div > nav, -body > footer > div.copyright p, -body > :not(nav) + main section, -body > :not(nav) + main :not(section) > .magazine, -body > :not(nav) + main :not(section) > .newspaper { - max-width: 80rem; - margin: 0 auto; - padding: 0 5%; -} - -/* body nav */ -body > nav { - padding: 0 5%; -} -body > nav > ul { - list-style: none; -} - -/* main */ -main > section, -main :not(aside):not(nav) > section { - padding: 2.5rem 5%; - word-wrap: break-word; -} -/* main section header */ -main section > header { - margin-bottom: 2.5rem; -} -main section > header > h1 { - margin-bottom: 0; -} -main section > header > h1 + p { - font-size: 1.125em; - margin-top: 0; -} -/* main section footer */ -main section > footer { - margin-top: 2.5rem; -} -main section > footer a { - display: inline-block; /* for images */ - max-width: 100%; /* for images */ -} -main section > footer > a > img { - display: block; - max-width: 100%; - height: auto; -} - -/* main sidebar */ -main > aside, -main > nav { - padding: 1rem 5%; -} -main > aside > ul, -main > nav > ul { - padding: 0; - list-style: none; -} -main aside > form, -main aside > nav, -main aside > section { - margin-bottom: 1em; - border: 1px solid #e2e2e2; - background-color: #f4f4f4; -} -main aside > section, -main aside > nav { - padding: 0 1em; -} -main aside > section > a, -main aside > nav > a { - display: inline-block; - margin-bottom: 1em; -} -main aside > section > form { - margin: 1em 0; - padding: 0; - border: 0; - background-color: transparent; -} - -main aside form, -main nav form { - padding-left: 1rem; - padding-right: 1rem; -} -main aside form > div, -main aside form fieldset > div, -main nav form > div, -main nav form fieldset > div { - display: block; /* cancel flexbox */ -} -main aside [type="submit"], -main nav [type="submit"] { - margin-left: 0; -} - -main aside legend { - color: inherit; -} - -body > nav + main { - padding: 0 5%; -} - -/* magazine and newspaper */ -main > .magazine, -main > .newspaper { - padding-left: 5%; - padding-right: 5%; -} -.magazine, -.newspaper { - column-gap: 3rem; - -moz-column-gap: 3rem; - -webkit-column-gap: 3rem; -} -.magazine { - columns: 20rem 2; - -moz-columns: 20rem 2; - -webkit-columns: 20rem 2; -} -.newspaper { - columns: 12rem 3; - -moz-columns: 12rem 3; - -webkit-columns: 12rem 3; -} -.magazine > *, -.newspaper > * { - display: inline-block; - width: 100%; -} -.magazine > section, -.newspaper > section { - padding: 0; -} -main section > header + .magazine > * > :first-child, -main section > header + .newspaper > * > :first-child { - /*margin-top: 0;*/ -} -main aside ~ div > .magazine, -main aside ~ div > .newspaper, -main > nav ~ div > .magazine, -main > nav ~ div > .newspaper { - padding: 0 5%; -} - -/* .pure-img */ -main > section > img { - display: block; - max-width: 100%; - height: auto; -} - -/* form */ -form ol, -form ul { - /*margin: 0 0 -.5rem 0;*/ - margin: .4375rem 0 .5rem; - padding: 0; - list-style: none; -} -form > div > p, -form fieldset > div > p { - margin-top: .5625rem; -} - -/* form li:not(:last-child) { - margin-bottom: .25rem; -} */ -/*form :not(fieldset) > ol > li, -form :not(fieldset) > ul > li { - display: inline-block; - margin-right: .5rem; -}*/ -form li > input[type="checkbox"] + label, -form li > input[type="radio"] + label { - display: inline; -} - -form :not(label):not(li) > input:not([type]), -form :not(label):not(li) > input[type="text"], -form :not(label):not(li) > input[type="password"], -form :not(label):not(li) > input[type="email"], -form :not(label):not(li) > input[type="url"], -form :not(label):not(li) > input[type="date"], -form :not(label):not(li) > input[type="month"], -form :not(label):not(li) > input[type="time"], -form :not(label):not(li) > input[type="datetime"], -form :not(label):not(li) > input[type="datetime-local"], -form :not(label):not(li) > input[type="week"], -form :not(label):not(li) > input[type="number"], -form :not(label):not(li) > input[type="search"], -form :not(label):not(li) > input[type="tel"], -form :not(label):not(li) > input[type="color"], -form :not(label):not(li) > input[type="file"], -form :not(label):not(li) > select, -form :not(label):not(li) > textarea { - width: 100%; - min-width: 0; -} - -form :not(label):not(li) > input[type="range"] { - height: 1.5rem; - margin-top: .4375rem; -} - -/* body header and footer */ -body > header, -body > footer { - font-size: .9375rem; -} -body > header nav ul, -body > footer nav ul { - padding: 0; - list-style: none; -} -body > header nav ul:not(.external) > li > a, -body > header div.logo > a { - text-decoration: none; -} - -/* body header */ -body > header { - color: #fff; - left: 0; - top: 0; - width: 100%; - height: 4.25rem; - border-bottom: .375rem solid #888; - background-color: #333; - overflow: hidden; - z-index: 1; - -webkit-tap-highlight-color: transparent; -} -body > header nav ul { - margin: 0; -} - -/* logo */ -body > header > div.logo { - /* the logo sometimes is a direct descendant of body header */ - padding: 0; -} -body > header div.logo > a { - font-size: 2rem; - font-weight: bold; - display: inline-block; /* decrease click area width */ - line-height: 2rem; - vertical-align: top; -} -body > header div.logo > a:only-child { - margin: .75rem 0; -} -body > header div.logo > p { - line-height: 1.5rem; /* round line-height */ - margin: 0; -} -@media screen and (max-width: 39.9375rem) { - body > header div.logo + nav { - padding-top: .375rem; - } -} - -/* menu toggle */ -body > header button.toggle { - text-indent: -9999rem; - position: absolute; - right: 5%; - top: 1rem; - width: 3rem; - min-width: 0; - height: 2.25rem; - border-radius: .125rem; - background: url('data:image/svg+xml,') center / 1rem no-repeat transparent; -} - -/* body header form */ -body > header > div > form > div { - margin-bottom: 0; -} -@media screen and (min-width: 40rem) { - body > header > div > form > div { - display: inline-block; - vertical-align: bottom; - } -} - -/* body header search */ -@media screen and (max-width: 39.9375rem) { - body > header > div > form + nav { - margin-top: .75rem; - } -} - -body > header > div > form + nav > ul.external { - margin: .875rem 0; -} - -@media screen and (max-width: 39.9375rem) { - body > header nav::after { - /* clearfix */ - content: ''; - display: table; - clear: right; - } -} - -/* external nav links */ -body > header nav > ul.external { - font-size: .875rem; - float: right; - margin: .375rem 0; -} -body > header nav > ul.external > li { - display: inline-block; -} -body > header nav > ul.external > li:not(:last-child) { - margin-right: 1em; -} -body > header nav > ul.external > li > a { - color: #888; -} - -/* site nav links */ -body > header nav > ul:not(.external) { - clear: right; - line-height: 1.25rem; /* round line-height */ -} -/* links and buttons */ -body > header nav > ul:not(.external) a, -body > header nav > ul:not(.external) > li > button { - width: 100%; - padding: .375rem 1em; - -webkit-transition: background-color .15s; - transition: background-color .15s; -} -body > header nav > ul:not(.external) a { - color: #fff; - display: block; - box-sizing: border-box; -} -body > header nav > ul:not(.external) a:focus, -body > header nav > ul:not(.external) > li > button:focus { - outline: 0; -} -body > header nav > ul:not(.external) a > i.fa { - /* equivalent of Font Awesome's fa-fw class */ - width: 1.28571429em; - text-align: center; -} -body > header nav > ul:not(.external) [type="submit"] { - margin-top: 0; - width: 100%; -} -@media screen and (min-width: 30rem) { - body > header nav > ul:not(.external) [type="submit"] { - margin-left: 0; /* reset */ - } -} -@media screen and (min-width: 40rem) { - body > header nav > ul:not(.external) [type="submit"] { - position: static; /* reset body > header [type="submit"] */ - line-height: inherit; - padding-top: .4375rem; - vertical-align: baseline; - } -} -/* top level links and buttons */ -body > header nav > ul:not(.external) > li > a, -body > header nav > ul:not(.external) > li > button { - border-radius: .125rem; - background-color: #444; - box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); - background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -body > header nav > ul:not(.external) > li > a:hover, -body > header nav > ul:not(.external) > li > a:focus, -body > header nav > ul:not(.external) > li > button:hover, -body > header nav > ul:not(.external) > li > button:focus { - background-color: #555; -} -body > header nav > ul:not(.external) > li > button { - color: inherit; - min-width: 0; - line-height: inherit; - vertical-align: baseline; -} -body > header nav > ul:not(.external) > li > button::after { - content: url('data:image/svg+xml,'); - display: inline-block; - width: .5rem; - height: 1.25rem; - margin-left: .25rem; - vertical-align: top; - opacity: .6; -} -body > header nav > ul:not(.external) > li.is-open > button { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - background-color: #555; -} - -/* sub links */ -body > header nav > ul:not(.external) > li > ul { - height: 0; - overflow: hidden; - -webkit-transition: height ease-in-out .2s; - transition: height ease-in-out .2s; -} -body > header nav > ul:not(.external) > li > ul > li > a { - background-color: #555; -} -body > header nav > ul:not(.external) > li > ul > li > a:hover, -body > header nav > ul:not(.external) > li > ul > li > a:focus { - background-color: #666; -} - -/* main */ - -/* inputs may have container divs */ -body > header > div > form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -body > header > div > form select, -body > header > div > form textarea, -main aside form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main aside form select, -main aside form textarea { - padding-top: .375rem; - padding-bottom: .375rem; -} -body > header > div > form > [type="submit"], -main aside form > [type="submit"] { - line-height: normal; - padding-top: .375rem; - padding-bottom: .375rem; -} - -main > .magazine form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main > .magazine form select, -main > .magazine form label, -main > .magazine form textarea, -main > .newspaper form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main > .newspaper form select, -main > .newspaper form label, -main > .newspaper form textarea, -main aside form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main aside form select, -main aside form label, -main aside form textarea, -main > nav form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main > nav form select, -main > nav form label, -main > nav form textarea, -body > header form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -body > header form select, -body > header form label, -body > header form textarea { - display: block; - width: 100%; - margin: .25rem 0; -} - -/* body group */ -body > div.group { - display: flex; -} -body > div.group > nav { - color: #fff; - background-color: #444; -} -body > div.group > nav a { - color: #fff; - text-decoration: none; - display: block; - padding: 1rem; - -webkit-transition: background-color 0.2s; - transition: background-color 0.2s; -} -body > div.group > nav a:hover { - background-color: rgba(255, 255, 255, .1); -} -body > div.group > nav > h2 { - text-align: center; - margin: 0; -} -body > div.group > nav > ul { - margin: 0; - padding: 0; - list-style: none; -} -body > div.group > nav > ul > li > a { - border-bottom: 1px solid rgba(0, 0, 0, .5); -} -body > div.group > main { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - max-width: none; - margin-left: 0; - margin-right: 0; - padding: 0 2rem; -} - -/* body footer */ -body > footer { - background-color: #222; -} -body > footer > div { - padding-top: 2rem; - padding-bottom: 2rem; -} -body > footer nav { - padding-left: 5%; - padding-right: 5%; -} -body > footer nav:not(.magazine):not(.newspaper) > ul { - text-align: center; -} -body > footer nav:not(.magazine):not(.newspaper) > ul > li { - display: inline-block; - margin: .25rem 1rem; -} -body > footer nav > ul.external > li > a:not(:hover) { - color: #777; -} -/* copyright */ -body > footer > div.copyright { - color: #666; - padding: .5rem 0; - background-color: #333; -} - -@media screen and (min-width: 30rem) { - form fieldset > div > textarea { - min-width: 62%; - max-width: 62%; - } - form > div > output, - form > div > span, - form fieldset > div > output, - form fieldset > div > span { - margin-top: .4375rem; - white-space: nowrap; - } - form > div > label + span:last-child, - form fieldset > div > label + span:last-child { - margin-top: 0; - } - - form div > label + span > input[type="text"], - form div > label + span > input[type="password"], - form div > label + span > input[type="email"], - form div > label + span > input[type="url"], - form div > label + span > input[type="date"], - form div > label + span > input[type="month"], - form div > label + span > input[type="time"], - form div > label + span > input[type="datetime"], - form div > label + span > input[type="datetime-local"], - form div > label + span > input[type="week"], - form div > label + span > input[type="number"], - form div > label + span > input[type="search"], - form div > label + span > input[type="tel"], - form div > label + span > input[type="color"], - form div > label + span > input:not([type]), - form div > label + span > select, - form div > label + span > textarea { - vertical-align: inherit; - } - - form > div > input + span, - form > div > select + span, - form > div > textarea + span, - form > div > output + span, - form fieldset > div > input + span, - form fieldset > div > select + span, - form fieldset > div > textarea + span, - form fieldset > div > output + span { - margin-left: .5rem; - } - /* first span after label */ - form > div > label + span:not(:last-child), - form fieldset > div > label + span:not(:last-child) { - margin-right: .5rem; - } - - form > div > span > label, - form > div > label + span > output, - form fieldset > div > span > label, - form fieldset > div > label + span > output { - display: inline-block; - margin-top: .4375rem; - } - form > div > span > input, - form > div > span > select, - form > div > span > textarea, - form > div > label + span > input, - form > div > label + span > select, - form > div > label + span > textarea { - width: auto; - } - - form > div, - form fieldset > div { - display: flex; - display: -webkit-flex; - align-items: flex-start; - -webkit-align-items: flex-start; - } - form > div > label:first-child, - form fieldset > div > label:first-child { - margin: .4375rem 0 0; - flex: 0 0 38%; - -webkit-flex: 0 0 38%; - } - form > div > input[type="file"], - form fieldset > div > input[type="file"] { - margin-top: .375em; - } - form ol, - form ul, - form input:not([type]), - form input[type="text"], - form input[type="password"], - form input[type="email"], - form input[type="url"], - form input[type="date"], - form input[type="month"], - form input[type="time"], - form input[type="datetime"], - form input[type="datetime-local"], - form input[type="week"], - form input[type="number"], - form input[type="search"], - form input[type="tel"], - form input[type="color"], - form input[type="file"], - form select, - form textarea, - form > div > label + div, - form fieldset > div > label + div { - flex: 0 1 62%; - -webkit-flex: 0 1 62%; - } - form > div + button, - form > [type="submit"], - form fieldset + button, - form > p, - form fieldset > p, - form > div > label:only-child, - form > div > span:only-child, - form fieldset > div > label:only-child, - form fieldset > div > span:only-child { - margin-left: 38%; - } - table [type="submit"], - form > [type="submit"]:only-child, - form > :not(div):not(fieldset) + [type="submit"] { - margin-left: 0; /* reset */ - } -} - -@media screen and (max-width: 39.9375rem) { - body > header { - position: absolute; - -webkit-transition: height ease-in-out .3s; - transition: height ease-in-out .3s; - } - body > header > div { - padding-top: .375rem; - padding-bottom: .375rem; - } - body > header nav > ul:not(.external) > li:not(:last-child) { - margin-bottom: .1875rem; - } - - main { - padding-top: 4.625rem; - } -} -@media screen and (min-width: 40rem) { - h1 { - font-size: 2.375em; - } - - body > nav { - float: left; - padding-right: 0; - } - body > nav + main { - margin-left: 20%; - } - - main > aside, - main > nav, - section > aside { - float: left; - width: 24%; - padding-left: 3%; - } -/* body > :not(header) :not(main) > aside, - body > :not(header) :not(main) > nav { - padding-right: 3%; - } */ - main > aside, - main > nav { - padding-right: 0; - } - main section > aside { - padding-left: 3%; - padding-right: 0; - padding-bottom: 0; - } - - main > nav form input, - main > nav form button:not([type]), - main > nav form [type="submit"] { - min-width: 0; - max-width: 100%; - } - main aside ~ div, - main > nav ~ div, - main > nav ~ section { - margin-left: 27% - } -/* main aside ~ div > section, - main > nav ~ div > section { - padding-left: 0; - } */ - -/* main aside ~ div > .magazine, - main aside ~ div > .newspaper, - main > nav ~ div > .magazine, - main > nav ~ div > .newspaper { - padding-left: 0; - } */ - - form > div > textarea { - /* only give textarea min width, since they can be resized */ - min-width: 62%; - } - - form fieldset > div > .input-1-2 { - width: 38%; - margin-right: 1%; - } - form fieldset > div > .input-1-2:last-child { - margin-right: 0; - } - - form fieldset > div > [type="submit"] { - margin-left: 38%; - } - - body > header { - min-height: 8rem; /* in case JS sets height to 0 */ - max-height: 8rem; - } - body > header div.logo { - float: left; - margin-top: 2.25rem; - } - - body > header > div > form { - float: right; - margin-bottom: 1.625rem; - } - body > header [type="submit"] { - /* position: relative; */ - /* top: 1.625rem; */ - margin-left: 0 !important; /* TODO: fix weight of :not(aside):not(nav):not(.magazine):not(.newspaper) > :not(td):not(th) > form > div + [type="submit"] */ - margin-bottom: .3125rem; - } - - body > header nav { - clear: right; /* in case float right previous form */ - } - body > header nav > ul.external { - margin: 2.25rem 0; - line-height: 1.5rem; /* round line-height */ - } - body > header > div > :not(form) + nav > ul:not(.external):only-child { - clear: left; /* clear logo */ - padding-top: .25rem; - } - body > header > div > :not(form) + nav > ul.external:only-child { - margin: 3rem 0; - } - - body > header nav > ul:not(.external) { - text-align: right; - } - body > header nav > ul:not(.external) > li { - text-align: left; - display: inline-block; - } - - body > header > div > form + nav > ul:not(.external):only-child { - margin-top: 2rem; - } - body > header nav > ul:not(.external) > li > ul { - position: absolute; - } - body > header nav > ul:not(.external) > li > a, - body > header nav > ul:not(.external) > li > button { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - } - body > header nav > ul ul { - border-bottom: .375rem solid #888; - } - - body > header button.toggle { - display: none; - } - - body > footer { - clear: both; - } -} - -@media screen and (min-width: 60rem) { - form { - padding-left: 2rem; - padding-right: 2rem; - } - -/* main > section, - main :not(aside):not(nav) > section, - main aside ~ div > .magazine, main aside ~ div > .newspaper, main > nav ~ div > .magazine, main > nav ~ div > .newspaper { - padding-left: 6%; - padding-right: 6%; - } */ - main > aside { - padding-right: 0; - } - .magazine, - .newspaper { - column-gap: 4rem; - -moz-column-gap: 4rem; - -webkit-column-gap: 4rem; - } -} - -/* trimmable - useful for e.g. long key codes that do not get trimmed somewhere else */ -td.trimmable { - max-width: 4rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -td.data { - word-break: break-all; -} diff --git a/uweb3/scaffold/base/static/css/module.css b/uweb3/scaffold/base/static/css/module.css deleted file mode 100644 index bdb80f81..00000000 --- a/uweb3/scaffold/base/static/css/module.css +++ /dev/null @@ -1,710 +0,0 @@ -/* This is the Underdark module CSS. Its structure is based on SMACSS. See -https://smacss.com for more info. */ - -/* ssh key */ -textarea.sshkey, -pre.sshkey { - word-break: break-all; -} -textarea.sshkey { - font-family: monospace, monospace; -} - -/* table */ -td.number, -th.number { - text-align: right; -} -td.number input { - text-align: inherit; -} - -/* sortable */ -th.sortable > a, -th.ascending > a, -th.descending > a { - color: transparent; - display: inline-block; - width: 1.5rem; - height: 1.5rem; - white-space: nowrap; - overflow: hidden; - vertical-align: top; -} -th.sortable > a::before { - content: url('data:image/svg+xml,'); -} -th.ascending > a::before { - content: url('data:image/svg+xml,'); -} -th.descending > a::before { - content: url('data:image/svg+xml,'); -} - -/* pros and cons list */ -ul.pros, -ul.cons { - font-size: .9375rem; - color: #555; - line-height: 1.6875rem; - margin: 1rem 0; - padding: .5rem 1rem .5rem 2.5rem; - background-color: #f7f7f7; - list-style: none; -} -ul.pros > li, -ul.cons > li { - position: relative; - margin: .5rem 0; -} -ul.pros > li > .fa, -ul.cons > li > .fa { - font-size: 1rem; - position: absolute; - left: -1.5rem; - width: 1.125rem; - text-align: center; - line-height: 1.6875rem; -} - -/* pagination */ -nav.pagination { - text-align: center; -} -nav.pagination > ol { - padding: 0; -} -nav.pagination > ol > li { - display: inline-block; -} -/* nav.pagination > ol > li:not(:last-child) { - margin-right: .5rem; -} */ -nav.pagination > ol > li { - margin: .125rem 0; -} -nav.pagination > ol > li.active, -nav.pagination > ol > li > a { - padding: .25rem .75rem; -} -nav.pagination > ol > li > a { - display: block; - border: 1px solid rgb(0, 0, 0, .1); -} - -/* form */ -form.is-submitting [type="submit"], -form.is-submitting button:not([type]) { - opacity: .2; - cursor: default; -} - -/* toggle */ -input[type="checkbox"].toggle { - opacity: 0; -} -input[type="checkbox"].toggle + label { - text-indent: -9999em; - display: inline-block; - position: relative; - left: -1em; - width: 3em; - height: 1.5em; - border-radius: .75em; - background-color: rgb(235, 35, 23); - overflow: hidden; - user-select: none; - -ms-user-select: none; - -moz-user-select: none; - -webkit-user-drag: none; - -webkit-user-select: none; - transition: background-color .2s; - -webkit-transition: background-color .2s; -} -input[type="checkbox"].toggle:checked + label { - background-color: rgb(35, 235, 23); -} -input[type="checkbox"].toggle[disabled] + label { - opacity: .4; - cursor: not-allowed; -} -input[type="checkbox"].toggle + label::after { - content: ''; - position: absolute; - left: .125em; - top: .125em; - width: 1.25em; - height: 1.25em; - border-radius: 50%; - background-color: #fff; - transition: transform .2s; - -webkit-transition: -webkit-transform .2s; -} -input[type="checkbox"].toggle:checked + label::after { - transform: translateX(1.5em); - -webkit-transform: translateX(1.5em); -} -input[type="checkbox"].toggle:not([disabled]):hover + label::after, -input[type="checkbox"].toggle:not([disabled]):focus + label::after { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); - background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -input[type="checkbox"].toggle:active:not([disabled]):hover + label::after, -input[type="checkbox"].toggle:active:not([disabled]):focus + label::after { - background-color: #eee; -} - -/* editable */ -div.editable { - position: relative; - overflow: hidden; - transition: height .5s; - -webkit-transition: height .5s; -} -div.editable > section { - position: absolute; - left: 0; - top: 0; - width: 100%; - max-width: 100%; - box-sizing: border-box; - transition-duration: .5s; - -webkit-transition-duration: .5s; - transition-property: opacity, transform; - -webkit-transition-property: opacity, -webkit-transform; -} -div.editable.is-editing > section:first-child, -div.editable:not(.is-editing) > section:nth-child(2) { - opacity: 0; -} -div.editable.is-editing > section:first-child { - transform: translateX(-100%); - -webkit-transform: translateX(-100%); -} -div.editable:not(.is-editing) > section:nth-child(2) { - transform: translateX(100%); - -webkit-transform: translateX(100%); -} -div.editable > section button.edit { - font-size: .875rem; - /* position: absolute; - top: 1rem; - right: 5%; */ - float: right; - min-width: 4rem; - margin-top: 1rem; -} -div.editable > section button.view { - margin-top: .5rem; -} - -/* filter */ -div.filter { - text-align: right; -} - -/* search */ -body > header form.search > div:only-of-type > label, -body > footer form.search > div:only-of-type > label, -aside form.search > div:only-of-type > label, -nav form.search > div:only-of-type > label { - position: absolute; - visibility: hidden; -} -form.search > div > input { - flex-basis: auto; -} -/* search */ -/* TODO: Should be basic module style. Scoped to main for now */ -main form.search > div:only-of-type > input[type="search"] { - flex-basis: 100%; -} -main form.search > div:only-of-type > [type="submit"], -main form.search > div:only-of-type > .button { - min-width: 6rem; -} -main form.search > div:only-of-type > .button { - margin-left: .5rem; -} - -/* search in body header */ -body > header form.search > div:only-of-type > label { - position: static; /* reset */ -} - -@media screen and (min-width: 40rem) { - body > header > div > form.login + form.search { - margin-right: 1rem; - } -} - -/* form.search { - display: inline-block; - float: right; -} -main > div > section > header > form.search > div, -main > div > section > header > form.search > p { - display: inline-block; -} */ - -/* steps */ -ol.steps { - padding: 0; - list-style: none; -} -ol.steps > li { - display: inline-block; - position: relative; - margin: .125rem 0; - vertical-align: top; -} -ol.steps > li > label { - display: block; - position: relative; - margin: 0; /* TODO: Make default margin on labels way more specific and remove this */ - padding: 0 .5rem 0 1.75rem; - background-color: rgba(0, 0, 0, .1); -} -ol.steps > li:not(:last-child) { - margin-right: .5rem; -} - -ol.steps > li > input[type="checkbox"] { - position: absolute; - left: .5rem; - top: .3125rem; - z-index: 1; -} -ol.steps > li:not(:first-child) > input[type="checkbox"] + label::before, -ol.steps > li:not(:last-child) > input[type="checkbox"] + label::after { - position: absolute; - width: .5rem; - height: 1.5rem; -} -ol.steps > li:not(:first-child) > input[type="checkbox"] + label::before { - content: url('data:image/svg+xml,'); - left: -.5rem; - top: 0; -} -ol.steps > li:not(:last-child) > input[type="checkbox"] + label::after { - content: url('data:image/svg+xml,'); - right: -.5rem; - bottom: 0; -} - -ol.steps > li > input[type="checkbox"]:disabled, -ol.steps > li > input[type="checkbox"]:disabled + label { - position: absolute; - visibility: hidden; -} - -/* tabs */ -.tabs { - display: block !important; /* TODO: fix flexbox layout issues and selector specificity */ - border: 1px solid rgba(0, 0, 0, .05); - background-color: rgba(0, 0, 0, .05); -} -.tabs > input[type="radio"] { - display: none; -} - -.tabs > label { - float: left; - margin: 0 !important; /* TODO: fix selector specificity */ - padding: .5em 1em; - -webkit-transition: background-color .2s; - transition: background-color .2s; -} -.tabs > label:not(.is-active):focus, -.tabs > label:not(.is-active):hover { - background-color: rgba(255, 255, 255, .5); -} -.tabs > label.is-active { - color: inherit; - background-color: #fff; -} - -.tabs > div, -.tabs > section { - clear: left; /* label float */ - padding: 1em; - background-color: #fff; -} - -.tabs > input[type="radio"]:not(:checked) + div, -.tabs > input[type="radio"]:not(:checked) + section { - display: none; -} -/* tabs in table */ -table .tabs > label { - font-size: .875rem; -} -table .tabs > div, -table .tabs > section { - padding: .5em; -} - -/* toddler */ -main.toddler > section { - box-sizing: border-box; -} - - -@media screen and (min-width: 100rem) { - div.editable > section > button.edit { - right: 10%; - } -} - -/* modal */ -.modal { - display: flex; - align-items: center; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: 0 1rem; - z-index: 9999; - box-sizing: border-box; - -webkit-transition: opacity .2s; - transition: opacity .2s; -} -.modal label[for^="modaltoggle"] { - font-weight: bold; - text-align: center; - display: block; - margin: 0 -1rem; - padding: .5rem 1rem; - background-color: #eee; - -webkit-transition: background-color .1s; - transition: background-color .1s; -} -.modal label[for^="modaltoggle"]::before { - content: ''; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: rgba(0, 0, 0, .5); - z-index: -1; -} -.modal label[for^="modaltoggle"]::hover { - background-color: #ddd; -} -.modal > aside, -.modal > section { - max-width: 30rem; - margin: 0 auto; - padding: 0 1rem; - border-radius: .5rem; - background-color: #fff; - box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .2); - overflow: hidden; -} -.modal > aside::before, -.modal > section::before { - font-size: 1rem; - font-weight: bold; - color: #fff; - display: block; - margin: 0 -1rem; - padding: .75rem 1rem .75rem 3.25rem; - background-position: 1rem center; - background-repeat: no-repeat; -} -/* modal type */ -.modal.success > aside::before, -.modal.success > section::before { - content: 'Success'; - background-color: hsl(103, 44%, 49%); - background-image: url('data:image/svg+xml,'); -} -.modal.info > aside::before, -.modal.info > section::before { - content: 'Information'; - background-color: hsl(200, 65%, 51%); - background-image: url('data:image/svg+xml,'); -} -.modal.warning > aside::before, -.modal.warning > section::before { - content: 'Warning'; - background-color: hsl(50, 81%, 54%); - background-image: url('data:image/svg+xml,'); -} -.modal.error > aside::before, -.modal.error > section::before { - content: 'Error'; - background-color: hsl(0, 43%, 51%); - background-image: url('data:image/svg+xml,'); -} -[lang="nl"] .modal.success > aside::before, -[lang="nl"] .modal.success > section::before { - content: 'Gelukt'; -} -[lang="nl"] .modal.info > aside::before, -[lang="nl"] .modal.info > section::before { - content: 'Informatie'; -} -[lang="nl"] .modal.warning > aside::before, -[lang="nl"] .modal.warning > section::before { - content: 'Waarschuwing'; -} -[lang="nl"] .modal.error > aside::before, -[lang="nl"] .modal.error > section::before { - content: 'Mislukt'; -} -/* modal state */ -.modaltoggle:not(:checked) + .modal { - opacity: 0; - pointer-events: none; -} - -/* messages block (-like) */ -p.primary, -tr.primary td, -tr.primary th, -td.primary, -th.primary, -button.primary, -input.primary, -mark.primary { - color: #fff; - background-color: hsl(208, 56%, 46%); -} -p.success, -tr.success td, -tr.success th, -td.success, -th.success, -input.success, -mark.success { - background-color: hsl(103, 44%, 89%); -} -p.info, -tr.info td, -tr.info th, -td.info, -th.info, -input.info, -mark.info { - background-color: hsl(200, 65%, 91%); -} -p.warning, -tr.warning td, -tr.warning th, -td.warning, -th.warning, -input.warning, -mark.warning { - background-color: hsl(50, 81%, 94%); -} -p.error, -tr.error td, -tr.error th, -td.error, -th.error, -input.error, -mark.error { - background-color: hsl(0, 43%, 91%); -} -/* messages p */ -p.primary, -p.success, -p.info, -p.warning, -p.error { - padding: 1em; -} -p.success::before, -p.info::before, -p.warning::before, -p.error::before { - display: inline-block; - width: 1.25em; - height: 1.25em; - margin-right: .5em; - vertical-align: middle; -} -p.success::before { - content: url('data:image/svg+xml,'); -} -p.info::before { - content: url('data:image/svg+xml,'); -} -p.warning::before { - content: url('data:image/svg+xml,'); -} -p.error::before { - content: url('data:image/svg+xml,'); -} -/* messages button */ -button.success, -input[type="button"].success { - color: #fff; - background-color: hsl(103, 44%, 49%); -} -button.info, -input[type="button"].info { - color: #fff; - background-color: hsl(200, 65%, 51%); -} -button.warning, -input[type="button"].warning { - color: #fff; - background-color: hsl(50, 81%, 54%); -} -button.error, -input[type="button"].error { - color: #fff; - background-color: hsl(0, 43%, 51%); -} -/* messages inline other */ -label.primary, -dd.primary { - color: hsl(208, 56%, 46%); -} -label.success, -dd.success { - color: hsl(103, 44%, 49%); -} -label.info, -dd.info { - color: hsl(200, 65%, 51%); -} -label.warning, -dd.warning { - color: hsl(50, 81%, 54%); -} -label.error, -dd.error { - color: hsl(0, 43%, 51%); -} - -dl.details > dt { - font-weight: bold; -} -dl.details > dd { - margin: 0 0 .5rem; -} - -@media screen and (min-width: 20rem) { - dl.details::after { - /* clearfix */ - content: ''; - display: table; - clear: left; - } - /* NOTE: Floating dts won't work with long terms */ - dl.details > dt { - clear: both; - float: left; - width: 38%; - margin-bottom: .5rem; - padding-right: 1rem; - box-sizing: border-box; - } - dl.details > dd { - clear: right; - float: right; - width: 62%; - } -} - -/* dl.details > dd::before { - content: '– '; -} */ -/* -dl.details { - display: -webkit-flex; - display: flex; - -webkit-flex-wrap: wrap; - flex-wrap: wrap; -} -dl.details > dt { - font-weight: bold; - width: 38%; -} -dl.details > dd { - width: 62%; - margin-left: 0; -} -dl.details > dt + dd { -} -dl.details > dd + dd { - margin-left: 38%; - -} -*/ - -/* vars */ -dl.vars > dt { - float: left; -} -dl.vars > dt::after { - content: ': '; - margin-right: .5rem; /* add margin because float eats the space */ -} -dl.vars > dt var { - font-style: inherit; -} -dl.vars > dd { - margin: 0; - word-break: break-all; -} - -/* tree */ -ul.tree, -ul.tree ul, -ul.tree li { - position: relative; -} -ul.tree { - list-style: none; -} -ul.tree ul { - list-style: none; - padding-left: 32px; -} -ul.tree li::before, -ul.tree li::after { - content: ""; - position: absolute; - left: -12px; -} -ul.tree li::before { - border-top: 2px solid #9b9b9b; - top: 11px; - width: 8px; -} -ul.tree li::after { - border-left: 2px solid #9b9b9b; - height: 100%; - padding-top: .5rem; - top: -.25rem; -} -ul.tree ul > li:last-child::after { - height: 8px; -} -ul.tree > li:last-child::after, -ul.tree > li:last-child::before { - display: none; -} - -/* scrollable (used for overflowing table) */ -div.scrollable { - overflow-x: auto; -} -div.scrollable + div.scrollable { - margin-top: 1rem; -} -@media screen and (min-width: 30rem) { - form > div.scrollable + [type="submit"] { - margin-left: 0; /* reset */ - } -} diff --git a/uweb3/scaffold/base/static/css/theme.css b/uweb3/scaffold/base/static/css/theme.css deleted file mode 100644 index 26cd970c..00000000 --- a/uweb3/scaffold/base/static/css/theme.css +++ /dev/null @@ -1,206 +0,0 @@ -/* This is your theme file. Use the selectors used in the other layers or extend them. -See https://css.underdark.nl/docs for further explanation */ - -/* Colours */ -/* 0, 120, 231 */ - -/* Complimentary */ -/* 154, 96, 0 */ -/* 0, 80, 154 */ - -/* Triad */ -/* 15, 88, 154 */ -/* 235, 35, 23 */ - -/* 167, 180, 18 */ -/* 143, 154, 8 */ - -a { - color: hsl(208, 56%, 46%); - -webkit-transition: color .2s; - transition: color .2s; -} -/* a:not(:hover) { - text-decoration: none; -} */ - -/* lvha */ -a:link { -} -a:visited { - /* color: rgb(118, 78, 127); */ -} -a:hover { - color: hsl(208, 56%, 66%); -} -a:active { - /* color: rgb(235, 35, 23); */ -} - -pre { - color: #000; -} -:not(pre) > code { - color: #000; -} - -label { - color: #888; -} - -figure > em { - color: #ccc; -} - -/* body header and footer */ -body > header, -body > header nav > ul ul { - border-color: hsl(208, 56%, 46%); -} - -body > header a:focus, -body > header a:hover, -body > footer a:focus, -body > footer a:hover { - text-shadow: 0 0 1em hsl(208, 56%, 46%); -} - -/* depth header and footer links */ -body > header nav > ul:not(.external) a, -body > header nav > ul:not(.external) > li > button { - text-shadow: 0 -1px 1px rgba(0, 0, 0, .5); -} - -body > header nav > ul.external > li > a, -body > header .logo > a, -body > footer a { - text-decoration: none; - -webkit-transition-duration: .2s; - transition-duration: .2s; - -webkit-transition-property: color, text-shadow; - transition-property: color, text-shadow; -} -body > header nav > ul.external > li > a:focus, -body > header .logo > a:focus, -body > footer a:focus { - outline: 0; -} -body > header nav > ul:not(.external) > li > button::after { - text-shadow: 0 1px 0 rgba(0, 0, 0, .5); -} - -/* table */ -table a { - /* increase click area size */ - /* display: inline-block; */ -} -thead, -tfoot { - color: #000; -} -/* caption { - color: #000; -} */ - -/* form */ -form p { - font-size: .875rem; - color: #666; -} - -main aside, -main nav { - /* TODO: should be more specific. aside and navs should be normaly usable inside sectioning elements */ - /* font-size: .875rem; */ - color: #666; -} - -/* button */ -/* [type="submit"], */ -[type="button"].primary, -[type="button"].selected, -button.primary, -button.selected, -a.button.primary, -a.button.selected { - background-color: hsl(208, 56%, 46%); - color: #fff; -} -a.button:hover { - color: #000; /* reset */ - /* text-decoration: none; */ -} -a.button.primary:hover, -a.button.selected:hover { - color: #fff; /* reset */ -} - -.logo > a { - -webkit-transition-property: color, text-shadow; - transition-property: color, text-shadow; -} - -/* sticky footer */ -body { - display: -webkit-flex; - display: flex; - min-height: 100vh; - -webkit-flex-direction: column; - flex-direction: column; -} -body > main { - -webkit-flex: 1; - flex: 1; - width: 100%; - box-sizing: border-box; -} - -/* - * When a user agent cannot parse the selector (i.e., it is not valid CSS 2.1), - * it must ignore the selector and the following declaration block (if any) as well. - * See: http://stackoverflow.com/questions/20541306/how-to-write-a-css-hack-for-ie-11 - */ -@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { - /* Fix body collapsing in IE 11 when flexbox is used for sticky footer. */ - body { - display: block; /* reset */ - } -} - -/* pagination */ -nav.pagination > ol > li.first, -nav.pagination > ol > li.previous, -nav.pagination > ol > li.next, -nav.pagination > ol > li.last { - overflow: hidden; - white-space: nowrap; - vertical-align: bottom; /* overflow hidden changes vertical alignment */ -} -nav.pagination > ol > li.first > a, -nav.pagination > ol > li.previous > a, -nav.pagination > ol > li.next > a, -nav.pagination > ol > li.last > a { - color: transparent; - width: 1rem; -} -nav.pagination > ol > li.first > a::before, -nav.pagination > ol > li.previous > a::before, -nav.pagination > ol > li.next > a::before, -nav.pagination > ol > li.last > a::before { - display: inline-block; - width: 1rem; - height: 1.5rem; - vertical-align: top; -} -nav.pagination > ol > li.first > a::before { - content: url('data:image/svg+xml,'); -} -nav.pagination > ol > li.previous > a::before { - content: url('data:image/svg+xml,'); -} -nav.pagination > ol > li.next > a::before { - content: url('data:image/svg+xml,'); -} -nav.pagination > ol > li.last > a::before { - content: url('data:image/svg+xml,'); -} diff --git a/uweb3/scaffold/base/static/scripts/ajax.js b/uweb3/scaffold/base/static/scripts/ajax.js deleted file mode 100644 index 65f7fa35..00000000 --- a/uweb3/scaffold/base/static/scripts/ajax.js +++ /dev/null @@ -1,234 +0,0 @@ -var ud = ud || {}; - -ud.ajax = (function () { - 'use strict'; - var MAX_CONNECTIONS_PER_HOSTNAME = 6; - var request = { - id: 0, - type: 'update', - method: 'get', - url: '', - data: {}, - mimeType: 'application/json', - contentType: 'application/x-www-form-urlencoded; charset=UTF-8', - form: null, - hostname: '', - pollDelay: 3000, - xhr: {}, - - change: null, - success: null, - error: null, - - create: function (url, settings) { - var that = Object.create(this); - - that.url = url; - that.extend(settings); - that.setUpXhr(); - return that; - }, - - extend: function (props) { - for (var prop in props) { - if (props.hasOwnProperty(prop)) { - this[prop] = props[prop]; - } - } - }, - - setUpXhr: function () { - this.xhr = new window.XMLHttpRequest(); - if (this.xhr.overrideMimeType) { - this.xhr.overrideMimeType(this.mimeType); - } - this.xhr.onreadystatechange = this.handleReadyStateChange.bind(this); - }, - - send: function () { - var query = this.form ? this.getFormDataString() : this.getQueryString(); - var body = null; - - if (query) { - if (this.method === 'get') { - this.url += '?' + query; - } - if (this.method === 'post') { - body = query; - } - } - this.xhr.open(this.method, this.url, true); - this.xhr.setRequestHeader('Content-type', this.contentType); - this.xhr.setRequestHeader('X-CSRF-Token', csrf.token); - this.xhr.setRequestHeader('HTTP_X_REQUESTED_WITH', 'xmlhttprequest'); - this.xhr.send(body); - }, - - handleReadyStateChange: function () { - if (this.change) { - this.change(this); - } - if (this.xhr.readyState === 4) { - if(this.mimeType === 'application/json'){ - if (this.xhr.status === 200 && this.success) { - this.success(JSON.parse(this.xhr.responseText), this.url); - } else if (this.error) { - this.error(this.xhr); - } - }else{ - this.success(this.xhr.responseText, this.url) - } - if (this.type === 'update') { - this.remove(); - } - } - }, - - getHostname: function () { - var parser; - - if (!this.hostname) { - parser = document.createElement('a'); - parser.href = this.url; - this.hostname = parser.hostname; - } - return this.hostname; - }, - - getQueryString: function (data) { - var list = []; - - data = data || this.data; - for (var name in data) { - if (data[name] instanceof Array) { - data[name] = data[name].join(','); - } - list.push(window.encodeURIComponent(name) + '=' + - window.encodeURIComponent(data[name]) - ); - } - return list.join('&'); - }, - - getFormDataString: function () { - var els = this.form.elements; - var data = {}; - - for (var i = 0; i < els.length; i++) { - if (els[i].name) { - data[els[i].name] = els[i].value; - } - } - return this.getQueryString(data); - } - }; - - var overrides = { - active: {}, - - add: function (req) { - var active = this.active[req.url]; - - if (active && active.xhr.readyState !== 4) { - active.xhr.abort(); - } - req.send(); - this.active[req.url] = req; - } - }; - - var updates = { - active: {}, - queue: {}, - - add: function (req) { - var hostname = req.getHostname(); - var active = this.active[hostname] || []; - var queue = this.queue[hostname] || []; - - if (active.length < MAX_CONNECTIONS_PER_HOSTNAME) { - req.send(); - active.push(req); - } else { - queue.push(req); - } - req.remove = this.remove.bind(this, req); - this.active[hostname] = active; - this.queue[hostname] = queue; - }, - - remove: function (req) { - var hostname = req.getHostname(); - var active = this.active[hostname]; - var next = this.queue[hostname].shift(); - - if (next) { - next.send(); - active.push(next); - } - active.splice(active.indexOf(req), 1); - this.active[hostname] = active; - } - }; - - var polls = { - active: {}, - interval: {}, - - add: function (req) { - var interval = this.interval[req.url]; - - if (!interval) { - req.send(); - req.remove = this.remove.bind(this, req); - interval = window.setInterval(req.send.bind(req), req.pollDelay); - this.interval[req.url] = interval; - this.active[req.url] = req; - } - }, - - remove: function (req) { - var interval = this.interval[req.url]; - - window.clearInterval(interval); - delete this.active[req.url]; - } - }; - - var proxy = { - types: { - override: overrides, - update: updates, - poll: polls - }, - count: 0, - - add: function (req) { - req.id = ++this.count; - this.types[req.type].add(req); - } - }; - - var csrf = { - token: null, - - handle: function (success, data) { - this.token = data.token; - success(); - } - }; - - return function (url, settings) { - if (typeof url === 'object' && typeof settings === 'undefined') { - settings = url; - settings.method = settings.form.method; - url = settings.form.action; - } - if (url === '/csrf') { - settings.type = 'poll'; - settings.pollDelay = 1000 * 60 * 59; - settings.success = csrf.handle.bind(csrf, settings.success); - } - proxy.add(request.create(url, settings)); - }; -}()); diff --git a/uweb3/scaffold/base/static/scripts/uweb-dynamic.js b/uweb3/scaffold/base/static/scripts/uweb-dynamic.js deleted file mode 100644 index 38aeeb0f..00000000 --- a/uweb3/scaffold/base/static/scripts/uweb-dynamic.js +++ /dev/null @@ -1,176 +0,0 @@ -var ud = ud || {}; -var _paq = _paq || []; - - -class Page { - html = null - - constructor(page){ - this.page_hash = page[2].page_hash; - this.content_hash = page[2].content_hash; - this.template = page[2].template; - this.replacements = page[2].replacements; - } - -} - -(function () { - 'use strict'; - let i = 0; - let getRawTemplateRoute = "getrawtemplate"; - let cacheHandler = { - previous_key: null, - create: function(page){ - if(this.cacheSize() >= 5){ - this.delete(0); - } - window.localStorage.setItem(page.page_hash, - JSON.stringify({ - 'created': new Date().getTime(), - 'content_hash': page.content_hash, - 'replacements': page.replacements, - 'template': page.template, - 'html': page.html - })); - this.previous_key = page.page_hash; - return 200 - }, - insertHTML: function(html){ - if(this.previous_key){ - let storedPage = this.read(this.previous_key); - if(!storedPage.html){ - storedPage.html = html; - window.localStorage.setItem(this.previous_key, JSON.stringify(storedPage)); - } - return storedPage; - } - }, - read: function(page_hash){ - return JSON.parse(window.localStorage.getItem(page_hash)); - }, - delete: function(index){ - let key = window.localStorage.key(index); - let oldest_item = { - created: null, - key: null - } - if(index === 0){ - const items = { ...localStorage }; - for(let item in items){ - let current = this.read(item); - if(current.created < oldest_item.created || oldest_item.created == null){ - oldest_item.created = current.created; - oldest_item.key = item; - } - } - return window.localStorage.removeItem(oldest_item.key); - } - window.localStorage.removeItem(key); - }, - cacheSize: function(){ - return window.localStorage.length; - } - } - - function handleAnchors(){ - var anchors = document.getElementsByTagName('a'); - for(var i=0;i0){ - var data = {}; - fetchPage(path, data); - event.preventDefault(); - } - } - } - - function handleClick(event){ - if(event.target.tagName == 'A'){ - var path = localPart(event.target.href); - if(path.length>0){ - if(event.altKey){ - // TODO: delete this when done - path += `&variable=newContent${i}&variable2=moreContent${i}`; - }else{ - path += '&variable=test&variable2=moresamecontent'; - } - fetchPage(path); - event.preventDefault(); - } - } - } - - function localPart(url){ - if(url.startsWith(window.location.origin)){ - return url.substring(window.location.origin.length); - } - if(url.startsWith('//'+window.location.host)){ - return url.substring(window.location.host.length+2); - } - if(url.startsWith('/') && !url.startsWith('//')){ - return url; - } - return false; - } - - function fetchPage(url, data){ - ud.ajax(url, { success: handlePage }); - i++; - } - - function handlePage(data, url){ - // If the page is the same but the content is different we can retrieve the page from the hash and replace the placeholders with new values - // If the page is different we need to reload everything and update the cache - // Create a new instance of the page object. This only happens on the first call. - if(url.split('?').length >= 2){ - // console.log(url); - url = url.split('?')[1]; - } - if(typeof data === 'object'){ - const { content_hash, page_hash } = data[2]; - const cached = cacheHandler.read(page_hash); - if(cached){ - if(cached.content_hash === content_hash){ - // console.log(`Retrieving page from hash: ${page_hash} with content hash: ${content_hash}`); - let template = new Template(cached.html, cached.replacements); - document.querySelector('html').innerHTML = template.template; - }else{ - // console.log(`Retrieving page from hash: ${page_hash} with content hash: ${content_hash}`); - let template = new Template(cached.html, data[2].replacements); - document.querySelector('html').innerHTML = template.template; - } - }else{ - // If there is no cached page... - cacheHandler.create(new Page(data)); - // return ud.ajax(`/getrawtemplate?template=${data[2].template}&content_hash=${data[2].content_hash}`, {success: handlePage, mimeType: 'text/html'}); - return ud.ajax(`/${getRawTemplateRoute}?${url}&content_hash=${data[2].content_hash}`, {success: handlePage, mimeType: 'text/html'}); - - } - }else if(typeof data === 'string'){ - let html = cacheHandler.insertHTML(data); - let template = new Template(html.html, html.replacements); - document.querySelector('html').innerHTML = template.template; - } - handleAnchors(); - } - - function init(){ - handleAnchors(); - } - - init(); - -}()); diff --git a/uweb3/scaffold/base/static/scripts/uweb3-template-parser.js b/uweb3/scaffold/base/static/scripts/uweb3-template-parser.js deleted file mode 100644 index c5837844..00000000 --- a/uweb3/scaffold/base/static/scripts/uweb3-template-parser.js +++ /dev/null @@ -1,175 +0,0 @@ -class Template { - openScopes = []; - - get FUNCTION() { - return /\{\{\s*(.*?)\s*\}\}/mg; - } - - get TAG() { - return /(\[\w+(?:(?::[\w-]+)+)?(?:(?:\|[\w-]+(?:\([^()]*?\))?)+)?\])/gm; - } - - constructor(template, replacements){ - window.replacements = replacements; - this.inForLoop = false; - this.tmp = {}; - this.scopes = []; - this.AddString(template); - this.template = ""; - - for(let property in this.tmp){ - this.template += this.tmp[property].nodes; - } - for(let replacement in replacements){ - this.template = this.template.split(replacement).join(replacements[replacement]); - } - } - - AddString(template) { - let nodes = template.split(this.FUNCTION); - nodes.map((node, index) => { - let tmp_node = node.split(" "); - let func = tmp_node.shift(); - func = func.charAt(0).toUpperCase() + func.substring(1); - - if(index % 2){ - this._ExtendFunction(func, tmp_node, index); - }else{ - this._ExtendText(node, index) - } - }); - console.log(this.scopes); - console.log(this.tmp); - this._EvaluateScope(); - } - _EvaluateScope(){ - this.scopes.map((object, index) => { - console.log(object); - // let deleteScopes = false; - // for(let branch in object.branches){ - // if(!object.branches[branch].istrue){ - // //Delete all the scopes from which the condition was not met - // if(object.branches[branch].istrue !== undefined || deleteScopes){ - // //If the branch returns undefined its the last clause in the if statement so it will always be true - // delete this.tmp[object.branches[branch].index + 1]; - // } - // }else{ - // deleteScopes = true; - // } - // } - }); - } - _ExtendFunction(func, nodes, index) { - this[`_TemplateConstruct${func}`](nodes, index); - } - - _AddToOpenScope(item){ - // this.scopes[this.scopes.length - 1]; - } - - _StartScope(scope){ - if(this.openScopes.length > 0){ - this.openScopes[this.openScopes.length - 1].branches.push(scope); - }else{ - this.scopes.push(scope); - } - } - - _ExtendText(nodes, index){ - this.tmp[index] = { nodes: nodes }; - } - - _TemplateConstructIf(nodes, index){ - //Processing for {{ if }} template syntax - this._StartScope(new TemplateConditional(nodes.join(' '), index)); - } - - _TemplateConstructFor(nodes, index){ - let template = new TemplateLoop(nodes.join(' '), index); - if(this.openScopes.length > 0){ - this.openScopes.push(template); - }else{ - // this.scopes.push(template); - this.openScopes.push(template); - } - } - - _TemplateConstructEndfor(nodes, index){ - // this.openScopes[this.openScopes.length - 1].branches.push({index: index}); - this.scopes.push(this.openScopes[this.openScopes.length - 1]); - this.openScopes.pop(); - } - - _TemplateConstructElif(nodes, index){ - this.scopes[this.scopes.length - 1] = this.scopes[this.scopes.length - 1].Elif(index, nodes.join(' ')) - } - _TemplateConstructElse(nodes, index){ - //Processing for {{ else }} template syntax. - this.scopes[this.scopes.length - 1] = this.scopes[this.scopes.length - 1].Else(index) - } - - _TemplateConstructEndif(){ - //Processing for {{ endif }} template syntax. - // self._CloseScope(TemplateConditional) - } -} - -class TemplateConditional { - get TAG() { - return /(\[\w+(?:(?::[\w-]+)+)?(?:(?:\|[\w-]+(?:\([^()]*?\))?)+)?\])/gm; - } - - constructor(expr, index) { - this.branches = []; - this.NewBranch(expr, index); - } - - NewBranch(expr, index){ - let isTrue = this._EvaluateClause(expr); - this.branches.push({ index: index, expr: expr }); - } - - _EvaluateClause(expr){ - expr = expr.replace(" and ", " && "); - expr = expr.replace(" or ", " || "); - let temp_expr = "" - let variables = "" - if(expr.search(' in ') !== -1){ - throw "NotImplemented" - }else{ - expr.match(this.TAG).map((value) => { - //if the regex is a match it is a variable else its a variable function such as [variable|len] - let regex = new RegExp(/(\[\w+?\])/gm); - if(regex.test(value)){ - variables += `let ${value.substring(1, value.length - 1)} = "${window.replacements[value]}";`; - temp_expr = expr.split(value).join(value.substring(1, value.length - 1)); - }else{ - temp_expr = expr.split(value).join(window.replacements[value]); - } - }); - } - return Function(`${variables} if(${temp_expr}){return true}else{return false}`)() - } - - Elif(index, expr){ - let isTrue = this._EvaluateClause(expr); - this.branches.push({ index: index, expr: expr, istrue: isTrue }); - return this; - } - - Else(index){ - this.branches.push({ index: index }); - return this; - } -} - -class TemplateLoop { - constructor(expr, index) { - this.branches = []; - this.NewBranch(expr, index); - } - - NewBranch(expr, index){ - this.branches.push({ index: index, expr: expr}); - } -} \ No newline at end of file diff --git a/uweb3/scaffold/base/static/uweb3_template.zip b/uweb3/scaffold/base/static/uweb3_template.zip deleted file mode 100644 index 9b8c94c5..00000000 Binary files a/uweb3/scaffold/base/static/uweb3_template.zip and /dev/null differ diff --git a/uweb3/scaffold/base/templates/403.html b/uweb3/scaffold/base/templates/403.html deleted file mode 100644 index 2596fa68..00000000 --- a/uweb3/scaffold/base/templates/403.html +++ /dev/null @@ -1 +0,0 @@ -403 [error] \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/404.html b/uweb3/scaffold/base/templates/404.html deleted file mode 100644 index d2259e5f..00000000 --- a/uweb3/scaffold/base/templates/404.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - uWeb3 project scaffold - - -

This is not the page you're looking for (HTTP 404)

-

- The URL you requested ("[path]") doesn't exist. -

- - diff --git a/uweb3/scaffold/base/templates/footer.html b/uweb3/scaffold/base/templates/footer.html deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/base/templates/header.html b/uweb3/scaffold/base/templates/header.html deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/base/templates/index.html b/uweb3/scaffold/base/templates/index.html deleted file mode 100644 index d135bbbf..00000000 --- a/uweb3/scaffold/base/templates/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - - -
-
- - \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/sqlalchemy.html b/uweb3/scaffold/base/templates/sqlalchemy.html deleted file mode 100644 index 2181d0f1..00000000 --- a/uweb3/scaffold/base/templates/sqlalchemy.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - - -
-
-
-

Sqlalchemy

- Output is in the console. -
- - \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/test.html b/uweb3/scaffold/base/templates/test.html deleted file mode 100644 index a2834292..00000000 --- a/uweb3/scaffold/base/templates/test.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - -
-
-
-
-

Alt + click to generate new content

- -
- - {{if [variable]}} - {{if [variable]}} - [variable] - {{endif}} - {{if [variable|len] > '100'}} - [variable] - {{else}} - oi - {{endif}} - {{endif}} -

- {{ for item in [variable] }} [item] - {{endfor}} -
- - - - - - - \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/test2.html b/uweb3/scaffold/base/templates/test2.html deleted file mode 100644 index db537e67..00000000 --- a/uweb3/scaffold/base/templates/test2.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - -
-
-
-
-

Alt + click to generate new content

- -
- - {{ for item in [variable] }} - [item] - {{if [variable]}}test{{endif}} - {{for item in [variable2] }} - {{if [variable]}}test{{endif}} - test - {{endfor}} - {{endfor}} - - -
- - - - - - - diff --git a/uweb3/scaffold/nohup.out b/uweb3/scaffold/nohup.out deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/routes/__init__.py b/uweb3/scaffold/routes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/routes/socket_handler.py b/uweb3/scaffold/routes/socket_handler.py deleted file mode 100644 index 43820c5c..00000000 --- a/uweb3/scaffold/routes/socket_handler.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 -"""Request handlers for the uWeb3 project scaffold""" - -import uweb3 -from uweb3 import PageMaker - -class SocketHandler(PageMaker): - """Holds all the request handlers for the application""" - - def EventHandler(sid, msg): - # print(sid, msg) - print("hello world from sockethandler") - - def Connect(sid, env): - print(sid, env) \ No newline at end of file diff --git a/uweb3/scaffold/routes/sqlalchemy.py b/uweb3/scaffold/routes/sqlalchemy.py deleted file mode 100644 index e41c405d..00000000 --- a/uweb3/scaffold/routes/sqlalchemy.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/python3 -"""Request handlers for the uWeb3 project scaffold""" - -from uweb3 import SqAlchemyPageMaker -from uweb3.alchemy_model import AlchemyRecord -from uweb3.pagemaker.new_login import Users, UserCookie, Test -from uweb3.pagemaker.new_decorators import checkxsrf - -from sqlalchemy import Column, Integer, String, update, MetaData, Table, ForeignKey, inspect -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship, lazyload - -Base = declarative_base() - -class User(AlchemyRecord, Base): - __tablename__ = 'alchemy_users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) - authorid = Column('authorid', Integer, ForeignKey('author.id')) - children = relationship("Author", lazy="select") - - - def __init__(self, *args, **kwargs): - super(User, self).__init__(*args, **kwargs) - -class Author(AlchemyRecord, Base): - __tablename__ = 'author' - - id = Column(Integer, primary_key=True) - name = Column(String, unique=True) - personid = Column('personid', Integer, ForeignKey('persons.id')) - children = relationship("Persons", lazy="select") - - -class Persons(AlchemyRecord, Base): - __tablename__ = 'persons' - - id = Column(Integer) - name = Column(String, primary_key=True) - - -def buildTables(connection, session): - meta = MetaData() - Table( - 'alchemy_users', meta, - Column('id', Integer, primary_key=True), - Column('username', String(255), nullable=False, unique=True), - Column('password', String(255), nullable=False), - Column('authorid', Integer, ForeignKey('author.id')), - ) - Table( - 'author', meta, - Column('id', Integer, primary_key=True), - Column('name', String(32), nullable=False), - Column('personid', Integer, ForeignKey('persons.id')) - ) - Table( - 'persons', meta, - Column('id', Integer,primary_key=True), - Column('name', String(32), nullable=False) - ) - - meta.create_all(connection) - - Persons.Create(session, {'name': 'Person name'}) - Author.Create(session, {'name': 'Author name', 'personid': 1}) - Author.Create(session, {'name': 'Author number 2', 'personid': 1}) - User.Create(session, {'username': 'name', 'password': 'test', 'authorid': 1}) - - -class UserPageMaker(SqAlchemyPageMaker): - """Holds all the request handlers for the application""" - - def Sqlalchemy(self): - """Returns the index template""" - tables = inspect(self.engine).get_table_names() - if not 'alchemy_users' in tables or not 'author' in tables or not 'persons' in tables: - buildTables(self.engine, self.session) - - user = User.FromPrimary(self.session, 1) - # print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - # print("Returns user with primary key 1: ", user) - # print("Will only load the children when we ask for them: ", user.children) - # print("Conditional list, lists users with id < 10: ", list(User.List(self.session, conditions=[User.id <= 10]))) - print("List item 0: ", list(User.List(self.session, conditions=[User.id <= 10]))[0]) - # print("List item 0.children: ", list(User.List(self.session, conditions=[User.id <= 10]))[0].children) - - # User.Update(self.session, [User.id > 2, User.id < 100], {User.username: 'username', User.password: 'password'}) - # print("User from primary key", user) - # user.Delete() - # print(user.children) - # print("deleted", User.DeletePrimary(self.session, user.key)) - # print(User.List(self.session, conditions=[User.id >= 1, User.id <= 10])) - # print(user) - # print("FromPrimary: ", user) - # print(self.session.query(Persons, Author).join(Author).filter().all()) - # user.username = f'USERNAME{result.id}' - # user.Save() - # user.author.name = f'AUTHOR{result.id}' - # user.author.Save() - # print("EditedUser", user) - # user_list = list(User.List(self.session, order=(User.id.desc(), User.username.asc()))) - # print("DeletePrimary: ", User.DeletePrimary(self.session, result.id)) - # print('---------------------------------------------------------------------------') - return self.parser.Parse('sqlalchemy.html') diff --git a/uweb3/scaffold/routes/test.py b/uweb3/scaffold/routes/test.py deleted file mode 100644 index def875a0..00000000 --- a/uweb3/scaffold/routes/test.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python3 -"""Request handlers for the uWeb3 project scaffold""" - -import uweb3 -import json -from uweb3 import PageMaker -from uweb3.pagemaker.new_login import UserCookie -from uweb3.pagemaker.new_decorators import loggedin, checkxsrf -from uweb3.ext_lib.underdark.libs.safestring import SQLSAFE, HTMLsafestring -from uweb3.model import SettingsManager - -class Test(PageMaker): - """Holds all the request handlers for the application""" - - @staticmethod - def Limit(length=80): - """Returns a closure that limits input to a number of chars/elements.""" - return lambda string: string[:length] - - def Test(self): - """Returns the index template""" - self.parser.RegisterFunction('substr', self.Limit) - return self.parser.Parse('test.html', variable='test') - - def GetRawTemplate(self): - """Endpoint that only returns the raw template""" - template = self.get.getfirst('template') - content_hash = self.get.getfirst('content_hash') - if not template or not content_hash: - return 404 - del self.get['template'] - del self.get['content_hash'] - kwds = {} - for item in self.get: - kwds[item] = self.get.getfirst(item) - content = self.parser.Parse(template, returnRawTemplate=True, **kwds) - if content.content_hash == content_hash: - return content - return 404 - - def Parsed(self): - self.parser.RegisterFunction('substr', self.Limit) - kwds = {} - template = self.get.getfirst('template') - del self.get['template'] - for item in self.get: - kwds[item] = self.get.getfirst(item) - try: - self.parser.noparse = True - content = self.parser.Parse( - template, **kwds) - finally: - self.parser.noparse = False - return json.dumps(((self.req.headers.get('http_x_requested_with', None), self.parser.noparse, content))) - - def StringEscaping(self): - if self.post: - result = SQLSAFE(self.post.getfirst('sql'), self.post.getfirst('value1'), self.post.getfirst('value2'), unsafe=True) - t = f"""t = 't"''""" - print(result + t) - return self.req.Redirect('/test') \ No newline at end of file diff --git a/uweb3/scaffold/serve.py b/uweb3/scaffold/serve.py deleted file mode 100644 index 46616907..00000000 --- a/uweb3/scaffold/serve.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Starts a simple application development server.""" - -# Application -import base -import socketio -from uweb3.sockets import Uweb3SocketIO - -def websocket_routes(sio): - @sio.on("connect") - def test(sid, env): - print("WEBSOCKET ROUTE CALLED: ", sid, env) - -def main(): - sio = socketio.Server() - # websocket_routes(sio) - return sio - -if __name__ == '__main__': - sio = main() - Uweb3SocketIO(base.main(sio), sio) - - -# # Application -# import base - -# def main(): -# app = base.main() -# app.serve() - -# if __name__ == '__main__': -# main() \ No newline at end of file diff --git a/uweb3/scaffold/uweb3_uncaught_exceptions.log b/uweb3/scaffold/uweb3_uncaught_exceptions.log deleted file mode 100644 index a3a81ae1..00000000 --- a/uweb3/scaffold/uweb3_uncaught_exceptions.log +++ /dev/null @@ -1,3296 +0,0 @@ -ERROR: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 141, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 15, in Login - raise Exception("TEST") -Exception: TEST -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 19, in Index - q() -NameError: name 'q' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 19, in Index - q() -NameError: name 'q' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 19, in Index - q() -NameError: name 'q' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 15, in magic - return f() -TypeError: Index() missing 1 required positional argument: 'self' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 15, in magic - return f() -TypeError: Index() missing 1 required positional argument: 'self' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 15, in magic - return f() -TypeError: Index() missing 1 required positional argument: 'self' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 229, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute 'StringEscaping' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 228, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute 'StringEscaping' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 228, in get_response - method, args, hostargs = self.router(path, method, host) - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 116, in request_router - for pattern, handler, routemethod, hostpattern in req_routes: -ValueError: too many values to unpack (expected 4) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 239, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 239, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 239, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - method, args, hostargs = self.router(path, method, host) -ValueError: too many values to unpack (expected 3) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception("test") -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception("test") -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception("test") -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -MySQLdb._exceptions.IntegrityError: (1062, "Duplicate entry 'hello' for key 'username'") - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 83, in Sqlalchemy - print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1543, in Create - return cls(session, record) - File "", line 4, in __init__ - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance - manager.dispatch.init_failure(self, args, kwargs) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance - return manager.original_init(*mixed[1:], **kwargs) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 26, in __init__ - super(User, self).__init__(*args, **kwargs) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1302, in __init__ - self._BuildClassFromRecord(record) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1317, in _BuildClassFromRecord - self.session.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1036, in commit - self.transaction.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 503, in commit - self._prepare_impl() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 482, in _prepare_impl - self.session.flush() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2496, in flush - self._flush(objects) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2637, in _flush - transaction.rollback(_capture_exception=True) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush - flush_context.execute() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute - rec.execute(self) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute - uow, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 245, in save_obj - insert, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 1136, in _emit_insert_statements - statement, params - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 984, in execute - return meth(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection - return connection._execute_clauseelement(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1103, in _execute_clauseelement - distilled_params, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1288, in _execute_context - e, statement, parameters, cursor, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1482, in _handle_dbapi_exception - sqlalchemy_exception, with_traceback=exc_info[2], from_=e - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -sqlalchemy.exc.IntegrityError: (MySQLdb._exceptions.IntegrityError) (1062, "Duplicate entry 'hello' for key 'username'") -[SQL: INSERT INTO alchemy_users (username, password, authorid) VALUES (%s, %s, %s)] -[parameters: ('hello', 'test', 1)] -(Background on this error at: http://sqlalche.me/e/gkpj) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -MySQLdb._exceptions.IntegrityError: (1062, "Duplicate entry 'hello' for key 'username'") - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 83, in Sqlalchemy - print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1543, in Create - return cls(session, record) - File "", line 4, in __init__ - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance - manager.dispatch.init_failure(self, args, kwargs) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance - return manager.original_init(*mixed[1:], **kwargs) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 26, in __init__ - super(User, self).__init__(*args, **kwargs) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1302, in __init__ - self._BuildClassFromRecord(record) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1317, in _BuildClassFromRecord - self.session.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1036, in commit - self.transaction.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 503, in commit - self._prepare_impl() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 482, in _prepare_impl - self.session.flush() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2496, in flush - self._flush(objects) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2637, in _flush - transaction.rollback(_capture_exception=True) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush - flush_context.execute() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute - rec.execute(self) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute - uow, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 245, in save_obj - insert, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 1136, in _emit_insert_statements - statement, params - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 984, in execute - return meth(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection - return connection._execute_clauseelement(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1103, in _execute_clauseelement - distilled_params, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1288, in _execute_context - e, statement, parameters, cursor, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1482, in _handle_dbapi_exception - sqlalchemy_exception, with_traceback=exc_info[2], from_=e - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -sqlalchemy.exc.IntegrityError: (MySQLdb._exceptions.IntegrityError) (1062, "Duplicate entry 'hello' for key 'username'") -[SQL: INSERT INTO alchemy_users (username, password, authorid) VALUES (%s, %s, %s)] -[parameters: ('hello', 'test', 1)] -(Background on this error at: http://sqlalche.me/e/gkpj) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -MySQLdb._exceptions.IntegrityError: (1062, "Duplicate entry 'hello' for key 'username'") - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 83, in Sqlalchemy - print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1543, in Create - return cls(session, record) - File "", line 4, in __init__ - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance - manager.dispatch.init_failure(self, args, kwargs) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance - return manager.original_init(*mixed[1:], **kwargs) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 26, in __init__ - super(User, self).__init__(*args, **kwargs) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1302, in __init__ - self._BuildClassFromRecord(record) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1317, in _BuildClassFromRecord - self.session.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1036, in commit - self.transaction.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 503, in commit - self._prepare_impl() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 482, in _prepare_impl - self.session.flush() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2496, in flush - self._flush(objects) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2637, in _flush - transaction.rollback(_capture_exception=True) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush - flush_context.execute() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute - rec.execute(self) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute - uow, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 245, in save_obj - insert, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 1136, in _emit_insert_statements - statement, params - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 984, in execute - return meth(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection - return connection._execute_clauseelement(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1103, in _execute_clauseelement - distilled_params, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1288, in _execute_context - e, statement, parameters, cursor, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1482, in _handle_dbapi_exception - sqlalchemy_exception, with_traceback=exc_info[2], from_=e - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -sqlalchemy.exc.IntegrityError: (MySQLdb._exceptions.IntegrityError) (1062, "Duplicate entry 'hello' for key 'username'") -[SQL: INSERT INTO alchemy_users (username, password, authorid) VALUES (%s, %s, %s)] -[parameters: ('hello', 'test', 1)] -(Background on this error at: http://sqlalche.me/e/gkpj) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception diff --git a/uweb3/scripts/__init__.py b/uweb3/scripts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scripts/tables.py b/uweb3/scripts/tables.py deleted file mode 100644 index f9b0a2d1..00000000 --- a/uweb3/scripts/tables.py +++ /dev/null @@ -1,45 +0,0 @@ -# Originally from: http://code.activestate.com/recipes/577202/#c4 -# Written by Vasilij Pupkin (2012) -# Minor changes by Elmer de Looff (2012) -# Licensed under the MIT License (http://opensource.org/licenses/MIT - - -class ALIGN(object): - LEFT, RIGHT = '-', '' - -class Column(list): - def __init__(self, name, data, align=ALIGN.LEFT): - list.__init__(self, data) - self.name = name - self.width = max(len(x) for x in self + [name]) - self.format = ' %%%s%ds ' % (align, self.width) - -class Table(object): - def __init__(self, *columns): - self.columns = columns - self.length = max(len(x) for x in columns) - - def get_row(self, i=None): - for x in self.columns: - if i is None: - yield x.format % x.name - else: - yield x.format % x[i] - - def get_line(self): - for x in self.columns: - yield '-' * (x.width + 2) - - def join_n_wrap(self, char, elements): - return ' ' + char + char.join(elements) + char - - def get_rows(self): - yield self.join_n_wrap('+', self.get_line()) - yield self.join_n_wrap('|', self.get_row(None)) - yield self.join_n_wrap('+', self.get_line()) - for i in range(0, self.length): - yield self.join_n_wrap('|', self.get_row(i)) - yield self.join_n_wrap('+', self.get_line()) - - def __str__(self): - return '\n'.join(self.get_rows()) diff --git a/uweb3/scripts/uweb b/uweb3/scripts/uweb deleted file mode 100755 index 464da648..00000000 --- a/uweb3/scripts/uweb +++ /dev/null @@ -1,386 +0,0 @@ -#!/usr/bin/python -"""uWeb development server management script""" - -import os -import shutil -import simplejson -import sys -import logging -import subprocess -from optparse import OptionParser - -# Application specific modules -import uweb -from uweb.scripts import tables - - -class Error(Exception): - """Base class for application errors.""" - - -class UwebSites(object): - """Abstraction for the uWeb site managing JSON file.""" - SITES_BASE = {'uweb_info': {'router': 'uweb.uweb_info.router.uweb_info', - 'workdir': '/'}, - 'logviewer': {'router': 'uweb.logviewer.router.logging', - 'workdir': '/'}} - - def __init__(self): - self.sites_file = os.path.expanduser('~/.uweb/sites.json') - self.sites = self._LoadSites() - - def _InstallBaseSites(self): - """Create sites file with default data, and directory where necessary.""" - dirname = os.path.dirname(self.sites_file) - if not os.path.isdir(dirname): - print '.. no uweb data directory; creating %r' % dirname - os.mkdir(os.path.dirname(self.sites_file)) - with file(self.sites_file, 'w') as sites: - print '.. creating %r with default sites' % self.sites_file - sites.write(simplejson.dumps(self.SITES_BASE)) - print '' - - def _LoadSites(self): - """Load the sites file and return parsed JSON.""" - if not os.path.exists(self.sites_file): - self._InstallBaseSites() - with file(self.sites_file) as sites: - try: - return simplejson.loads(sites.read()) - except simplejson.JSONDecodeError: - raise Error('Could not read %r: Illegal JSON syntax' % self.sites_file) - - def _WriteSites(self): - """Write a new sites file after changes were made.""" - with file(self.sites_file, 'w') as sites: - sites.write(simplejson.dumps(self.sites)) - - def __contains__(self, key): - return key in self.sites - - def __iter__(self): - return iter(sorted(self.sites.items())) - - def __nonzero__(self): - return bool(self.sites) - - def __getitem__(self, name): - return self.sites[name] - - def __setitem__(self, name, router): - self.sites[name] = router - self._WriteSites() - - def __delitem__(self, name): - if name not in self.sites: - raise ValueError('There is no site with name %r' % name) - del self.sites[name] - self._WriteSites() - - -class BaseOperation(object): - """A simple class which parses command line values and call's it self.""" - def ParseCall(self): - """Base method to parse arguments and options.""" - raise NotImplementedError - - @staticmethod - def Banner(message): - line = '-' * 62 - return '+%s+\n| %-60s |\n+%s+' % (line, message[:60], line) - - def Run(self): - """Default method to parse arguments/options and activate class""" - opts, args = self.ParseCall() - self(*args[1:], **opts) - - def __call__(self, *args, **kwds): - """Base method to activate class""" - raise NotImplementedError - - -# ############################################################################## -# Initialization of and Apache configuration for projects -# -class Init(BaseOperation): - """Initialize uweb generator which create new uweb instance""" - # Base directory where the uWeb library lives - ROUTER_PATH = 'router' - ROUTER_NAME = 'router.py' - APACHE_CONFIG_NAME = 'apache.conf' - - def ParseCall(self): - parser = OptionParser(add_help_option=False) - parser.add_option('-f', '--force', action='store_true', - default=False, dest='force') - parser.add_option('-h', '--host', action='store', dest='host') - parser.add_option('-p', '--path', action='store', - default=os.getcwd(), dest='path') - parser.add_option('-s', '--silent', action='store_true', - default=False, dest='silent') - - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, name=None, force=False, path=None, silent=False, - host='uweb.local'): - if name is None: - raise Error('Initialization requires a project name.') - project_path = os.path.abspath(name) - source_path = os.path.dirname('%s/base_project/' % uweb.__path__[0]) - apache_path = os.path.join(project_path, self.APACHE_CONFIG_NAME) - - print self.Banner('initializing new uWeb project %r' % name) - if os.path.exists(project_path): - if force: - print '* Removing existing project directory' - shutil.rmtree(project_path) - else: - raise Error('Target already exists, use -f (force) to overwrite.') - print '* copying uWeb base project directory' - shutil.copytree(source_path, project_path) - print '* setting up router' - # Rename default name 'router' to that of the project. - shutil.move( - os.path.join(project_path, self.ROUTER_PATH, self.ROUTER_NAME), - os.path.join(project_path, self.ROUTER_PATH, '%s.py' % name)) - - print '* setting up apache config' - GenerateApacheConfig.WriteApacheConfig( - name, host, apache_path, project_path) - - # Make sure we add the project to the sites list - sites = UwebSites() - sites[name] = {'router': '%s.router.%s' % (name, name), - 'workdir': os.getcwd()} - print self.Banner('initialization complete - have fun with uWeb') - - -class GenerateApacheConfig(BaseOperation): - """Generate apache config file for uweb project""" - def ParseCall(self): - parser = OptionParser(add_help_option=True) - parser.add_option('-n', - '--name', - action='store', - default='uweb_project', - dest='name') - - parser.add_option('-p', - '--path', - action='store', - default=os.getcwd(), - dest='path') - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, name, host, path): - """Returns apache config string based on arguments""" - return ('\n' - ' documentroot %(path)s\n' - ' servername %(host)s\n' - '\n\n' - '\n' - ' SetHandler mod_python\n' - ' PythonHandler %(name)s\n' - ' PythonAutoReload on\n' - ' PythonDebug on\n' - '') % {'path': path, 'name': name, 'host': host} - - @staticmethod - def WriteApacheConfig(name, host, apache_config_path, project_path): - """write apache config file""" - with open(apache_config_path, 'w') as apache_file: - string = GenerateApacheConfig()(name, host, project_path) - apache_file.write(string) - - -# ############################################################################## -# Commands to manage configured uWeb sites. -# -class ListSites(BaseOperation): - """Print available uweb sites.""" - def ParseCall(self): - return {}, () - - def __call__(self, *args): - sites = UwebSites() - if not sites: - raise Error('No configured uWeb sites.') - print 'Overview of active sites:\n' - configs = [(name, site['router'], site['workdir']) for name, site in sites] - names, routers, dirs = zip(*configs) - print tables.Table(tables.Column('Name', names), - tables.Column('Router', routers), - tables.Column('Working dir', dirs)) - - -class Add(BaseOperation): - """Register uweb site""" - def ParseCall(self): - parser = OptionParser() - parser.add_option('-d', '--directory', action='store', - default='/', dest='directory') - parser.add_option('-u', '--update', action='store_true', - default=False, dest='update') - - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *name_router, **opts): - if len(name_router) != 2: - sys.exit(self.Help()) - sites = UwebSites() - name, router = name_router - directory = opts.get('directory', '/') - update = opts.get('update', False) - if name in sites and not update: - raise Error('Could not add a router with this name, one already exists.' - '\n\nTo update the existing, use the --update flag') - sites[name] = {'router': router, 'workdir': os.path.expanduser(directory)} - - def Help(self): - return ('Please provide a name and the module path for the router.\n' - 'Example: uweb add cookie_api cookies.router.api ' - '--directory="~/devel".') - - -class Remove(BaseOperation): - """Unregister uweb site""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - try: - sites = UwebSites() - del sites[args[0]] - except ValueError: - raise Error('There was no site named %r' % args[0]) - - def Help(self): - return ('Please provide a name for the router to remove.\n' - 'Router names can be retrieved using the "list" command.') - - -# ############################################################################## -# Commands to control configured uWeb routers. -# -class Start(BaseOperation): - """Start project router""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - site = UwebSites()[args[0]] - return subprocess.Popen(['python', '-m', site['router'], 'start'], - cwd=site['workdir']).wait() - - def Help(self): - return ('Please provide a name for the router to start.\n' - 'Router names can be retrieved using the "list" command.') - - -class Stop(BaseOperation): - """Stop project router""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - site = UwebSites()[args[0]] - return subprocess.Popen(['python', '-m', site['router'], 'stop'], - cwd=site['workdir']).wait() - - def Help(self): - return ('Please provide a name for the router to stop.\n' - 'Router names can be retrieved using the "list" command.') - - -class Restart(BaseOperation): - """Restart project router""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - site = UwebSites()[args[0]] - return subprocess.Popen(['python', '-m', site['router'], 'restart'], - cwd=site['workdir']).wait() - - def Help(self): - return ('Please provide a name for the router to restart.\n' - 'Router names can be retrieved using the "list" command.') - - -FUNCTIONS = {'init': Init, - 'genconf': GenerateApacheConfig, - 'list': ListSites, - 'add': Add, - 'remove': Remove, - 'start': Start, - 'restart': Restart, - 'stop': Stop} - - -def LongestImportPrefix(package): - candidates = [] - for path in sys.path: - if package.startswith(path + os.sep): - candidates.append(path) - print max(candidates, key=len) - - -def Help(): - return """uWeb management tool. - - Usage: `uweb COMMAND [options]` - - Project - init - Starts a new uWeb project with the given name - genconf - Generates an Apache configuration file - - Router management commands: - list - Lists all uWeb projects, their routers and working directories. - add - Adds a new project to the managed routers. - remove - Removes a project from the managed routers. - - Router control commands: - start - Starts a named router (as created with 'add'). - stop - Stops a named router (as created with 'add'). - restart - Convenience command to stop, and then start a router. - """ - - -def main(): - """Main uweb method""" - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - root_logger.addHandler(handler) - - if len(sys.argv) < 2 or sys.argv[1] not in FUNCTIONS: - print Help() - sys.exit(1) - try: - FUNCTIONS[sys.argv[1]]().Run() - except Error, err_obj: - sys.exit('Error: %s' % err_obj) - except (IOError, OSError), err_obj: - sys.exit('I/O Error: %s' % err_obj) - -if __name__ == '__main__': - main() diff --git a/uweb3/sockets.py b/uweb3/sockets.py deleted file mode 100644 index 68624e39..00000000 --- a/uweb3/sockets.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import sys - -import socketio -import eventlet - -from uweb3 import uWeb, HotReload -from uweb3.helpers import StaticMiddleware - - -class SocketMiddleWare(socketio.WSGIApp): - def __init__(self, socketio_server, uweb3_server, socketio_path='socket.io'): - super(SocketMiddleWare, self).__init__(socketio_server, - uweb3_server, - socketio_path=socketio_path - ) - -class Uweb3SocketIO(object): - def __init__(self, app, sio, static_dir=os.path.dirname(os.path.abspath(__file__))): - if not isinstance(app, uWeb): - raise Exception("App must be an uWeb3 instance!") - - self.host = app.config.options['development'].get('host', '127.0.0.1') - self.port = app.config.options['development'].get('port', 8000) - if app.config.options['development'].get('dev', False) == 'True': - HotReload(app.executing_path, uweb_dev=app.config.options['development'].get('uweb_dev', 'False')) - self.setup_app(app, sio, static_dir) - - - def setup_app(self, app, sio, static_dir): - path = os.path.join(app.executing_path, 'static') - app = SocketMiddleWare(sio, app) - static_directory = [os.path.join(sys.path[0], path)] - app = StaticMiddleware(app, static_root='static', static_dirs=static_directory) - eventlet.wsgi.server(eventlet.listen((self.host, int(self.port))), app) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 9d245e37..55c904a9 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """uWeb TemplateParser Classes: @@ -11,15 +11,16 @@ """ __author__ = ('Elmer de Looff ', 'Jan Klopper ') -__version__ = '1.6' +__version__ = '1.7' # Standard modules import os import re import urllib.parse as urlparse -from .ext_lib.underdark.libs.safestring import * +from .libs.safestring import * import hashlib import itertools +import ast, math class Error(Exception): """Superclass used for inheritance and external exception handling.""" @@ -30,7 +31,11 @@ class TemplateKeyError(Error): class TemplateNameError(Error): - """The referenced tag or function does not exist.""" + """The referenced tag does not exist.""" + + +class TemplateFunctionError(Error): + """The referenced function does not exist.""" class TemplateValueError(Error, ValueError): @@ -49,7 +54,11 @@ class TemplateReadError(Error, IOError): """Template file could not be read or found.""" -class LazyTagValueRetrieval(object): +class TemplateEvaluationError(Error): + """Template condition was not within allowed set of operators.""" + + +class LazyTagValueRetrieval: """Provides a means for lazy tag value retrieval. This is necessary for instance for TemplateConditional.Expression, where @@ -100,6 +109,19 @@ def values(self): return list(self.itervalues()) +EVALWHITELIST = { + 'functions': {"abs": abs, "complex": complex, "min": min, "max": max, + "pow": pow, "round": round, "len": len, "type": type, + "isinstance": isinstance, "list": list, + **{key: value for (key,value) in vars(math).items() if not key.startswith('__')}}, + 'operators': (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.And, + ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp, ast.Mult, ast.Gt, ast.GtE, + ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.Lt, ast.LtE, + ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod, ast.In, ast.LShift, + ast.RShift, ast.Invert, ast.Call, ast.Name, ast.Compare, + ast.Eq, ast.NotEq, ast.Not, ast.Or, ast.BoolOp, ast.Str)} + + class Parser(dict): """A template parser that loads and caches templates and parses them by name. @@ -122,7 +144,7 @@ class Parser(dict): providing the `RegisterFunction` method to add or replace functions in this module constant. """ - def __init__(self, path='.', templates=(), noparse=False): + def __init__(self, path=None, templates=(), dictoutput=False, templateEncoding='utf-8'): """Initializes a Parser instance. This sets up the template directory and preloads any templates given. @@ -132,13 +154,19 @@ def __init__(self, path='.', templates=(), noparse=False): Search path for loading templates using AddTemplate(). % templates: iter of str ~~ None Names of templates to preload. - % noparse: Bool ~~ False + % dictoutput: Bool ~~ False Skip parsing the templates to output, instead return their - structure and replaced values + structure and replaced values as a dict + % templateEncoding: str ~~ utf-8 + Encoding of the template, used when reading the file. """ - super(Parser, self).__init__() + super().__init__() self.template_dir = path - self.noparse = noparse + self.dictoutput = dictoutput + self.tags = {} + self.requesttags = {} + self.astvisitor = AstVisitor(EVALWHITELIST) + self.templateEncoding = templateEncoding for template in templates: self.AddTemplate(template) @@ -161,7 +189,7 @@ def __getitem__(self, template): """ if template not in self: self.AddTemplate(template) - return super(Parser, self).__getitem__(template) + return super().__getitem__(template) def AddTemplate(self, location, name=None): """Reads the given `template` filename and adds it to the cache. @@ -180,9 +208,14 @@ def AddTemplate(self, location, name=None): Raises: TemplateReadError: When the template file cannot be read """ + if self.template_dir: + template_path = os.path.realpath(os.path.join(self.template_dir, location)) + if os.path.commonprefix((template_path, self.template_dir)) != self.template_dir: + raise TemplateReadError('Could not load template %r, not in template dir' % template_path) + else: + template_path = location try: - template_path = os.path.join(self.template_dir, location) - self[name or location] = FileTemplate(template_path, parser=self) + self[name or location] = FileTemplate(template_path, parser=self, encoding=None) except IOError: raise TemplateReadError('Could not load template %r' % template_path) @@ -201,7 +234,11 @@ def Parse(self, template, **replacements): Returns: str: The template with relevant tags replaced by the replacement dict. """ - return self[template].Parse(**replacements) + output = {} + output.update(self.tags) + output.update(self.requesttags) + output.update(replacements) + return self[template].Parse(**output) def ParseString(self, template, **replacements): """Returns the given `template` with its tags replaced by **replacements. @@ -213,11 +250,14 @@ def ParseString(self, template, **replacements): @ replacements: dict Dictionary of replacement objects. Tags are looked up in here. - Returns: str: template with replaced tags. """ - return Template(template, parser=self).Parse(**replacements) + output = {} + output.update(self.tags) + output.update(self.requesttags) + output.update(replacements) + return Template(template, parser=self).Parse(**output) @staticmethod def RegisterFunction(name, function): @@ -231,6 +271,82 @@ def RegisterFunction(name, function): """ TAG_FUNCTIONS[name] = function + def RegisterTag(self, tag, value, persistent=False): + """Registers a `value`, allowing use in templates by `tag`. + + Arguments: + @ tag: str + The name of the tag + @ value: str, or function + Value or function to be executed when replacing this tag + @ persistent: bool + will this tag be present for multiple requests? + """ + storage = self.tags if persistent else self.requesttags + if ':' not in tag: + storage[tag] = value + return + tag = TemplateTag.FromString('[%s]' % tag) + # if we are dealing with a tag consisting of multiple path parts, lets reconstruct the path + obj = storage + prevnode = tag.name + d = storage + for node in tag.indices: + try: + node = int(node) + subtype = SparseList() + except ValueError: + subtype = {} + + # add the new sublist to the path if not existant + if prevnode not in obj: + obj[prevnode] = subtype + + obj = obj[prevnode] + prevnode = node + obj[node] = value + + @classmethod + def JITTag(cls, function): + """Creates a JITTag instance of the given function + + Arguments: + % function: reference + Reference to the function + """ + return JITTag(function) + + def ClearRequestTags(self): + """Resets the non persistent tags to None, is to be called after each + completed request""" + self.requesttags = {} + + def SetTemplateEncoding(self, templateEncoding='utf-8'): + """Allows the user to set the templateEncoding for this parser instance's + templates. Any template reads, and reloads will be attempted with this + encoding. + + Arguments: + % templateEncoding: str ~~ utf-8 + Encoding of the template, used when reading the file. + """ + self.templateEncoding = templateEncoding + + def SetEvalWhitelist(self, evalwhitelist=None, append=False): + """Allows the user to set the Eval Whitelist which limits the python + operations allowed within this templateParsers Context. These are usually + triggered by If/Elif conditions and the like. + + Arguments: + % evalwhitelist: Dict ~~ None + The new Dict of whitelisted eval AST items. + % append: bool ~~ False + When true, add the new items to the current list, else overwrite. + """ + if append: + evalwhitelist = EVALWHITELIST.update(evalwhitelist) + self.astvisitor = AstVisitor(evalwhitelist) + TemplateReadError = TemplateReadError @@ -240,14 +356,14 @@ class Template(list): # For a full tag syntax explanation, refer to the TAG regex in TemplateTag. TAG = re.compile(""" (\[\w+ # Tag start and alphanum tagname - (?:(?::[\w-]+)+)? # 0+ indices, alphanum with dashes + (?:(?::[\w\-\.]+)+)? # 0+ indices, alphanum with dashes (?:(?:\|[\w-]+ # 0+ functions, alphanum with dashes (?:\([^()]*?\))? # closure parentheses and arguments )+)? # end of function block \]) # end of tag""", re.VERBOSE) - def __init__(self, raw_template, parser=None): + def __init__(self, raw_template, parser=None, dictoutput=False): """Initializes a Template from a string. Arguments: @@ -256,11 +372,18 @@ def __init__(self, raw_template, parser=None): % parser: Parser ~~ None An optional parser instance that is necessary to enable support for adding files to the current template. This is used by {{ inline }}. + % dictoutput: Bool ~~ False + An optional parser parameter which can be used to ask the parser to + output a dictionary of the template and its replaced vars, will only be + used when parser is None. + """ - super(Template, self).__init__() + super().__init__() self.parser = parser + self.dictoutput = dictoutput self.scopes = [self] self.AddString(raw_template) + self.name = None def __eq__(self, other): """Returns the equality to another Template. @@ -293,11 +416,12 @@ def AddFile(self, name): TemplateReadError: The template file could not be read by the Parser. TypeError: There is no parser associated with the template. """ + self.name = name if self.parser is None: raise TypeError('The template requires parser for adding template files.') return self._AddToOpenScope(self.parser[name]) - def AddString(self, raw_template): + def AddString(self, raw_template, filename=None): """Extends the Template by adding a raw template string. The given template is parsed and added to the existing template. @@ -315,35 +439,31 @@ def AddString(self, raw_template): if len(self.scopes) != scope_depth: scope_diff = len(self.scopes) - scope_depth if scope_diff < 0: - raise TemplateSyntaxError('Closed %d scopes too many' % abs(scope_diff)) - raise TemplateSyntaxError('Template left %d open scopes.' % scope_diff) + raise TemplateSyntaxError('Closed %d scopes too many in "%s"' % (abs(scope_diff), filename or raw_template)) + raise TemplateSyntaxError('TemplateString left %d open scopes in "%s"' % (scope_diff, filename or raw_template)) - def Parse(self, returnRawTemplate=False, **kwds): - """Returns the parsed template as SafeString. + def Parse(self, **kwds): + """Returns the parsed template as HTMLsafestring. The template is parsed by parsing each of its members and combining that. """ - htmlsafe = HTMLsafestring(''.join(tag.Parse(**kwds) for tag in self)) - htmlsafe.content_hash = hashlib.md5(htmlsafe.encode()).hexdigest() - if returnRawTemplate: - raw = HTMLsafestring(self) - raw.content_hash = htmlsafe.content_hash - return raw - - if self.parser and self.parser.noparse: - #Hash the page so that we can compare on the frontend if the html has changed - htmlsafe.page_hash = hashlib.md5(HTMLsafestring(self).encode()).hexdigest() - #Hashes the page and the content so we can know if we need to refresh the page on the frontend - htmlsafe.tags = {} + dictoutput = self.parser and self.parser.dictoutput or self.dictoutput + if dictoutput: + output = {'tags': {}} + if self.name: + output['template'] = self.name + else: + output['templatecontent'] = str(self) for tag in self: if isinstance(tag, TemplateConditional): for flattend_branch in list(itertools.chain(*tag.branches)): for branch_tag in flattend_branch: if isinstance(branch_tag, TemplateTag): - htmlsafe.tags[str(branch_tag)] = branch_tag.Parse(**kwds) + output['tags'][str(branch_tag)] = branch_tag.Parse(**kwds) if isinstance(tag, TemplateTag): - htmlsafe.tags[str(tag)] = tag.Parse(**kwds) - return htmlsafe + output['tags'][str(tag)] = tag.Parse(**kwds) + return output + return HTMLsafestring('').join(HTMLsafestring(tag.Parse(**kwds)) for tag in self) @classmethod def TagSplit(cls, template): @@ -370,7 +490,9 @@ def _ExtendFunction(self, nodes): try: getattr(self, '_TemplateConstruct%s' % function.title())(*nodes) except AttributeError: - raise TemplateSyntaxError('Unknown template function {{ %s }}' % function) + raise TemplateSyntaxError('Unknown template function {{ %s }}%s' % + (function, + ' in template "%s"' % self._template_path if self._template_path else '')) def _ExtendText(self, node): """Processes a text node and adds its tags and texts to the Template.""" @@ -381,9 +503,6 @@ def _ExtendText(self, node): # Template syntax constructs # - def _TemplateConstructXsrf(self, value): - self.AddString(''.format(value)) - def _TemplateConstructInline(self, name): """Processing for {{ inline }} template syntax.""" self.AddFile(name) @@ -398,15 +517,18 @@ def _TemplateConstructEndfor(self): def _TemplateConstructIf(self, *nodes): """Processing for {{ if }} template syntax.""" - self._StartScope(TemplateConditional(' '.join(nodes))) + self._StartScope(TemplateConditional(' '.join(nodes), + self.parser.astvisitor if self.parser else AstVisitor(EVALWHITELIST))) def _TemplateConstructIfpresent(self, *nodes): """Processing for {{ ifpresent }} template syntax.""" - self._StartScope(TemplateConditionalPresence(' '.join(nodes))) + self._StartScope(TemplateConditionalPresence(' '.join(nodes), + self.parser.astvisitor if self.parser else AstVisitor(EVALWHITELIST))) def _TemplateConstructIfnotpresent(self, *nodes): """Processing for {{ ifnotpresent }} template syntax.""" - self._StartScope(TemplateConditionalPresence(' '.join(nodes), checking_presence=True)) + self._StartScope(TemplateConditionalNotPresence(' '.join(nodes), + self.parser.astvisitor if self.parser else AstVisitor(EVALWHITELIST))) def _TemplateConstructElif(self, *nodes): """Processing for {{ elif }} template syntax.""" @@ -457,7 +579,7 @@ def _VerifyOpenScope(self, scope_cls): class FileTemplate(Template): """Template class that loads from file.""" - def __init__(self, template_path, parser=None): + def __init__(self, template_path, parser=None, encoding='utf-8'): """Initializes a FileTemplate based on a given template path. Arguments: @@ -469,13 +591,17 @@ def __init__(self, template_path, parser=None): adding files to the current template. This is used by {{ inline }}. """ self._template_path = template_path + self.parser = parser + self.templateEncoding = encoding or (self.parser.templateEncoding if self.parser else 'utf-8') try: self._file_name = os.path.abspath(template_path) self._file_mtime = os.path.getmtime(self._file_name) - raw_template = open(self._file_name).read() - super(FileTemplate, self).__init__(raw_template, parser=parser) - except (IOError, OSError): - raise TemplateReadError('Cannot open: %r' % template_path) + with open(self._file_name, encoding=self.templateEncoding) as templatefile: + raw_template = templatefile.read() + self._template_hash = HashContent(raw_template) + super().__init__(raw_template, parser=parser) + except (IOError, OSError) as error: + raise TemplateReadError('Cannot open: %r %r' % (template_path, error)) def Parse(self, **kwds): """Returns the parsed template as SafeString. @@ -483,13 +609,16 @@ def Parse(self, **kwds): The template is parsed by parsing each of its members and combining that. """ self.ReloadIfModified() - result = super(FileTemplate, self).Parse(**kwds) - if self.parser and self.parser.noparse: - return {'template': self._file_name.rsplit('/')[-1], - 'replacements': result.tags, - 'content_hash':result.content_hash, - 'page_hash': result.page_hash - } + try: + result = super().Parse(**kwds) + except TemplateFunctionError as error: + raise TemplateFunctionError('%s in %s' % (error, self._template_path)) + if self.parser and self.parser.dictoutput: + return {'template': self._template_path[len(self.parser.template_dir):], + 'replacements': result['tags'], + 'template_hash': self._template_hash}#, + # 'content_hash': result.content_hash, + # 'page_hash': result.page_hash} return result def ReloadIfModified(self): @@ -505,10 +634,11 @@ def ReloadIfModified(self): try: mtime = os.path.getmtime(self._file_name) if mtime > self._file_mtime: - template = open(self._file_name).read() + with open(self._file_name, encoding=self.templateEncoding) as templatefile: + template = templatefile.read() del self[:] self.scopes = [self] - self.AddString(template) + self.AddString(template, self._file_name) self._file_mtime = mtime except (IOError, OSError): # File cannot be stat'd or read. No longer exists or we lack permissions. @@ -518,11 +648,11 @@ def ReloadIfModified(self): class TemplateConditional(object): """A template construct to control flow based on the value of a tag.""" - def __init__(self, expr, checking_presence=True): - self.checking_presence = checking_presence + def __init__(self, expr, astvisitor): self.branches = [] self.default = None self.NewBranch(expr) + self.astvisitor = astvisitor def __repr__(self): repr_branches = [] @@ -574,9 +704,8 @@ def Else(self): raise TemplateSyntaxError('Only one {{ else }} clause is allowed.') self.default = [] - @staticmethod - def Expression(expr, **kwds): - """Returns the eval()'ed result of a tag expression.""" + def Expression(self, expr, **kwds): + """Returns the evaluated result of a tag expression.""" nodes = [] local_vars = LazyTagValueRetrieval(kwds) for num, node in enumerate(expr): @@ -587,10 +716,11 @@ def Expression(expr, **kwds): else: nodes.append(node) try: - #XXX(Elmer): This uses eval, it's so much easier than lexing and parsing - return eval(''.join(nodes), None, local_vars) + return LimitedEval(''.join(nodes), self.astvisitor, local_vars) except NameError as error: - raise TemplateNameError(str(error).capitalize() + '. Try it as tagname?') + raise TemplateNameError(str(error).capitalize() + '. Try it as [tagname]?') + except SyntaxError as error: + raise TemplateSyntaxError('%s while evaluating: %s' % (str(error).capitalize(), ''.join(nodes))) def NewBranch(self, expr): """Begins a new branch based on the given expression.""" @@ -611,8 +741,6 @@ def Parse(self, **kwds): `else` branch exists '' is returned. """ for expr, branch in self.branches: - if type(self) == TemplateConditionalPresence: - kwds['checking_presence'] = True if self.Expression(expr, **kwds): return ''.join(part.Parse(**kwds) for part in branch) if self.default: @@ -620,7 +748,6 @@ def Parse(self, **kwds): return '' - class TemplateConditionalPresence(TemplateConditional): """A template construct to safely check for the presence of tags.""" @@ -630,17 +757,28 @@ def Expression(tags, **kwds): try: for tag in tags: tag.GetValue(kwds) - if kwds.get('checking_presence'): - return True - return False - except (TemplateKeyError, TemplateNameError): - if kwds.get('checking_presence'): - return False return True + except (TemplateKeyError, TemplateNameError): + return False def NewBranch(self, tags): """Begins a new branch based on the given tags.""" - self.branches.append((map(TemplateTag.FromString, tags.split()), [])) + self.branches.append((list(map(TemplateTag.FromString, tags.split())), [])) + + +class TemplateConditionalNotPresence(TemplateConditionalPresence): + """A template construct to safely check for the presence of tags.""" + + @staticmethod + def Expression(tags, **kwds): + """Checks the presence of all tags named on the branch.""" + try: + for tag in tags: + f + return False + except (TemplateKeyError, TemplateNameError): + return True + class TemplateLoop(list): """Template loops are used to repeat a portion of template multiple times. @@ -658,16 +796,15 @@ def __init__(self, tag, aliases): @ aliases: *str The alias(es) under which the loop variable should be made available. """ + super().__init__() + self.aliases = ''.join(aliases).split(',') + self.aliascount = len(self.aliases) try: - tag = TemplateTag.FromString(tag) + self.tag = TemplateTag.FromString(tag) except TemplateSyntaxError: + self.tag = tag raise TemplateSyntaxError('Tag %r in {{ for }} loop is not valid' % tag) - super(TemplateLoop, self).__init__() - self.aliases = ''.join(aliases).split(',') - self.aliascount = len(self.aliases) - self.tag = tag - def __repr__(self): return '%s(%s)' % (type(self).__name__, list(self)) @@ -711,7 +848,7 @@ class TemplateTag(object): TAG = re.compile(""" \[ # Tag starts with opening bracket (\w+) # Capture tagname (1+ alphanum length) - ((?::[\w-]+)+)? # Capture 0+ indices (1+ alphanum+dashes length) + ((?::[\w\-\.]+)+)? # Capture 0+ indices (1+ alphanum+dashes length) ((?:\|[\w-]+ # Capture 0+ functions (1+ alphanum+dashes length) (?:\([^()]*?\))? # Functions may be closures with arguments. )+)? # // end of optional functions @@ -719,6 +856,7 @@ class TemplateTag(object): re.VERBOSE) FUNC_FINDER = re.compile('\|([\w-]+(?:\([^()]*?\))?)') FUNC_CLOSURE = re.compile('(\w+)\((.*)\)') + ALLOWPRIVATE = False # will we allow access to private members for object lookup def __init__(self, name, indices=(), functions=()): """Initializes a TemplateTag instant. @@ -732,10 +870,12 @@ def __init__(self, name, indices=(), functions=()): Names of template functions that should be applied to the value. """ self.name = name - self.indices = indices + self.indices = (indices if self.ALLOWPRIVATE else [ + index for index in indices + if not index.startswith('_') or not index.endswith('_') + ]) self.functions = functions - def __repr__(self): return '%s(%r)' % (type(self).__name__, str(self)) @@ -785,9 +925,13 @@ def GetValue(self, replacements): value = replacements[self.name] for index in self.indices: value = self._GetIndex(value, index) + if isinstance(value, JITTag): + return value(**replacements) return value except KeyError: raise TemplateNameError('No replacement with name %r' % self.name) + except TemplateKeyError as error: + raise TemplateKeyError('%s on %r' % (error, self.name)) @classmethod def ApplyFunction(cls, func, value): @@ -797,7 +941,9 @@ def ApplyFunction(cls, func, value): return TAG_FUNCTIONS[func](value) func, args = closure.groups() #XXX(Elmer): This uses eval, it's so much easier than lexing and parsing - args = eval(args + ',') if args.strip() else () + # the regex leading up to this point make sure no function calls end up in + # here, nor variables, Math might show up though + args = eval(args + ',', {'__builtins__': {}}, {}) if args.strip() else () return TAG_FUNCTIONS[func](*args)(value) except SyntaxError: raise TemplateSyntaxError('Invalid argument syntax: %r' % args) @@ -805,8 +951,11 @@ def ApplyFunction(cls, func, value): raise TemplateTypeError( ('Templatefunction raised an TypeError %s(%s) ' % (func, value), err_obj)) except KeyError as err_obj: - raise TemplateNameError( + raise TemplateFunctionError( 'Unknown template tag function %r' % err_obj.args[0]) + except NameError as err_obj: + raise TemplateNameError( + 'Access to scope outside of parser variables is not allowed: %r' % err_obj.args[0]) def Parse(self, **kwds): """Returns the parsed string of the tag, using given replacements. @@ -832,14 +981,18 @@ def Parse(self, **kwds): except (TemplateKeyError, TemplateNameError): # On any failure to get the given index, return the unmodified tag. return str(self) - # Process functions, or apply default if value is not HTMLsafestring + # Process functions, or apply default if value is not Basesafestring if self.functions: for func in self.functions: - value = self.ApplyFunction(func, value) - else: - if not isinstance(value, Basesafestring): - value = TAG_FUNCTIONS['default'](value) - return str(value) + try: + value = self.ApplyFunction(func, value) + except TemplateFunctionError as error: + raise TemplateFunctionError('%s on %s' % (error, self)) + except TemplateSyntaxError as error: + raise TemplateSyntaxError('%s on %s' % (error, self)) + if not isinstance(value, Basesafestring): + value = TAG_FUNCTIONS['default'](value) + return value def Iterator(self, **kwds): """Parses the tag for iteration purposes. @@ -856,7 +1009,6 @@ def Iterator(self, **kwds): value = TAG_FUNCTIONS[func](value) return iter(value) - @staticmethod def _GetIndex(haystack, needle): """Returns the `needle` from the `haystack` by index, key or attribute name. @@ -872,7 +1024,7 @@ def _GetIndex(haystack, needle): Returns: obj: the object existing on `needle` in `haystack`. - """ + """ try: if needle.isdigit(): try: @@ -889,13 +1041,13 @@ def _GetIndex(haystack, needle): # TypeError: `haystack` is no mapping but may have a matching attr. return getattr(haystack, needle) except (AttributeError, LookupError): - raise TemplateKeyError('Item has no index, key or attribute %r.' % needle) + raise TemplateKeyError('Item has no index, key or attribute %r' % needle) class TemplateText(str): """A raw piece of template text, upon which no replacements will be done.""" def __new__(cls, string): - return super(TemplateText, cls).__new__(cls, string) + return super().__new__(cls, string) def __repr__(self): """Returns the object representation of the TemplateText.""" @@ -906,11 +1058,82 @@ def Parse(self, **_kwds): return str(self) +class JITTag(object): + """This is a template Tag which is only evaulated on replacement. + It is usefull for situations where not all all of this functions input vars + are available just yet. + """ + + def __init__(self, function): + """Stores the function for later use""" + self.wrapped = function + self.result = None # cache for results + self.called = False # keep score of result cache usage, None and False might be correct results in the cache + + def __call__(self, *args, **kwargs): + """Returns the output of the earlier wrapped function""" + if not self.called: + try: + self.result = self.wrapped(*args, **kwargs) + except TypeError: # the lambda does not expect params + self.result = self.wrapped() + self.called = True + return self.result + + +class SparseList(list): + """A spare list implementation to allow us to set the nth item on a list""" + def __setitem__(self, index, value): + missing = index - len(self) + 1 + if missing > 0: + self.extend([None] * missing) + super().__setitem__(index, value) + + def __getitem__(self, index): + """Return the value at the index, and None of that index was not available + instead of raisig IndexError + """ + try: + return super().__getitem__(index) + except IndexError: + return None + + +class AstVisitor(ast.NodeVisitor): + def __init__(self, whitelists): + self.whitelists = whitelists + + def visit(self, node): + if not isinstance(node, self.whitelists['operators']): + raise TemplateEvaluationError('`%s` is not an allowed operation' % node) + return super().visit(node) + + def visit_Call(self, call): + """Filter calls""" + if call.func.id not in self.whitelists['functions']: + raise TemplateEvaluationError('`%s` is not an allowed function call' % call.func.id) + +def LimitedEval(expr, astvisitor, evallocals = {}): + """A limited Eval function which only allows certain operations""" + tree = ast.parse(expr, mode='eval') + astvisitor.visit(tree) + return eval(compile(tree, "", "eval"), + astvisitor.whitelists['functions'], + evallocals) + +def HashContent(string): + """Helper function for hashing of template files, this is needed to allow + users to download raw templates on their own which they know the hash for.""" + return hashlib.sha256(string.encode('utf-8')).hexdigest() + + TAG_FUNCTIONS = { 'default': lambda d: HTMLsafestring('') + d, 'html': lambda d: HTMLsafestring('') + d, - 'raw': lambda x: x, - 'url': lambda d: URLqueryargumentsafestring(d, unsafe=True), + 'htmlsource': lambda d: HTMLsafestring(d, unsafe=True), + 'raw': lambda d: Unsafestring(d), + 'url': lambda d: HTMLsafestring(URLqueryargumentsafestring(d, unsafe=True)), + 'type': type, 'items': lambda d: list(d.items()), 'values': lambda d: list(d.values()), 'sorted': sorted,