From 9bf877f169a6dea6898c3dbbead23f0a6694e77b Mon Sep 17 00:00:00 2001 From: Mpho raf Date: Tue, 2 Feb 2021 10:43:26 +0200 Subject: [PATCH] updated the file and adding jenkinsfile --- .gitignore | 66 +- Jenkinsfile | 10 + LICENSE | 741 +++--------------- Pipfile | 19 + Pipfile.lock | 124 +++ Procfile | 1 + README.md | 40 +- catalog/__init__.py | 0 catalog/admin.py | 78 ++ catalog/apps.py | 5 + catalog/forms.py | 24 + catalog/migrations/0001_initial.py | 47 ++ catalog/migrations/0002_auto_20160921_1401.py | 21 + catalog/migrations/0003_auto_20160921_1420.py | 25 + catalog/migrations/0004_auto_20160921_1422.py | 20 + catalog/migrations/0005_auto_20160921_1433.py | 41 + catalog/migrations/0006_auto_20160921_1439.py | 25 + catalog/migrations/0007_auto_20160921_1444.py | 25 + catalog/migrations/0008_auto_20160921_1511.py | 41 + .../0009_remove_bookinstance_summary.py | 19 + catalog/migrations/0010_auto_20160921_1527.py | 21 + catalog/migrations/0011_auto_20160922_1029.py | 36 + .../0012_bookinstance_date_acquired.py | 22 + catalog/migrations/0013_auto_20160926_1901.py | 21 + .../0014_remove_bookinstance_date_acquired.py | 19 + catalog/migrations/0015_auto_20160927_1808.py | 25 + catalog/migrations/0016_auto_20160927_1947.py | 39 + catalog/migrations/0017_language.py | 22 + catalog/migrations/0018_book_language.py | 21 + .../migrations/0019_bookinstance_borrower.py | 23 + catalog/migrations/0020_auto_20161012_1044.py | 19 + catalog/migrations/0021_auto_20170504_1512.py | 20 + catalog/migrations/0021_auto_20171229_1056.py | 18 + .../migrations/0022_merge_20180115_2033.py | 14 + catalog/migrations/0023_auto_20200902_1539.py | 17 + catalog/migrations/__init__.py | 0 catalog/models.py | 136 ++++ catalog/static/css/styles.css | 6 + .../static/images/local_library_model_uml.png | Bin 0 -> 21273 bytes catalog/templates/base_generic.html | 78 ++ .../catalog/author_confirm_delete.html | 14 + catalog/templates/catalog/author_detail.html | 19 + catalog/templates/catalog/author_form.html | 13 + catalog/templates/catalog/author_list.html | 26 + .../catalog/book_confirm_delete.html | 14 + catalog/templates/catalog/book_detail.html | 26 + catalog/templates/catalog/book_form.html | 13 + catalog/templates/catalog/book_list.html | 21 + .../catalog/book_renew_librarian.html | 17 + .../bookinstance_list_borrowed_all.html | 19 + .../bookinstance_list_borrowed_user.html | 20 + catalog/templates/index.html | 32 + catalog/tests/__init__.py | 0 catalog/tests/test_forms.py | 61 ++ catalog/tests/test_models.py | 56 ++ catalog/tests/test_views.py | 343 ++++++++ catalog/urls.py | 39 + catalog/views.py | 166 ++++ locallibrary/__init__.py | 0 locallibrary/settings.py | 155 ++++ locallibrary/urls.py | 60 ++ locallibrary/wsgi.py | 24 + manage.py | 22 + requirements.txt | 18 + runtime.txt | 1 + templates/registration/logged_out.html | 7 + templates/registration/login.html | 38 + .../registration/password_reset_complete.html | 8 + .../registration/password_reset_confirm.html | 33 + .../registration/password_reset_done.html | 7 + .../registration/password_reset_email.html | 2 + .../registration/password_reset_form.html | 11 + ...forms.RenewBookFormTest-20200901215420.xml | 33 + ..._models.AuthorModelTest-20200901215420.xml | 11 + ...ws.AuthorCreateViewTest-20200901215420.xml | 17 + ...iews.AuthorListViewTest-20200901215420.xml | 8 + ...ancesByUserListViewTest-20200901215420.xml | 8 + ...ewBookInstancesViewTest-20200901215420.xml | 13 + 78 files changed, 2628 insertions(+), 676 deletions(-) create mode 100644 Jenkinsfile create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 Procfile create mode 100644 catalog/__init__.py create mode 100644 catalog/admin.py create mode 100644 catalog/apps.py create mode 100644 catalog/forms.py create mode 100644 catalog/migrations/0001_initial.py create mode 100644 catalog/migrations/0002_auto_20160921_1401.py create mode 100644 catalog/migrations/0003_auto_20160921_1420.py create mode 100644 catalog/migrations/0004_auto_20160921_1422.py create mode 100644 catalog/migrations/0005_auto_20160921_1433.py create mode 100644 catalog/migrations/0006_auto_20160921_1439.py create mode 100644 catalog/migrations/0007_auto_20160921_1444.py create mode 100644 catalog/migrations/0008_auto_20160921_1511.py create mode 100644 catalog/migrations/0009_remove_bookinstance_summary.py create mode 100644 catalog/migrations/0010_auto_20160921_1527.py create mode 100644 catalog/migrations/0011_auto_20160922_1029.py create mode 100644 catalog/migrations/0012_bookinstance_date_acquired.py create mode 100644 catalog/migrations/0013_auto_20160926_1901.py create mode 100644 catalog/migrations/0014_remove_bookinstance_date_acquired.py create mode 100644 catalog/migrations/0015_auto_20160927_1808.py create mode 100644 catalog/migrations/0016_auto_20160927_1947.py create mode 100644 catalog/migrations/0017_language.py create mode 100644 catalog/migrations/0018_book_language.py create mode 100644 catalog/migrations/0019_bookinstance_borrower.py create mode 100644 catalog/migrations/0020_auto_20161012_1044.py create mode 100644 catalog/migrations/0021_auto_20170504_1512.py create mode 100644 catalog/migrations/0021_auto_20171229_1056.py create mode 100644 catalog/migrations/0022_merge_20180115_2033.py create mode 100644 catalog/migrations/0023_auto_20200902_1539.py create mode 100644 catalog/migrations/__init__.py create mode 100644 catalog/models.py create mode 100644 catalog/static/css/styles.css create mode 100644 catalog/static/images/local_library_model_uml.png create mode 100644 catalog/templates/base_generic.html create mode 100644 catalog/templates/catalog/author_confirm_delete.html create mode 100644 catalog/templates/catalog/author_detail.html create mode 100644 catalog/templates/catalog/author_form.html create mode 100644 catalog/templates/catalog/author_list.html create mode 100644 catalog/templates/catalog/book_confirm_delete.html create mode 100644 catalog/templates/catalog/book_detail.html create mode 100644 catalog/templates/catalog/book_form.html create mode 100644 catalog/templates/catalog/book_list.html create mode 100644 catalog/templates/catalog/book_renew_librarian.html create mode 100644 catalog/templates/catalog/bookinstance_list_borrowed_all.html create mode 100644 catalog/templates/catalog/bookinstance_list_borrowed_user.html create mode 100644 catalog/templates/index.html create mode 100644 catalog/tests/__init__.py create mode 100644 catalog/tests/test_forms.py create mode 100644 catalog/tests/test_models.py create mode 100644 catalog/tests/test_views.py create mode 100644 catalog/urls.py create mode 100644 catalog/views.py create mode 100644 locallibrary/__init__.py create mode 100644 locallibrary/settings.py create mode 100644 locallibrary/urls.py create mode 100644 locallibrary/wsgi.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 runtime.txt create mode 100644 templates/registration/logged_out.html create mode 100644 templates/registration/login.html create mode 100644 templates/registration/password_reset_complete.html create mode 100644 templates/registration/password_reset_confirm.html create mode 100644 templates/registration/password_reset_done.html create mode 100644 templates/registration/password_reset_email.html create mode 100644 templates/registration/password_reset_form.html create mode 100644 test-results/TEST-catalog.tests.test_forms.RenewBookFormTest-20200901215420.xml create mode 100644 test-results/TEST-catalog.tests.test_models.AuthorModelTest-20200901215420.xml create mode 100644 test-results/TEST-catalog.tests.test_views.AuthorCreateViewTest-20200901215420.xml create mode 100644 test-results/TEST-catalog.tests.test_views.AuthorListViewTest-20200901215420.xml create mode 100644 test-results/TEST-catalog.tests.test_views.LoanedBookInstancesByUserListViewTest-20200901215420.xml create mode 100644 test-results/TEST-catalog.tests.test_views.RenewBookInstancesViewTest-20200901215420.xml diff --git a/.gitignore b/.gitignore index 13d1490..2d7f420 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ -# ---> Python +# Test reports +test-reports/ + +# Text backup files +*.bak + +#Database +*.sqlite3 + + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -9,6 +18,7 @@ __pycache__/ # Distribution / packaging .Python +env/ build/ develop-eggs/ dist/ @@ -20,13 +30,9 @@ lib64/ parts/ sdist/ var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -41,16 +47,13 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*.cover -*.py,cover +*,cover .hypothesis/ -.pytest_cache/ # Translations *.mo @@ -59,8 +62,6 @@ coverage.xml # Django stuff: *.log local_settings.py -db.sqlite3 -db.sqlite3-journal # Flask stuff: instance/ @@ -75,57 +76,24 @@ docs/_build/ # PyBuilder target/ -# Jupyter Notebook +# IPython Notebook .ipynb_checkpoints -# IPython -profile_default/ -ipython_config.py - # pyenv .python-version -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff +# celery beat schedule file celerybeat-schedule -celerybeat.pid -# SageMath parsed files -*.sage.py - -# Environments +# dotenv .env -.venv -env/ + +# virtualenv venv/ ENV/ -env.bak/ -venv.bak/ # Spyder project settings .spyderproject -.spyproject # Rope project settings .ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..2968c05 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,10 @@ +pipeline { + agent { docker { image 'python:3-alpine' } } + stages { + stage('test') { + steps { + sh 'python --version' + } + } + } +} diff --git a/LICENSE b/LICENSE index e142a52..3bbbc1e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,625 +1,116 @@ -GNU GENERAL PUBLIC LICENSE - -Version 3, 29 June 2007 - -Copyright © 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license -document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for software and -other kinds of works. - -The licenses for most software and other practical works are designed to take -away your freedom to share and change the works. By contrast, the GNU General -Public License is intended to guarantee your freedom to share and change all -versions of a program--to make sure it remains free software for all its users. -We, the Free Software Foundation, use the GNU General Public License for most -of our software; it applies also to any other work released this way by its -authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our -General Public Licenses are designed to make sure that you have the freedom -to distribute copies of free software (and charge for them if you wish), that -you receive source code or can get it if you want it, that you can change -the software or use pieces of it in new free programs, and that you know you -can do these things. - -To protect your rights, we need to prevent others from denying you these rights -or asking you to surrender the rights. Therefore, you have certain responsibilities -if you distribute copies of the software, or if you modify it: responsibilities -to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or -for a fee, you must pass on to the recipients the same freedoms that you received. -You must make sure that they, too, receive or can get the source code. And -you must show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: (1) assert -copyright on the software, and (2) offer you this License giving you legal -permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that -there is no warranty for this free software. For both users' and authors' -sake, the GPL requires that modified versions be marked as changed, so that -their problems will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified -versions of the software inside them, although the manufacturer can do so. -This is fundamentally incompatible with the aim of protecting users' freedom -to change the software. The systematic pattern of such abuse occurs in the -area of products for individuals to use, which is precisely where it is most -unacceptable. Therefore, we have designed this version of the GPL to prohibit -the practice for those products. If such problems arise substantially in other -domains, we stand ready to extend this provision to those domains in future -versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States -should not allow patents to restrict development and use of software on general-purpose -computers, but in those that do, we wish to avoid the special danger that -patents applied to a free program could make it effectively proprietary. To -prevent this, the GPL assures that patents cannot be used to render the program -non-free. - -The precise terms and conditions for copying, distribution and modification -follow. - -TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of works, -such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this License. -Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals -or organizations. - -To "modify" a work means to copy from or adapt all or part of the work in -a fashion requiring copyright permission, other than the making of an exact -copy. The resulting work is called a "modified version" of the earlier work -or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based on the -Program. - -To "propagate" a work means to do anything with it that, without permission, -would make you directly or secondarily liable for infringement under applicable -copyright law, except executing it on a computer or modifying a private copy. -Propagation includes copying, distribution (with or without modification), -making available to the public, and in some countries other activities as -well. - -To "convey" a work means any kind of propagation that enables other parties -to make or receive copies. Mere interaction with a user through a computer -network, with no transfer of a copy, is not conveying. - -An interactive user interface displays "Appropriate Legal Notices" to the -extent that it includes a convenient and prominently visible feature that -(1) displays an appropriate copyright notice, and (2) tells the user that -there is no warranty for the work (except to the extent that warranties are -provided), that licensees may convey the work under this License, and how -to view a copy of this License. If the interface presents a list of user commands -or options, such as a menu, a prominent item in the list meets this criterion. - - 1. Source Code. - -The "source code" for a work means the preferred form of the work for making -modifications to it. "Object code" means any non-source form of a work. - -A "Standard Interface" means an interface that either is an official standard -defined by a recognized standards body, or, in the case of interfaces specified -for a particular programming language, one that is widely used among developers -working in that language. - -The "System Libraries" of an executable work include anything, other than -the work as a whole, that (a) is included in the normal form of packaging -a Major Component, but which is not part of that Major Component, and (b) -serves only to enable use of the work with that Major Component, or to implement -a Standard Interface for which an implementation is available to the public -in source code form. A "Major Component", in this context, means a major essential -component (kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to produce -the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source -code needed to generate, install, and (for an executable work) run the object -code and to modify the work, including scripts to control those activities. -However, it does not include the work's System Libraries, or general-purpose -tools or generally available free programs which are used unmodified in performing -those activities but which are not part of the work. For example, Corresponding -Source includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically linked -subprograms that the work is specifically designed to require, such as by -intimate data communication or control flow between those subprograms and -other parts of the work. - -The Corresponding Source need not include anything that users can regenerate -automatically from other parts of the Corresponding Source. - - The Corresponding Source for a work in source code form is that same work. - - 2. Basic Permissions. - -All rights granted under this License are granted for the term of copyright -on the Program, and are irrevocable provided the stated conditions are met. -This License explicitly affirms your unlimited permission to run the unmodified -Program. The output from running a covered work is covered by this License -only if the output, given its content, constitutes a covered work. This License -acknowledges your rights of fair use or other equivalent, as provided by copyright -law. - -You may make, run and propagate covered works that you do not convey, without -conditions so long as your license otherwise remains in force. You may convey -covered works to others for the sole purpose of having them make modifications -exclusively for you, or provide you with facilities for running those works, -provided that you comply with the terms of this License in conveying all material -for which you do not control copyright. Those thus making or running the covered -works for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of your copyrighted -material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions -stated below. Sublicensing is not allowed; section 10 makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological measure -under any applicable law fulfilling obligations under article 11 of the WIPO -copyright treaty adopted on 20 December 1996, or similar laws prohibiting -or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention -of technological measures to the extent such circumvention is effected by -exercising rights under this License with respect to the covered work, and -you disclaim any intention to limit operation or modification of the work -as a means of enforcing, against the work's users, your or third parties' -legal rights to forbid circumvention of technological measures. - - 4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you receive -it, in any medium, provided that you conspicuously and appropriately publish -on each copy an appropriate copyright notice; keep intact all notices stating -that this License and any non-permissive terms added in accord with section -7 apply to the code; keep intact all notices of the absence of any warranty; -and give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you -may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to produce -it from the Program, in the form of source code under the terms of section -4, provided that you also meet all of these conditions: - -a) The work must carry prominent notices stating that you modified it, and -giving a relevant date. - -b) The work must carry prominent notices stating that it is released under -this License and any conditions added under section 7. This requirement modifies -the requirement in section 4 to "keep intact all notices". - -c) You must license the entire work, as a whole, under this License to anyone -who comes into possession of a copy. This License will therefore apply, along -with any applicable section 7 additional terms, to the whole of the work, -and all its parts, regardless of how they are packaged. This License gives -no permission to license the work in any other way, but it does not invalidate -such permission if you have separately received it. - -d) If the work has interactive user interfaces, each must display Appropriate -Legal Notices; however, if the Program has interactive interfaces that do -not display Appropriate Legal Notices, your work need not make them do so. - -A compilation of a covered work with other separate and independent works, -which are not by their nature extensions of the covered work, and which are -not combined with it such as to form a larger program, in or on a volume of -a storage or distribution medium, is called an "aggregate" if the compilation -and its resulting copyright are not used to limit the access or legal rights -of the compilation's users beyond what the individual works permit. Inclusion -of a covered work in an aggregate does not cause this License to apply to -the other parts of the aggregate. - - 6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of sections -4 and 5, provided that you also convey the machine-readable Corresponding -Source under the terms of this License, in one of these ways: - -a) Convey the object code in, or embodied in, a physical product (including -a physical distribution medium), accompanied by the Corresponding Source fixed -on a durable physical medium customarily used for software interchange. - -b) Convey the object code in, or embodied in, a physical product (including -a physical distribution medium), accompanied by a written offer, valid for -at least three years and valid for as long as you offer spare parts or customer -support for that product model, to give anyone who possesses the object code -either (1) a copy of the Corresponding Source for all the software in the -product that is covered by this License, on a durable physical medium customarily -used for software interchange, for a price no more than your reasonable cost -of physically performing this conveying of source, or (2) access to copy the -Corresponding Source from a network server at no charge. - -c) Convey individual copies of the object code with a copy of the written -offer to provide the Corresponding Source. This alternative is allowed only -occasionally and noncommercially, and only if you received the object code -with such an offer, in accord with subsection 6b. - -d) Convey the object code by offering access from a designated place (gratis -or for a charge), and offer equivalent access to the Corresponding Source -in the same way through the same place at no further charge. You need not -require recipients to copy the Corresponding Source along with the object -code. If the place to copy the object code is a network server, the Corresponding -Source may be on a different server (operated by you or a third party) that -supports equivalent copying facilities, provided you maintain clear directions -next to the object code saying where to find the Corresponding Source. Regardless -of what server hosts the Corresponding Source, you remain obligated to ensure -that it is available for as long as needed to satisfy these requirements. - -e) Convey the object code using peer-to-peer transmission, provided you inform -other peers where the object code and Corresponding Source of the work are -being offered to the general public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from -the Corresponding Source as a System Library, need not be included in conveying -the object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible -personal property which is normally used for personal, family, or household -purposes, or (2) anything designed or sold for incorporation into a dwelling. -In determining whether a product is a consumer product, doubtful cases shall -be resolved in favor of coverage. For a particular product received by a particular -user, "normally used" refers to a typical or common use of that class of product, -regardless of the status of the particular user or of the way in which the -particular user actually uses, or expects or is expected to use, the product. -A product is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent the -only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, -authorization keys, or other information required to install and execute modified -versions of a covered work in that User Product from a modified version of -its Corresponding Source. The information must suffice to ensure that the -continued functioning of the modified object code is in no case prevented -or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically -for use in, a User Product, and the conveying occurs as part of a transaction -in which the right of possession and use of the User Product is transferred -to the recipient in perpetuity or for a fixed term (regardless of how the -transaction is characterized), the Corresponding Source conveyed under this -section must be accompanied by the Installation Information. But this requirement -does not apply if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has been installed -in ROM). - -The requirement to provide Installation Information does not include a requirement -to continue to provide support service, warranty, or updates for a work that -has been modified or installed by the recipient, or for the User Product in -which it has been modified or installed. Access to a network may be denied -when the modification itself materially and adversely affects the operation -of the network or violates the rules and protocols for communication across -the network. - -Corresponding Source conveyed, and Installation Information provided, in accord -with this section must be in a format that is publicly documented (and with -an implementation available to the public in source code form), and must require -no special password or key for unpacking, reading or copying. - - 7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this License -by making exceptions from one or more of its conditions. Additional permissions -that are applicable to the entire Program shall be treated as though they -were included in this License, to the extent that they are valid under applicable -law. If additional permissions apply only to part of the Program, that part -may be used separately under those permissions, but the entire Program remains -governed by this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any -additional permissions from that copy, or from any part of it. (Additional -permissions may be written to require their own removal in certain cases when -you modify the work.) You may place additional permissions on material, added -by you to a covered work, for which you have or can give appropriate copyright -permission. - -Notwithstanding any other provision of this License, for material you add -to a covered work, you may (if authorized by the copyright holders of that -material) supplement the terms of this License with terms: - -a) Disclaiming warranty or limiting liability differently from the terms of -sections 15 and 16 of this License; or - -b) Requiring preservation of specified reasonable legal notices or author -attributions in that material or in the Appropriate Legal Notices displayed -by works containing it; or - -c) Prohibiting misrepresentation of the origin of that material, or requiring -that modified versions of such material be marked in reasonable ways as different -from the original version; or - -d) Limiting the use for publicity purposes of names of licensors or authors -of the material; or - -e) Declining to grant rights under trademark law for use of some trade names, -trademarks, or service marks; or - -f) Requiring indemnification of licensors and authors of that material by -anyone who conveys the material (or modified versions of it) with contractual -assumptions of liability to the recipient, for any liability that these contractual -assumptions directly impose on those licensors and authors. - -All other non-permissive additional terms are considered "further restrictions" -within the meaning of section 10. If the Program as you received it, or any -part of it, contains a notice stating that it is governed by this License -along with a term that is a further restriction, you may remove that term. -If a license document contains a further restriction but permits relicensing -or conveying under this License, you may add to a covered work material governed -by the terms of that license document, provided that the further restriction -does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, -in the relevant source files, a statement of the additional terms that apply -to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form -of a separately written license, or stated as exceptions; the above requirements -apply either way. - - 8. Termination. - -You may not propagate or modify a covered work except as expressly provided -under this License. Any attempt otherwise to propagate or modify it is void, -and will automatically terminate your rights under this License (including -any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from -a particular copyright holder is reinstated (a) provisionally, unless and -until the copyright holder explicitly and finally terminates your license, -and (b) permanently, if the copyright holder fails to notify you of the violation -by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently -if the copyright holder notifies you of the violation by some reasonable means, -this is the first time you have received notice of violation of this License -(for any work) from that copyright holder, and you cure the violation prior -to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses -of parties who have received copies or rights from you under this License. -If your rights have been terminated and not permanently reinstated, you do -not qualify to receive new licenses for the same material under section 10. - - 9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run a copy -of the Program. Ancillary propagation of a covered work occurring solely as -a consequence of using peer-to-peer transmission to receive a copy likewise -does not require acceptance. However, nothing other than this License grants -you permission to propagate or modify any covered work. These actions infringe -copyright if you do not accept this License. Therefore, by modifying or propagating -a covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically receives -a license from the original licensors, to run, modify and propagate that work, -subject to this License. You are not responsible for enforcing compliance -by third parties with this License. - -An "entity transaction" is a transaction transferring control of an organization, -or substantially all assets of one, or subdividing an organization, or merging -organizations. If propagation of a covered work results from an entity transaction, -each party to that transaction who receives a copy of the work also receives -whatever licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the Corresponding -Source of the work from the predecessor in interest, if the predecessor has -it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights -granted or affirmed under this License. For example, you may not impose a -license fee, royalty, or other charge for exercise of rights granted under -this License, and you may not initiate litigation (including a cross-claim -or counterclaim in a lawsuit) alleging that any patent claim is infringed -by making, using, selling, offering for sale, or importing the Program or -any portion of it. - - 11. Patents. - -A "contributor" is a copyright holder who authorizes use under this License -of the Program or a work on which the Program is based. The work thus licensed -is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims owned or controlled -by the contributor, whether already acquired or hereafter acquired, that would -be infringed by some manner, permitted by this License, of making, using, -or selling its contributor version, but do not include claims that would be -infringed only as a consequence of further modification of the contributor -version. For purposes of this definition, "control" includes the right to -grant patent sublicenses in a manner consistent with the requirements of this -License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent -license under the contributor's essential patent claims, to make, use, sell, -offer for sale, import and otherwise run, modify and propagate the contents -of its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement -or commitment, however denominated, not to enforce a patent (such as an express -permission to practice a patent or covenant not to sue for patent infringement). -To "grant" such a patent license to a party means to make such an agreement -or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the -Corresponding Source of the work is not available for anyone to copy, free -of charge and under the terms of this License, through a publicly available -network server or other readily accessible means, then you must either (1) -cause the Corresponding Source to be so available, or (2) arrange to deprive -yourself of the benefit of the patent license for this particular work, or -(3) arrange, in a manner consistent with the requirements of this License, -to extend the patent license to downstream recipients. "Knowingly relying" -means you have actual knowledge that, but for the patent license, your conveying -the covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that country -that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, -you convey, or propagate by procuring conveyance of, a covered work, and grant -a patent license to some of the parties receiving the covered work authorizing -them to use, propagate, modify or convey a specific copy of the covered work, -then the patent license you grant is automatically extended to all recipients -of the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope -of its coverage, prohibits the exercise of, or is conditioned on the non-exercise -of one or more of the rights that are specifically granted under this License. -You may not convey a covered work if you are a party to an arrangement with -a third party that is in the business of distributing software, under which -you make payment to the third party based on the extent of your activity of -conveying the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by you -(or copies made from those copies), or (b) primarily for and in connection -with specific products or compilations that contain the covered work, unless -you entered into that arrangement, or that patent license was granted, prior -to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied -license or other defenses to infringement that may otherwise be available -to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or otherwise) -that contradict the conditions of this License, they do not excuse you from -the conditions of this License. If you cannot convey a covered work so as -to satisfy simultaneously your obligations under this License and any other -pertinent obligations, then as a consequence you may not convey it at all. -For example, if you agree to terms that obligate you to collect a royalty -for further conveying from those to whom you convey the Program, the only -way you could satisfy both those terms and this License would be to refrain -entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - -Notwithstanding any other provision of this License, you have permission to -link or combine any covered work with a work licensed under version 3 of the -GNU Affero General Public License into a single combined work, and to convey -the resulting work. The terms of this License will continue to apply to the -part which is the covered work, but the special requirements of the GNU Affero -General Public License, section 13, concerning interaction through a network -will apply to the combination as such. - - 14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of the -GNU General Public License from time to time. Such new versions will be similar -in spirit to the present version, but may differ in detail to address new -problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies -that a certain numbered version of the GNU General Public License "or any -later version" applies to it, you have the option of following the terms and -conditions either of that numbered version or of any later version published -by the Free Software Foundation. If the Program does not specify a version -number of the GNU General Public License, you may choose any version ever -published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of -the GNU General Public License can be used, that proxy's public statement -of acceptance of a version permanently authorizes you to choose that version -for the Program. - -Later license versions may give you additional or different permissions. However, -no additional obligations are imposed on any author or copyright holder as -a result of your choosing to follow a later version. - - 15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE -LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER -EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM -PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. - - 16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL -ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM -AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, -INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO -USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED -INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE -PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER -PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided above cannot -be given local legal effect according to their terms, reviewing courts shall -apply local law that most closely approximates an absolute waiver of all civil -liability in connection with the Program, unless a warranty or assumption -of liability accompanies a copy of the Program in return for a fee. END OF -TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible -use to the public, the best way to achieve this is to make it free software -which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach -them to the start of each source file to most effectively state the exclusion -of warranty; and each file should have at least the "copyright" line and a -pointer to where the full notice is found. - - - -Copyright (C) - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. - -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with -this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short notice like -this when it starts in an interactive mode: - - Copyright (C) - -This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - -This is free software, and you are welcome to redistribute it under certain -conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands might -be different; for a GUI interface, you would use an "about box". - -You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. For -more information on this, and how to apply and follow the GNU GPL, see . - -The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General Public -License instead of this License. But first, please read . +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..7c9ee13 --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[packages] + +dj-database-url = "*" +django = "*" +gunicorn = "*" +"psycopg2" = "*" +whitenoise = "*" +unittest-xml-reporting = "*" + + +[dev-packages] + diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..1e4df48 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,124 @@ +{ + "_meta": { + "hash": { + "sha256": "fed1293db1dad173865419f29f284b5f0185dcfbe26f951c787764ed3ba3283d" + }, + "host-environment-markers": { + "implementation_name": "cpython", + "implementation_version": "3.6.4", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "17.3.0", + "platform_system": "Darwin", + "platform_version": "Darwin Kernel Version 17.3.0: Thu Nov 9 18:09:22 PST 2017; root:xnu-4570.31.3~1/RELEASE_X86_64", + "python_full_version": "3.6.4", + "python_version": "3.6", + "sys_platform": "darwin" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "dj-database-url": { + "hashes": [ + "sha256:e16d94c382ea0564c48038fa7fe8d9c890ef1ab1a8ec4cb48e732c124b9482fd", + "sha256:a6832d8445ee9d788c5baa48aef8130bf61fdc442f7d9a548424d25cd85c9f08" + ], + "version": "==0.4.2" + }, + "django": { + "hashes": [ + "sha256:52475f607c92035d4ac8fee284f56213065a4a6b25ed43f7e39df0e576e69e9f", + "sha256:d96b804be412a5125a594023ec524a2010a6ffa4d408e5482ab6ff3cb97ec12f" + ], + "version": "==2.0.1" + }, + "gunicorn": { + "hashes": [ + "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6", + "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622" + ], + "version": "==19.7.1" + }, + "psycopg2": { + "hashes": [ + "sha256:594aa9a095de16614f703d759e10c018bdffeafce2921b8e80a0e8a0ebbc12e5", + "sha256:1cf5d84290c771eeecb734abe2c6c3120e9837eb12f99474141a862b9061ac51", + "sha256:0344b181e1aea37a58c218ccb0f0f771295de9aa25a625ed076e6996c6530f9e", + "sha256:25250867a4cd1510fb755ef9cb38da3065def999d8e92c44e49a39b9b76bc893", + "sha256:317612d5d0ca4a9f7e42afb2add69b10be360784d21ce4ecfbca19f1f5eadf43", + "sha256:9d6266348b15b4a48623bf4d3e50445d8e581da413644f365805b321703d0fac", + "sha256:ddca39cc55877653b5fcf59976d073e3d58c7c406ef54ae8e61ddf8782867182", + "sha256:988d2ec7560d42ef0ac34b3b97aad14c4f068792f00e1524fa1d3749fe4e4b64", + "sha256:7a9c6c62e6e05df5406e9b5235c31c376a22620ef26715a663cee57083b3c2ea", + "sha256:7a75565181e75ba0b9fb174b58172bf6ea9b4331631cfe7bafff03f3641f5d73", + "sha256:94e4128ba1ea56f02522fffac65520091a9de3f5c00da31539e085e13db4771b", + "sha256:92179bd68c2efe72924a99b6745a9172471931fc296f9bfdf9645b75eebd6344", + "sha256:b9358e203168fef7bfe9f430afaed3a2a624717a1d19c7afa7dfcbd76e3cd95c", + "sha256:009e0bc09a57dbef4b601cb8b46a2abad51f5274c8be4bba276ff2884cd4cc53", + "sha256:d3ac07240e2304181ffdb13c099840b5eb555efc7be9344503c0c03aa681de79", + "sha256:40fa5630cd7d237cd93c4d4b64b9e5ed9273d1cfce55241c7f9066f5db70629d", + "sha256:6c2f1a76a9ebd9ecf7825b9e20860139ca502c2bf1beabf6accf6c9e66a7e0c3", + "sha256:37f54452c7787dbdc0a634ca9773362b91709917f0b365ed14b831f03cbd34ba", + "sha256:8f5942a4daf1ffac42109dc4a72f786af4baa4fa702ede1d7c57b4b696c2e7d6", + "sha256:bf708455cd1e9fa96c05126e89a0c59b200d086c7df7bbafc7d9be769e4149a3", + "sha256:82c40ea3ac1555e0462803380609fbe8b26f52620f3d4f8eb480cfd8ceed8a14", + "sha256:207ba4f9125a0a4200691e82d5eee7ea1485708eabe99a07fc7f08696fae62f4", + "sha256:0cd4c848f0e9d805d531e44973c8f48962e20eb7fc0edac3db4f9dbf9ed5ab82", + "sha256:57baf63aeb2965ca4b52613ce78e968b6d2bde700c97f6a7e8c6c236b51ab83e", + "sha256:2954557393cfc9a5c11a5199c7a78cd9c0c793a047552d27b1636da50d013916", + "sha256:7c31dade89634807196a6b20ced831fbd5bec8a21c4e458ea950c9102c3aa96f", + "sha256:1286dd16d0e46d59fa54582725986704a7a3f3d9aca6c5902a7eceb10c60cb7e", + "sha256:697ff63bc5451e0b0db48ad205151123d25683b3754198be7ab5fcb44334e519", + "sha256:fc993c9331d91766d54757bbc70231e29d5ceb2d1ac08b1570feaa0c38ab9582", + "sha256:9d64fed2681552ed642e9c0cc831a9e95ab91de72b47d0cb68b5bf506ba88647", + "sha256:5c3213be557d0468f9df8fe2487eaf2990d9799202c5ff5cb8d394d09fad9b2a" + ], + "version": "==2.7.3.2" + }, + "pytz": { + "hashes": [ + "sha256:80af0f3008046b9975242012a985f04c5df1f01eed4ec1633d56cc47a75a6a48", + "sha256:feb2365914948b8620347784b6b6da356f31c9d03560259070b2f30cff3d469d", + "sha256:59707844a9825589878236ff2f4e0dc9958511b7ffaae94dc615da07d4a68d33", + "sha256:d0ef5ef55ed3d37854320d4926b04a4cb42a2e88f71da9ddfdacfde8e364f027", + "sha256:c41c62827ce9cafacd6f2f7018e4f83a6f1986e87bfd000b8cfbd4ab5da95f1a", + "sha256:8cc90340159b5d7ced6f2ba77694d946fc975b09f1a51d93f3ce3bb399396f94", + "sha256:dd2e4ca6ce3785c8dd342d1853dd9052b19290d5bf66060846e5dc6b8d6667f7", + "sha256:699d18a2a56f19ee5698ab1123bbcc1d269d061996aeb1eda6d89248d3542b82", + "sha256:fae4cffc040921b8a2d60c6cf0b5d662c1190fe54d718271db4eb17d44a185b7" + ], + "version": "==2017.3" + }, + "six": { + "hashes": [ + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" + ], + "version": "==1.11.0" + }, + "unittest-xml-reporting": { + "hashes": [ + "sha256:28ff367b13073e307ded09deb5351ec4037724c1fd28c9c351f4094723ae27c9", + "sha256:9a6d3474bb86331152a798cf6d6d28c3ccee2a09c31ccbe30dbb061bf38ce60b" + ], + "version": "==2.1.0" + }, + "whitenoise": { + "hashes": [ + "sha256:15f43b2e701821b95c9016cf469d29e2a546cb1c7dead584ba82c36f843995cf", + "sha256:9d81515f2b5b27051910996e1e860b1332e354d9e7bcf30c98f21dcb6713e0dd" + ], + "version": "==3.3.1" + } + }, + "develop": {} +} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..2eaea55 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn locallibrary.wsgi --log-file - diff --git a/README.md b/README.md index 52731bf..4a6e042 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ -# python-test +# CircleCI Demo Application: Python / Django + +[![CircleCI](https://circleci.com/gh/CircleCI-Public/circleci-demo-python-django.svg?style=svg)](https://circleci.com/gh/CircleCI-Public/circleci-demo-python-django) + +This is an example application showcasing how to build test and deploy a Django app on CircleCI 2.0. + +You can follow along with this project by reading the [documentation](https://circleci.com/docs/2.0/language-python/). + +## Features of the demos + +- regularly updated to use latest Python and Django (currently Python 3.6.4 and Django 2.0.1) +- uses [pipenv](http://pipenv.readthedocs.io/en/latest/) to install and manage dependencies and virtualenvs on CircleCI +- shows usage of caching on CircleCI 2.0 to speed up builds. Makes use of Ppipfile.lock to invalidate cache if dependencies change +- runs tests against a PostgreSQL database +- store and upload test result in Junit XML format with [unittest-xml-reporting](https://github.com/xmlrunner/unittest-xml-reporting) to enable Test Summary and Insights on CircleCI + +## About the app: django_local_library + +Tutorial "Local Library" website written in Django. This is based on the excellent [MDN Django tutorial.](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Tutorial_local_library_website). + +---- + +This web application creates an online catalog for a small local library, where users can browse available books and manage their accounts. + +The main features that have currently been implemented are: + +* There are models for books, book copies, genre, language and authors. +* Users can view list and detail information for books and authors. +* Admin users can create and manage models. The admin has been optimised (the basic registration is present in admin.py, but commented out). +* Librarians can renew reserved books + +![Local Library Model](https://github.com/mdn/django-locallibrary-tutorial/blob/master/catalog/static/images/local_library_model_uml.png) + +## License Information + +Documentation (guides, references, and associated images) is licensed as Creative Commons Attribution-NonCommercial-ShareAlike CC BY-NC-SA. The full license can be found [here](http://creativecommons.org/licenses/by-nc-sa/4.0/legalcode), and the human-readable summary [here](http://creativecommons.org/licenses/by-nc-sa/4.0/). + +Everything in this repository not covered above is licensed under the [included CC0 license](LICENSE). -python-test \ No newline at end of file diff --git a/catalog/__init__.py b/catalog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/catalog/admin.py b/catalog/admin.py new file mode 100644 index 0000000..ba5e294 --- /dev/null +++ b/catalog/admin.py @@ -0,0 +1,78 @@ +from django.contrib import admin + +# Register your models here. + +from .models import Genre, Book, BookInstance, Language + +""" +# Minimal registration of Models. +admin.site.register(Book) +admin.site.register(Author) +admin.site.register(BookInstance) +admin.site.register(Genre) +admin.site.register(Language) +""" + +admin.site.register(Genre) +admin.site.register(Language) + +class BooksInline(admin.TabularInline): + """ + Defines format of inline book insertion (used in AuthorAdmin) + """ + model = Book + + +@admin.register(Author) +class AuthorAdmin(admin.ModelAdmin): + """ + Administration object for Author models. + Defines: + - fields to be displayed in list view (list_display) + - orders fields in detail view (fields), grouping the date fields horizontally + - adds inline addition of books in author view (inlines) + """ + list_display = ('last_name', 'first_name', 'date_of_birth', 'date_of_death') + fields = ['first_name', 'last_name', ('date_of_birth', 'date_of_death')] + inlines = [BooksInline] + + +class BooksInstanceInline(admin.TabularInline): + """ + Defines format of inline book instance insertion (used in BookAdmin) + """ + model = BookInstance + +class BookAdmin(admin.ModelAdmin): + """ + Administration object for Book models. + Defines: + - fields to be displayed in list view (list_display) + - adds inline addition of book instances in book view (inlines) + """ + list_display = ('title', 'author', 'display_genre') + inlines = [BooksInstanceInline] + +admin.site.register(Book, BookAdmin) + + +@admin.register(BookInstance) +class BookInstanceAdmin(admin.ModelAdmin): + """ + Administration object for BookInstance models. + Defines: + - fields to be displayed in list view (list_display) + - filters that will be displayed in sidebar (list_filter) + - grouping of fields into sections (fieldsets) + """ + list_display = ('book', 'status', 'borrower','due_back', 'id') + list_filter = ('status', 'due_back') + + fieldsets = ( + (None, { + 'fields': ('book','imprint', 'id') + }), + ('Availability', { + 'fields': ('status', 'due_back','borrower') + }), + ) \ No newline at end of file diff --git a/catalog/apps.py b/catalog/apps.py new file mode 100644 index 0000000..ac42aa1 --- /dev/null +++ b/catalog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CatalogConfig(AppConfig): + name = 'catalog' diff --git a/catalog/forms.py b/catalog/forms.py new file mode 100644 index 0000000..5059c32 --- /dev/null +++ b/catalog/forms.py @@ -0,0 +1,24 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ +import datetime #for checking renewal date range. + +from django import forms + +class RenewBookForm(forms.Form): + """ + Form for a librarian to renew books. + """ + renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).") + + def clean_renewal_date(self): + data = self.cleaned_data['renewal_date'] + + #Check date is not in past. + if data < datetime.date.today(): + raise ValidationError(_('Invalid date - renewal in past')) + #Check date is in range librarian allowed to change (+4 weeks) + if data > datetime.date.today() + datetime.timedelta(weeks=4): + raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead')) + + # Remember to always return the cleaned data. + return data \ No newline at end of file diff --git a/catalog/migrations/0001_initial.py b/catalog/migrations/0001_initial.py new file mode 100644 index 0000000..592c625 --- /dev/null +++ b/catalog/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 03:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Author', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ], + ), + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('summary', models.CharField(max_length=200)), + ('imprint', models.CharField(max_length=200)), + ('isbn', models.CharField(max_length=13)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalog.Author')), + ], + ), + migrations.CreateModel( + name='Subject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject_name', models.CharField(max_length=200)), + ], + ), + migrations.AddField( + model_name='book', + name='subject', + field=models.ManyToManyField(to='catalog.Subject'), + ), + ] diff --git a/catalog/migrations/0002_auto_20160921_1401.py b/catalog/migrations/0002_auto_20160921_1401.py new file mode 100644 index 0000000..68d0126 --- /dev/null +++ b/catalog/migrations/0002_auto_20160921_1401.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 04:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='author', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Author'), + ), + ] diff --git a/catalog/migrations/0003_auto_20160921_1420.py b/catalog/migrations/0003_auto_20160921_1420.py new file mode 100644 index 0000000..ffd80c8 --- /dev/null +++ b/catalog/migrations/0003_auto_20160921_1420.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 04:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0002_auto_20160921_1401'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='summary', + field=models.TextField(help_text='Enter a brief description of the book', max_length=200), + ), + migrations.AlterField( + model_name='subject', + name='subject_name', + field=models.CharField(help_text='Enter a book category - e.g. Science Fiction, Non Fiction etc.', max_length=200), + ), + ] diff --git a/catalog/migrations/0004_auto_20160921_1422.py b/catalog/migrations/0004_auto_20160921_1422.py new file mode 100644 index 0000000..e94dab7 --- /dev/null +++ b/catalog/migrations/0004_auto_20160921_1422.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 04:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0003_auto_20160921_1420'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='summary', + field=models.TextField(help_text='Enter a brief description of the book', max_length=1000), + ), + ] diff --git a/catalog/migrations/0005_auto_20160921_1433.py b/catalog/migrations/0005_auto_20160921_1433.py new file mode 100644 index 0000000..4dbcc06 --- /dev/null +++ b/catalog/migrations/0005_auto_20160921_1433.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 04:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0004_auto_20160921_1422'), + ] + + operations = [ + migrations.RemoveField( + model_name='author', + name='name', + ), + migrations.AddField( + model_name='author', + name='first_name', + field=models.CharField(default='Ben', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='author', + name='last_name', + field=models.CharField(default='Bova', max_length=100), + preserve_default=False, + ), + migrations.AlterField( + model_name='book', + name='isbn', + field=models.CharField(help_text='13 Character ISBN number', max_length=13), + ), + migrations.AlterField( + model_name='book', + name='subject', + field=models.ManyToManyField(help_text='Select a grouping category for this book', to='catalog.Subject', verbose_name='Category'), + ), + ] diff --git a/catalog/migrations/0006_auto_20160921_1439.py b/catalog/migrations/0006_auto_20160921_1439.py new file mode 100644 index 0000000..7d9a3dd --- /dev/null +++ b/catalog/migrations/0006_auto_20160921_1439.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 04:39 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0005_auto_20160921_1433'), + ] + + operations = [ + migrations.AddField( + model_name='author', + name='date_of_birth', + field=models.DateField(null=True, verbose_name='D.O.B'), + ), + migrations.AddField( + model_name='author', + name='date_of_death', + field=models.DateField(null=True, verbose_name='Died'), + ), + ] diff --git a/catalog/migrations/0007_auto_20160921_1444.py b/catalog/migrations/0007_auto_20160921_1444.py new file mode 100644 index 0000000..cf3878f --- /dev/null +++ b/catalog/migrations/0007_auto_20160921_1444.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 04:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0006_auto_20160921_1439'), + ] + + operations = [ + migrations.AlterField( + model_name='author', + name='date_of_birth', + field=models.DateField(blank=True, null=True, verbose_name='D.O.B'), + ), + migrations.AlterField( + model_name='author', + name='date_of_death', + field=models.DateField(blank=True, null=True, verbose_name='Died'), + ), + ] diff --git a/catalog/migrations/0008_auto_20160921_1511.py b/catalog/migrations/0008_auto_20160921_1511.py new file mode 100644 index 0000000..83f38d4 --- /dev/null +++ b/catalog/migrations/0008_auto_20160921_1511.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 05:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0007_auto_20160921_1444'), + ] + + operations = [ + migrations.CreateModel( + name='BookInstance', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique ID for this particular book across whole library', primary_key=True, serialize=False)), + ('summary', models.TextField(help_text='Enter a brief description of the book', max_length=1000)), + ('imprint', models.CharField(max_length=200)), + ('due_back', models.DateField(blank=True, null=True)), + ('status', models.CharField(blank=True, choices=[('d', 'maintenance'), ('o', 'on loan'), ('a', 'available'), ('r', 'reserved')], default='d', help_text='Book availability', max_length=1)), + ], + ), + migrations.RemoveField( + model_name='book', + name='imprint', + ), + migrations.AlterField( + model_name='author', + name='date_of_birth', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='bookinstance', + name='book', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Book'), + ), + ] diff --git a/catalog/migrations/0009_remove_bookinstance_summary.py b/catalog/migrations/0009_remove_bookinstance_summary.py new file mode 100644 index 0000000..4755f27 --- /dev/null +++ b/catalog/migrations/0009_remove_bookinstance_summary.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 05:14 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0008_auto_20160921_1511'), + ] + + operations = [ + migrations.RemoveField( + model_name='bookinstance', + name='summary', + ), + ] diff --git a/catalog/migrations/0010_auto_20160921_1527.py b/catalog/migrations/0010_auto_20160921_1527.py new file mode 100644 index 0000000..59252c1 --- /dev/null +++ b/catalog/migrations/0010_auto_20160921_1527.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-21 05:27 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0009_remove_bookinstance_summary'), + ] + + operations = [ + migrations.AlterField( + model_name='bookinstance', + name='book', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='Fishcakes instance+', to='catalog.Book'), + ), + ] diff --git a/catalog/migrations/0011_auto_20160922_1029.py b/catalog/migrations/0011_auto_20160922_1029.py new file mode 100644 index 0000000..168eb1d --- /dev/null +++ b/catalog/migrations/0011_auto_20160922_1029.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-22 00:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0010_auto_20160921_1527'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bookinstance', + options={'ordering': ['due_back']}, + ), + migrations.AlterField( + model_name='bookinstance', + name='book', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Book'), + ), + migrations.AlterField( + model_name='bookinstance', + name='id', + field=models.UUIDField(default=uuid.uuid4, help_text='Unique ID for this particular book across whole library', primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='bookinstance', + name='status', + field=models.CharField(blank=True, choices=[('d', 'Maintenance'), ('o', 'On loan'), ('a', 'Available'), ('r', 'Reserved')], default='d', help_text='Book availability', max_length=1), + ), + ] diff --git a/catalog/migrations/0012_bookinstance_date_acquired.py b/catalog/migrations/0012_bookinstance_date_acquired.py new file mode 100644 index 0000000..f8f91e0 --- /dev/null +++ b/catalog/migrations/0012_bookinstance_date_acquired.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-26 08:27 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0011_auto_20160922_1029'), + ] + + operations = [ + migrations.AddField( + model_name='bookinstance', + name='date_acquired', + field=models.DateField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/catalog/migrations/0013_auto_20160926_1901.py b/catalog/migrations/0013_auto_20160926_1901.py new file mode 100644 index 0000000..9742c61 --- /dev/null +++ b/catalog/migrations/0013_auto_20160926_1901.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-26 09:01 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0012_bookinstance_date_acquired'), + ] + + operations = [ + migrations.AlterField( + model_name='bookinstance', + name='date_acquired', + field=models.DateField(default=datetime.date.today), + ), + ] diff --git a/catalog/migrations/0014_remove_bookinstance_date_acquired.py b/catalog/migrations/0014_remove_bookinstance_date_acquired.py new file mode 100644 index 0000000..c328a59 --- /dev/null +++ b/catalog/migrations/0014_remove_bookinstance_date_acquired.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-26 09:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0013_auto_20160926_1901'), + ] + + operations = [ + migrations.RemoveField( + model_name='bookinstance', + name='date_acquired', + ), + ] diff --git a/catalog/migrations/0015_auto_20160927_1808.py b/catalog/migrations/0015_auto_20160927_1808.py new file mode 100644 index 0000000..35a22da --- /dev/null +++ b/catalog/migrations/0015_auto_20160927_1808.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-27 08:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0014_remove_bookinstance_date_acquired'), + ] + + operations = [ + migrations.RemoveField( + model_name='subject', + name='subject_name', + ), + migrations.AddField( + model_name='subject', + name='name', + field=models.CharField(default='Fantasy', help_text='Enter a book category - e.g. Science Fiction, French Poetry etc.', max_length=200), + preserve_default=False, + ), + ] diff --git a/catalog/migrations/0016_auto_20160927_1947.py b/catalog/migrations/0016_auto_20160927_1947.py new file mode 100644 index 0000000..88c3e99 --- /dev/null +++ b/catalog/migrations/0016_auto_20160927_1947.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-09-27 09:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0015_auto_20160927_1808'), + ] + + operations = [ + migrations.CreateModel( + name='Genre', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Enter a book genre (e.g. Science Fiction, French Poetry etc.)', max_length=200)), + ], + ), + migrations.RemoveField( + model_name='book', + name='subject', + ), + migrations.AlterField( + model_name='book', + name='isbn', + field=models.CharField(help_text='13 Character ISBN number', max_length=13, verbose_name='ISBN'), + ), + migrations.DeleteModel( + name='Subject', + ), + migrations.AddField( + model_name='book', + name='genre', + field=models.ManyToManyField(help_text='Select a genre for this book', to='catalog.Genre'), + ), + ] diff --git a/catalog/migrations/0017_language.py b/catalog/migrations/0017_language.py new file mode 100644 index 0000000..09f7ea0 --- /dev/null +++ b/catalog/migrations/0017_language.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-10-05 10:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0016_auto_20160927_1947'), + ] + + operations = [ + migrations.CreateModel( + name='Language', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text="Enter a the book's natural language (e.g. English, French, Japanese etc.)", max_length=200)), + ], + ), + ] diff --git a/catalog/migrations/0018_book_language.py b/catalog/migrations/0018_book_language.py new file mode 100644 index 0000000..9327c4a --- /dev/null +++ b/catalog/migrations/0018_book_language.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-10-05 10:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0017_language'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='language', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Language'), + ), + ] diff --git a/catalog/migrations/0019_bookinstance_borrower.py b/catalog/migrations/0019_bookinstance_borrower.py new file mode 100644 index 0000000..20ee34e --- /dev/null +++ b/catalog/migrations/0019_bookinstance_borrower.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-10-11 09:40 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('catalog', '0018_book_language'), + ] + + operations = [ + migrations.AddField( + model_name='bookinstance', + name='borrower', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/catalog/migrations/0020_auto_20161012_1044.py b/catalog/migrations/0020_auto_20161012_1044.py new file mode 100644 index 0000000..9962af8 --- /dev/null +++ b/catalog/migrations/0020_auto_20161012_1044.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-10-11 23:44 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0019_bookinstance_borrower'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bookinstance', + options={'ordering': ['due_back'], 'permissions': (('can_mark_returned', 'Set book as returned'),)}, + ), + ] diff --git a/catalog/migrations/0021_auto_20170504_1512.py b/catalog/migrations/0021_auto_20170504_1512.py new file mode 100644 index 0000000..cebaa67 --- /dev/null +++ b/catalog/migrations/0021_auto_20170504_1512.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2017-05-04 15:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0020_auto_20161012_1044'), + ] + + operations = [ + migrations.AlterField( + model_name='author', + name='date_of_death', + field=models.DateField(blank=True, null=True, verbose_name='died'), + ), + ] diff --git a/catalog/migrations/0021_auto_20171229_1056.py b/catalog/migrations/0021_auto_20171229_1056.py new file mode 100644 index 0000000..ec5ae32 --- /dev/null +++ b/catalog/migrations/0021_auto_20171229_1056.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0 on 2017-12-29 10:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0020_auto_20161012_1044'), + ] + + operations = [ + migrations.AlterField( + model_name='author', + name='date_of_death', + field=models.DateField(blank=True, null=True, verbose_name='died'), + ), + ] diff --git a/catalog/migrations/0022_merge_20180115_2033.py b/catalog/migrations/0022_merge_20180115_2033.py new file mode 100644 index 0000000..7aa8279 --- /dev/null +++ b/catalog/migrations/0022_merge_20180115_2033.py @@ -0,0 +1,14 @@ +# Generated by Django 2.0.1 on 2018-01-15 20:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0021_auto_20171229_1056'), + ('catalog', '0021_auto_20170504_1512'), + ] + + operations = [ + ] diff --git a/catalog/migrations/0023_auto_20200902_1539.py b/catalog/migrations/0023_auto_20200902_1539.py new file mode 100644 index 0000000..499a58a --- /dev/null +++ b/catalog/migrations/0023_auto_20200902_1539.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.1 on 2020-09-02 15:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0022_merge_20180115_2033'), + ] + + operations = [ + migrations.AlterModelOptions( + name='author', + options={'ordering': ['last_name', 'first_name']}, + ), + ] diff --git a/catalog/migrations/__init__.py b/catalog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/catalog/models.py b/catalog/models.py new file mode 100644 index 0000000..6e51a25 --- /dev/null +++ b/catalog/models.py @@ -0,0 +1,136 @@ +from django.db import models + +# Create your models here. + +from django.urls import reverse #Used to generate urls by reversing the URL patterns + + +class Genre(models.Model): + """ + Model representing a book genre (e.g. Science Fiction, Non Fiction). + """ + name = models.CharField(max_length=200, help_text="Enter a book genre (e.g. Science Fiction, French Poetry etc.)") + + def __str__(self): + """ + String for representing the Model object (in Admin site etc.) + """ + return self.name + + +class Language(models.Model): + """ + Model representing a Language (e.g. English, French, Japanese, etc.) + """ + name = models.CharField(max_length=200, help_text="Enter a the book's natural language (e.g. English, French, Japanese etc.)") + + def __str__(self): + """ + String for representing the Model object (in Admin site etc.) + """ + return self.name + + +class Book(models.Model): + """ + Model representing a book (but not a specific copy of a book). + """ + title = models.CharField(max_length=200) + author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True) + # Foreign Key used because book can only have one author, but authors can have multiple books + # Author as a string rather than object because it hasn't been declared yet in file. + summary = models.TextField(max_length=1000, help_text="Enter a brief description of the book") + isbn = models.CharField('ISBN',max_length=13, help_text='13 Character ISBN number') + genre = models.ManyToManyField(Genre, help_text="Select a genre for this book") + # ManyToManyField used because Subject can contain many books. Books can cover many subjects. + # Subject declared as an object because it has already been defined. + language = models.ForeignKey('Language', on_delete=models.SET_NULL, null=True) + + def display_genre(self): + """ + Creates a string for the Genre. This is required to display genre in Admin. + """ + return ', '.join([ genre.name for genre in self.genre.all()[:3] ]) + display_genre.short_description = 'Genre' + + + def get_absolute_url(self): + """ + Returns the url to access a particular book instance. + """ + return reverse('book-detail', args=[str(self.id)]) + + def __str__(self): + """ + String for representing the Model object. + """ + return self.title + + +import uuid # Required for unique book instances +from datetime import date + +# from django.contrib.auth.models import User #Required to assign User as a borrower + +class BookInstance(models.Model): + """ + Model representing a specific copy of a book (i.e. that can be borrowed from the library). + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text="Unique ID for this particular book across whole library") + book = models.ForeignKey('Book', on_delete=models.SET_NULL, null=True) + imprint = models.CharField(max_length=200) + due_back = models.DateField(null=True, blank=True) + borrower = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + + @property + def is_overdue(self): + if self.due_back and date.today() > self.due_back: + return True + return False + + + LOAN_STATUS = ( + ('d', 'Maintenance'), + ('o', 'On loan'), + ('a', 'Available'), + ('r', 'Reserved'), + ) + + status= models.CharField(max_length=1, choices=LOAN_STATUS, blank=True, default='d', help_text='Book availability') + + class Meta: + ordering = ["due_back"] + permissions = (("can_mark_returned", "Set book as returned"),) + + def __str__(self): + """ + String for representing the Model object. + """ + #return '%s (%s)' % (self.id,self.book.title) + return '{0} ({1})'.format(self.id,self.book.title) + + +class Author(models.Model): + """ + Model representing an author. + """ + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + date_of_birth = models.DateField(null=True, blank=True) + date_of_death = models.DateField('died', null=True, blank=True) + + class Meta: + ordering = ["last_name","first_name"] + + def get_absolute_url(self): + """ + Returns the url to access a particular author instance. + """ + return reverse('author-detail', args=[str(self.id)]) + + + def __str__(self): + """ + String for representing the Model object. + """ + return '{0}, {1}'.format(self.last_name,self.first_name) diff --git a/catalog/static/css/styles.css b/catalog/static/css/styles.css new file mode 100644 index 0000000..5b791ad --- /dev/null +++ b/catalog/static/css/styles.css @@ -0,0 +1,6 @@ +.sidebar-nav { + margin-top: 20px; + padding: 0; + list-style: none; +} + diff --git a/catalog/static/images/local_library_model_uml.png b/catalog/static/images/local_library_model_uml.png new file mode 100644 index 0000000000000000000000000000000000000000..12c6bc5ac9d0bc1c3ed78531b0612fe08c46fac4 GIT binary patch literal 21273 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 RvL65eHnjdVRJ|@P@=qb6Ss4HT literal 0 HcmV?d00001 diff --git a/catalog/templates/base_generic.html b/catalog/templates/base_generic.html new file mode 100644 index 0000000..c73ccce --- /dev/null +++ b/catalog/templates/base_generic.html @@ -0,0 +1,78 @@ + + + + + {% block title %}Local Library{% endblock %} + + + + + + + + {% load static %} + + + + +
+ +
+
+ {% block sidebar %} + + + + + {% if user.is_staff %} +
+ + {% endif %} + +{% endblock %} +
+
+ {% block content %}{% endblock %} + + {% block pagination %} + {% if is_paginated %} + + {% endif %} + {% endblock %} + + +
+
+ +
+ + diff --git a/catalog/templates/catalog/author_confirm_delete.html b/catalog/templates/catalog/author_confirm_delete.html new file mode 100644 index 0000000..2ab7632 --- /dev/null +++ b/catalog/templates/catalog/author_confirm_delete.html @@ -0,0 +1,14 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

Delete Author

+ +

Are you sure you want to delete the author: {{ author }}?

+ +
+ {% csrf_token %} + +
+ +{% endblock %} diff --git a/catalog/templates/catalog/author_detail.html b/catalog/templates/catalog/author_detail.html new file mode 100644 index 0000000..cb7f5f0 --- /dev/null +++ b/catalog/templates/catalog/author_detail.html @@ -0,0 +1,19 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

Author: {{ author }}

+

{{author.date_of_birth}} - {% if author.date_of_death %}{{author.date_of_death}}{% endif %}

+ +
+

Books

+ +
+{% for book in author.book_set.all %} +
{{book}} ({{book.bookinstance_set.all.count}})
+
{{book.summary}}
+{% endfor %} +
+ +
+{% endblock %} diff --git a/catalog/templates/catalog/author_form.html b/catalog/templates/catalog/author_form.html new file mode 100644 index 0000000..e3325e0 --- /dev/null +++ b/catalog/templates/catalog/author_form.html @@ -0,0 +1,13 @@ +{% extends "base_generic.html" %} + +{% block content %} + +
+ {% csrf_token %} + + {{ form.as_table }} +
+ + +
+{% endblock %} diff --git a/catalog/templates/catalog/author_list.html b/catalog/templates/catalog/author_list.html new file mode 100644 index 0000000..a1120fd --- /dev/null +++ b/catalog/templates/catalog/author_list.html @@ -0,0 +1,26 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

Author List

+ +{% if author_list %} + +{% else %} +

There are no authors available.

+{% endif %} + + + +{% endblock %} + diff --git a/catalog/templates/catalog/book_confirm_delete.html b/catalog/templates/catalog/book_confirm_delete.html new file mode 100644 index 0000000..2e72248 --- /dev/null +++ b/catalog/templates/catalog/book_confirm_delete.html @@ -0,0 +1,14 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

Delete Book

+ +

Are you sure you want to delete the book: {{ book }}?

+ +
+ {% csrf_token %} + +
+ +{% endblock %} diff --git a/catalog/templates/catalog/book_detail.html b/catalog/templates/catalog/book_detail.html new file mode 100644 index 0000000..82d6343 --- /dev/null +++ b/catalog/templates/catalog/book_detail.html @@ -0,0 +1,26 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

Title: {{ book.title }}

+ +

Author: {{ book.author }}

+

Summary: {{ book.summary }}

+

ISBN: {{ book.isbn }}

+

Language: {{ book.language }}

+

Genre: {% for genre in book.genre.all %}{{genre}}{% if not forloop.last %}, {% endif %}{% endfor %}

+ +
+

Copies

+ +{% for copy in book.bookinstance_set.all %} +
+

{{ copy.get_status_display }}

+{% if copy.status != 'a' %}

Due to be returned: {{copy.due_back}}

{% endif %} +

Imprint: {{copy.imprint}}

+

Id: {{copy.id}}

+ +{% endfor %} +
+{% endblock %} + diff --git a/catalog/templates/catalog/book_form.html b/catalog/templates/catalog/book_form.html new file mode 100644 index 0000000..e3325e0 --- /dev/null +++ b/catalog/templates/catalog/book_form.html @@ -0,0 +1,13 @@ +{% extends "base_generic.html" %} + +{% block content %} + +
+ {% csrf_token %} + + {{ form.as_table }} +
+ + +
+{% endblock %} diff --git a/catalog/templates/catalog/book_list.html b/catalog/templates/catalog/book_list.html new file mode 100644 index 0000000..4fd4174 --- /dev/null +++ b/catalog/templates/catalog/book_list.html @@ -0,0 +1,21 @@ +{% extends "base_generic.html" %} + +{% block content %} +

Book List

+ + {% if book_list %} +
    + + {% for book in book_list %} +
  • + {{ book.title }} ({{book.author}}) +
  • + {% endfor %} + +
+ + {% else %} +

There are no books in the library.

+ {% endif %} +{% endblock %} + diff --git a/catalog/templates/catalog/book_renew_librarian.html b/catalog/templates/catalog/book_renew_librarian.html new file mode 100644 index 0000000..56e5ef9 --- /dev/null +++ b/catalog/templates/catalog/book_renew_librarian.html @@ -0,0 +1,17 @@ +{% extends "base_generic.html" %} + +{% block content %} +

Renew: {{bookinst.book.title}}

+

Borrower: {{bookinst.borrower}}

+ Due date: {{bookinst.due_back}}

+ +
+ {% csrf_token %} + + {{ form }} +
+ +
+ +{% endblock %} + diff --git a/catalog/templates/catalog/bookinstance_list_borrowed_all.html b/catalog/templates/catalog/bookinstance_list_borrowed_all.html new file mode 100644 index 0000000..4802eaf --- /dev/null +++ b/catalog/templates/catalog/bookinstance_list_borrowed_all.html @@ -0,0 +1,19 @@ +{% extends "base_generic.html" %} + +{% block content %} +

All Borrowed Books

+ + {% if bookinstance_list %} +
    + + {% for bookinst in bookinstance_list %} +
  • + {{bookinst.book.title}} ({{ bookinst.due_back }}) {% if user.is_staff %}- {{ bookinst.borrower }}{% endif %} {% if perms.catalog.can_mark_returned %}- Renew {% endif %} +
  • + {% endfor %} +
+ + {% else %} +

There are no books borrowed.

+ {% endif %} +{% endblock %} diff --git a/catalog/templates/catalog/bookinstance_list_borrowed_user.html b/catalog/templates/catalog/bookinstance_list_borrowed_user.html new file mode 100644 index 0000000..5a3614e --- /dev/null +++ b/catalog/templates/catalog/bookinstance_list_borrowed_user.html @@ -0,0 +1,20 @@ +{% extends "base_generic.html" %} + +{% block content %} +

Borrowed books

+ + {% if bookinstance_list %} +
    + + {% for bookinst in bookinstance_list %} +
  • + {{bookinst.book.title}} ({{ bookinst.due_back }}) +
  • + {% endfor %} +
+ + {% else %} +

There are no books borrowed.

+ {% endif %} +{% endblock %} + diff --git a/catalog/templates/index.html b/catalog/templates/index.html new file mode 100644 index 0000000..632f60e --- /dev/null +++ b/catalog/templates/index.html @@ -0,0 +1,32 @@ +{% extends "base_generic.html" %} + +{% block content %} +

Local Library Home

+ +

Welcome to LocalLibrary, a very basic Django website developed as a tutorial example on the Mozilla Developer Network.

+

The tutorial demonstrates how to create a Django skeleton website and application, define URL mappings, views (including Generic List and Detail Views), models and templates.

+ + +

UML Models

+

An UML diagram of the site's Django model structure is shown below.

+ +
+{% load static %} +My image +
+ + +

Dynamic content

+ +

The library has the following record counts:

+
    +
  • Books: {{ num_books }}
  • +
  • Copies: {{ num_instances }}
  • +
  • Copies available: {{ num_instances_available }}
  • +
  • Authors: {{ num_authors }}
  • +
+ + +You have visited this page {{ num_visits }} times. + +{% endblock %} \ No newline at end of file diff --git a/catalog/tests/__init__.py b/catalog/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/catalog/tests/test_forms.py b/catalog/tests/test_forms.py new file mode 100644 index 0000000..52cf8ac --- /dev/null +++ b/catalog/tests/test_forms.py @@ -0,0 +1,61 @@ +from django.test import TestCase + +# Create your tests here. + +import datetime +from django.utils import timezone +from catalog.forms import RenewBookForm + +class RenewBookFormTest(TestCase): + + def test_renew_form_date_in_past(self): + """ + Test form is invalid if renewal_date is before today + """ + date = datetime.date.today() - datetime.timedelta(days=1) + form_data = {'renewal_date': date} + form = RenewBookForm(data=form_data) + self.assertFalse(form.is_valid()) + + def test_renew_form_date_too_far_in_future(self): + """ + Test form is invalid if renewal_date more than 4 weeks from today + """ + date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1) + form_data = {'renewal_date': date} + form = RenewBookForm(data=form_data) + self.assertFalse(form.is_valid()) + + def test_renew_form_date_today(self): + """ + Test form is valid if renewal_date is today + """ + date = datetime.date.today() + form_data = {'renewal_date': date} + form = RenewBookForm(data=form_data) + self.assertTrue(form.is_valid()) + + def test_renew_form_date_max(self): + """ + Test form is valid if renewal_date is within 4 weeks + """ + date = timezone.now() + datetime.timedelta(weeks=4) + form_data = {'renewal_date': date} + form = RenewBookForm(data=form_data) + self.assertTrue(form.is_valid()) + + + def test_renew_form_date_field_label(self): + """ + Test renewal_date label is "renewal date" + """ + form = RenewBookForm() + self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date') + + + def test_renew_form_date_field_help_text(self): + """ + Test renewal_date help_text is as expected. + """ + form = RenewBookForm() + self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).') \ No newline at end of file diff --git a/catalog/tests/test_models.py b/catalog/tests/test_models.py new file mode 100644 index 0000000..3e5fed9 --- /dev/null +++ b/catalog/tests/test_models.py @@ -0,0 +1,56 @@ +from django.test import TestCase + +# Create your tests here. + +import django.db +from ..models import Author + +class AuthorModelTest(TestCase): + + @classmethod + def setUpTestData(cls): + #Set up non-modified objects used by all test methods + Author.objects.create(first_name='Big', last_name='Bob') + + def test_first_name_label(self): + author=Author.objects.get(id=1) + field_label = author._meta.get_field('first_name').verbose_name + self.assertEquals(field_label,'first name') + + def test_last_name_label(self): + author=Author.objects.get(id=1) + field_label = author._meta.get_field('last_name').verbose_name + self.assertEquals(field_label,'last name') + + def test_date_of_birth_label(self): + author=Author.objects.get(id=1) + field_label = author._meta.get_field('date_of_birth').verbose_name + self.assertEquals(field_label,'date of birth') + + def test_date_of_death_label(self): + author=Author.objects.get(id=1) + field_label = author._meta.get_field('date_of_death').verbose_name + self.assertEquals(field_label,'died') + + def test_first_name_max_length(self): + author=Author.objects.get(id=1) + max_length = author._meta.get_field('first_name').max_length + self.assertEquals(max_length,100) + + def test_last_name_max_length(self): + author=Author.objects.get(id=1) + max_length = author._meta.get_field('last_name').max_length + self.assertEquals(max_length,100) + + def test_object_name_is_last_name_comma_first_name(self): + author=Author.objects.get(id=1) + #expected_object_name = '%s, %s' % (author.last_name, author.first_name) + expected_object_name = '{0}, {1}'.format(author.last_name,author.first_name) + + self.assertEquals(expected_object_name,str(author)) + + def test_get_absolute_url(self): + author=Author.objects.get(id=1) + #This will also fail if the urlconf is not defined. + self.assertEquals(author.get_absolute_url(),'/catalog/author/1') + diff --git a/catalog/tests/test_views.py b/catalog/tests/test_views.py new file mode 100644 index 0000000..7a5e40e --- /dev/null +++ b/catalog/tests/test_views.py @@ -0,0 +1,343 @@ +from django.test import TestCase + +# Create your tests here. +import django.db +from ..models import Author +from django.urls import reverse + +class AuthorListViewTest(TestCase): + + @classmethod + def setUpTestData(cls): + #Create authors for pagination tests + number_of_authors = 13 + for author_num in range(number_of_authors): + #Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,) + Author.objects.create(first_name='Christian {0}'.format(author_num), last_name = 'Surname {0}'.format(author_num) ) + + def test_view_url_exists_at_desired_location(self): + resp = self.client.get('/catalog/authors/') + self.assertEqual(resp.status_code, 200) + + def test_view_url_accessible_by_name(self): + resp = self.client.get(reverse('authors')) + self.assertEqual(resp.status_code, 200) + + def test_view_uses_correct_template(self): + resp = self.client.get(reverse('authors')) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, 'catalog/author_list.html') + + def test_pagination_is_ten(self): + resp = self.client.get(reverse('authors')) + self.assertEqual(resp.status_code, 200) + self.assertTrue('is_paginated' in resp.context) + self.assertTrue(resp.context['is_paginated'] == True) + self.assertTrue( len(resp.context['author_list']) == 10) + + def test_lists_all_authors(self): + #Get second page and confirm it has (exactly) the remaining 3 items + resp = self.client.get(reverse('authors')+'?page=2') + self.assertEqual(resp.status_code, 200) + self.assertTrue('is_paginated' in resp.context) + self.assertTrue(resp.context['is_paginated'] == True) + self.assertTrue( len(resp.context['author_list']) == 3) + + +import datetime +from django.utils import timezone + +from catalog.models import BookInstance, Book, Genre, Language +from django.contrib.auth.models import User #Required to assign User as a borrower + +class LoanedBookInstancesByUserListViewTest(TestCase): + + def setUp(self): + #Create two users + test_user1 = User.objects.create_user(username='testuser1', password='12345') + test_user1.save() + test_user2 = User.objects.create_user(username='testuser2', password='12345') + test_user2.save() + + #Create a book + test_author = Author.objects.create(first_name='John', last_name='Smith') + test_genre = Genre.objects.create(name='Fantasy') + test_language = Language.objects.create(name='English') + test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,) + # Create genre as a post-step + genre_objects_for_book = Genre.objects.all() + test_book.genre.set(genre_objects_for_book) + test_book.save() + + #Create 30 BookInstance objects + number_of_book_copies = 30 + for book_copy in range(number_of_book_copies): + return_date= timezone.now() + datetime.timedelta(days=book_copy%5) + if book_copy % 2: + the_borrower=test_user1 + else: + the_borrower=test_user2 + status='m' + BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status) + + def test_redirect_if_not_logged_in(self): + resp = self.client.get(reverse('my-borrowed')) + self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/') + + def test_logged_in_uses_correct_template(self): + login = self.client.login(username='testuser1', password='12345') + resp = self.client.get(reverse('my-borrowed')) + + #Check our user is logged in + self.assertEqual(str(resp.context['user']), 'testuser1') + #Check that we got a response "success" + self.assertEqual(resp.status_code, 200) + + #Check we used correct template + self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html') + + def test_only_borrowed_books_in_list(self): + login = self.client.login(username='testuser1', password='12345') + resp = self.client.get(reverse('my-borrowed')) + + #Check our user is logged in + self.assertEqual(str(resp.context['user']), 'testuser1') + #Check that we got a response "success" + self.assertEqual(resp.status_code, 200) + + #Check that initially we don't have any books in list (none on loan) + self.assertTrue('bookinstance_list' in resp.context) + self.assertEqual( len(resp.context['bookinstance_list']),0) + + #Now change all books to be on loan + get_ten_books = BookInstance.objects.all()[:10] + + for copy in get_ten_books: + copy.status='o' + copy.save() + + #Check that now we have borrowed books in the list + resp = self.client.get(reverse('my-borrowed')) + #Check our user is logged in + self.assertEqual(str(resp.context['user']), 'testuser1') + #Check that we got a response "success" + self.assertEqual(resp.status_code, 200) + + self.assertTrue('bookinstance_list' in resp.context) + + #Confirm all books belong to testuser1 and are on loan + for bookitem in resp.context['bookinstance_list']: + self.assertEqual(resp.context['user'], bookitem.borrower) + self.assertEqual('o', bookitem.status) + + def test_pages_paginated_to_ten(self): + + #Change all books to be on loan. + #This should make 15 test user ones. + for copy in BookInstance.objects.all(): + copy.status='o' + copy.save() + + login = self.client.login(username='testuser1', password='12345') + resp = self.client.get(reverse('my-borrowed')) + + #Check our user is logged in + self.assertEqual(str(resp.context['user']), 'testuser1') + #Check that we got a response "success" + self.assertEqual(resp.status_code, 200) + + #Confirm that only 10 items are displayed due to pagination (if pagination not enabled, there would be 15 returned) + self.assertEqual( len(resp.context['bookinstance_list']),10) + + def test_pages_ordered_by_due_date(self): + + #Change all books to be on loan + for copy in BookInstance.objects.all(): + copy.status='o' + copy.save() + + login = self.client.login(username='testuser1', password='12345') + resp = self.client.get(reverse('my-borrowed')) + + #Check our user is logged in + self.assertEqual(str(resp.context['user']), 'testuser1') + #Check that we got a response "success" + self.assertEqual(resp.status_code, 200) + + #Confirm that of the items, only 10 are displayed due to pagination. + self.assertEqual( len(resp.context['bookinstance_list']),10) + + last_date=0 + for copy in resp.context['bookinstance_list']: + if last_date==0: + last_date=copy.due_back + else: + self.assertTrue(last_date <= copy.due_back) + + + +from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned. + +class RenewBookInstancesViewTest(TestCase): + + def setUp(self): + #Create a user + test_user1 = User.objects.create_user(username='testuser1', password='12345') + test_user1.save() + + test_user2 = User.objects.create_user(username='testuser2', password='12345') + test_user2.save() + permission = Permission.objects.get(name='Set book as returned') + test_user2.user_permissions.add(permission) + test_user2.save() + + #Create a book + test_author = Author.objects.create(first_name='John', last_name='Smith') + test_genre = Genre.objects.create(name='Fantasy') + test_language = Language.objects.create(name='English') + test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,) + # Create genre as a post-step + genre_objects_for_book = Genre.objects.all() + test_book.genre.set(genre_objects_for_book) + test_book.save() + + #Create a BookInstance object for test_user1 + return_date= datetime.date.today() + datetime.timedelta(days=5) + self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o') + + #Create a BookInstance object for test_user2 + return_date= datetime.date.today() + datetime.timedelta(days=5) + self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o') + + def test_redirect_if_not_logged_in(self): + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) ) + #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable) + self.assertEqual( resp.status_code,302) + self.assertTrue( resp.url.startswith('/accounts/login/') ) + + def test_redirect_if_logged_in_but_not_correct_permission(self): + login = self.client.login(username='testuser1', password='12345') + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) ) + + #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable) + self.assertEqual( resp.status_code,302) + self.assertTrue( resp.url.startswith('/accounts/login/') ) + + def test_logged_in_with_permission_borrowed_book(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance2.pk,}) ) + + #Check that it lets us login - this is our book and we have the right permissions. + self.assertEqual( resp.status_code,200) + + def test_logged_in_with_permission_another_users_borrowed_book(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) ) + + #Check that it lets us login. We're a librarian, so we can view any users book + self.assertEqual( resp.status_code,200) + + def test_uses_correct_template(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) ) + self.assertEqual( resp.status_code,200) + + #Check we used correct template + self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html') + + def test_form_renewal_date_initially_has_date_three_weeks_in_future(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) ) + self.assertEqual( resp.status_code,200) + + date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3) + self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future ) + + def test_form_invalid_renewal_date_past(self): + login = self.client.login(username='testuser2', password='12345') + + date_in_past = datetime.date.today() - datetime.timedelta(weeks=1) + resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':date_in_past} ) + self.assertEqual( resp.status_code,200) + self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past') + + def test_form_invalid_renewal_date_future(self): + login = self.client.login(username='testuser2', password='12345') + + invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5) + resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':invalid_date_in_future} ) + self.assertEqual( resp.status_code,200) + self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead') + + def test_redirects_to_all_borrowed_book_list_on_success(self): + login = self.client.login(username='testuser2', password='12345') + valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2) + resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future} ) + self.assertRedirects(resp, reverse('all-borrowed') ) + + def test_HTTP404_for_invalid_book_if_logged_in(self): + import uuid + test_uid = uuid.uuid4() #unlikely UID to match our bookinstance! + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid,}) ) + self.assertEqual( resp.status_code,404) + + + + +class AuthorCreateViewTest(TestCase): + """ + Test case for the AuthorCreate view (Created as Challenge!) + """ + + def setUp(self): + #Create a user + test_user1 = User.objects.create_user(username='testuser1', password='12345') + test_user1.save() + + test_user2 = User.objects.create_user(username='testuser2', password='12345') + test_user2.save() + permission = Permission.objects.get(name='Set book as returned') + test_user2.user_permissions.add(permission) + test_user2.save() + + #Create a book + test_author = Author.objects.create(first_name='John', last_name='Smith') + + + def test_redirect_if_not_logged_in(self): + resp = self.client.get(reverse('author_create') ) + self.assertRedirects(resp, '/accounts/login/?next=/catalog/author/create/' ) + + def test_redirect_if_logged_in_but_not_correct_permission(self): + login = self.client.login(username='testuser1', password='12345') + resp = self.client.get(reverse('author_create') ) + self.assertRedirects(resp, '/accounts/login/?next=/catalog/author/create/' ) + + def test_logged_in_with_permission(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('author_create') ) + self.assertEqual( resp.status_code,200) + + def test_uses_correct_template(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('author_create') ) + self.assertEqual( resp.status_code,200) + self.assertTemplateUsed(resp, 'catalog/author_form.html') + + def test_form_date_of_death_initially_set_to_expected_date(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.get(reverse('author_create') ) + self.assertEqual( resp.status_code,200) + + expected_initial_date = datetime.date(2018, 1, 5) + response_date=resp.context['form'].initial['date_of_death'] + response_date=datetime.datetime.strptime(response_date, "%d/%m/%Y").date() + self.assertEqual(response_date, expected_initial_date ) + + def test_redirects_to_detail_view_on_success(self): + login = self.client.login(username='testuser2', password='12345') + resp = self.client.post(reverse('author_create'),{'first_name':'Christian Name','last_name':'Surname',} ) + #Manually check redirect because we don't know what author was created + self.assertEqual( resp.status_code,302) + self.assertTrue( resp.url.startswith('/catalog/author/') ) diff --git a/catalog/urls.py b/catalog/urls.py new file mode 100644 index 0000000..0668c19 --- /dev/null +++ b/catalog/urls.py @@ -0,0 +1,39 @@ +from django.urls import path + +from . import views + + +urlpatterns = [ + path('', views.index, name='index'), + path('books/', views.BookListView.as_view(), name='books'), + path('book/', views.BookDetailView.as_view(), name='book-detail'), + path('authors/', views.AuthorListView.as_view(), name='authors'), + path('author/', views.AuthorDetailView.as_view(), name='author-detail'), +] + + +urlpatterns += [ + path('mybooks/', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'), + path(r'borrowed/', views.LoanedBooksAllListView.as_view(), name='all-borrowed'), #Added for challenge +] + + +# Add URLConf for librarian to renew a book. +urlpatterns += [ + path('book//renew/', views.renew_book_librarian, name='renew-book-librarian'), +] + + +# Add URLConf to create, update, and delete authors +urlpatterns += [ + path('author/create/', views.AuthorCreate.as_view(), name='author_create'), + path('author//update/', views.AuthorUpdate.as_view(), name='author_update'), + path('author//delete/', views.AuthorDelete.as_view(), name='author_delete'), +] + +# Add URLConf to create, update, and delete books +urlpatterns += [ + path('book/create/', views.BookCreate.as_view(), name='book_create'), + path('book//update/', views.BookUpdate.as_view(), name='book_update'), + path('book//delete/', views.BookDelete.as_view(), name='book_delete'), +] diff --git a/catalog/views.py b/catalog/views.py new file mode 100644 index 0000000..13bc289 --- /dev/null +++ b/catalog/views.py @@ -0,0 +1,166 @@ +from django.shortcuts import render + +# Create your views here. + +from .models import Book, Author, BookInstance, Genre + +def index(request): + """ + View function for home page of site. + """ + # Generate counts of some of the main objects + num_books=Book.objects.all().count() + num_instances=BookInstance.objects.all().count() + # Available copies of books + num_instances_available=BookInstance.objects.filter(status__exact='a').count() + num_authors=Author.objects.count() # The 'all()' is implied by default. + + # Number of visits to this view, as counted in the session variable. + num_visits=request.session.get('num_visits', 0) + request.session['num_visits'] = num_visits+1 + + # Render the HTML template index.html with the data in the context variable. + return render( + request, + 'index.html', + context={'num_books':num_books,'num_instances':num_instances,'num_instances_available':num_instances_available,'num_authors':num_authors, + 'num_visits':num_visits}, + ) + +from django.views import generic + + +class BookListView(generic.ListView): + """ + Generic class-based view for a list of books. + """ + model = Book + paginate_by = 10 + +class BookDetailView(generic.DetailView): + """ + Generic class-based detail view for a book. + """ + model = Book + +class AuthorListView(generic.ListView): + """ + Generic class-based list view for a list of authors. + """ + model = Author + paginate_by = 10 + + +class AuthorDetailView(generic.DetailView): + """ + Generic class-based detail view for an author. + """ + model = Author + + +from django.contrib.auth.mixins import LoginRequiredMixin + +class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView): + """ + Generic class-based view listing books on loan to current user. + """ + model = BookInstance + template_name ='catalog/bookinstance_list_borrowed_user.html' + paginate_by = 10 + + def get_queryset(self): + return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back') + + +# Added as part of challenge! +from django.contrib.auth.mixins import PermissionRequiredMixin + +class LoanedBooksAllListView(PermissionRequiredMixin,generic.ListView): + """ + Generic class-based view listing all books on loan. Only visible to users with can_mark_returned permission. + """ + model = BookInstance + permission_required = 'catalog.can_mark_returned' + template_name ='catalog/bookinstance_list_borrowed_all.html' + paginate_by = 10 + + def get_queryset(self): + return BookInstance.objects.filter(status__exact='o').order_by('due_back') + + +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseRedirect +from django.urls import reverse +import datetime +from django.contrib.auth.decorators import permission_required + +from .forms import RenewBookForm + +@permission_required('catalog.can_mark_returned') +def renew_book_librarian(request, pk): + """ + View function for renewing a specific BookInstance by librarian + """ + book_inst=get_object_or_404(BookInstance, pk = pk) + + # If this is a POST request then process the Form data + if request.method == 'POST': + + # Create a form instance and populate it with data from the request (binding): + form = RenewBookForm(request.POST) + + # Check if the form is valid: + if form.is_valid(): + # process the data in form.cleaned_data as required (here we just write it to the model due_back field) + book_inst.due_back = form.cleaned_data['renewal_date'] + book_inst.save() + + # redirect to a new URL: + return HttpResponseRedirect(reverse('all-borrowed') ) + + # If this is a GET (or any other method) create the default form + else: + proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) + form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,}) + + return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst}) + + + +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.urls import reverse_lazy +from .models import Author + + +class AuthorCreate(PermissionRequiredMixin, CreateView): + model = Author + fields = '__all__' + initial={'date_of_death':'05/01/2018',} + permission_required = 'catalog.can_mark_returned' + +class AuthorUpdate(PermissionRequiredMixin, UpdateView): + model = Author + fields = ['first_name','last_name','date_of_birth','date_of_death'] + permission_required = 'catalog.can_mark_returned' + +class AuthorDelete(PermissionRequiredMixin, DeleteView): + model = Author + success_url = reverse_lazy('authors') + permission_required = 'catalog.can_mark_returned' + + +#Classes created for the forms challenge +class BookCreate(PermissionRequiredMixin, CreateView): + model = Book + fields = '__all__' + permission_required = 'catalog.can_mark_returned' + +class BookUpdate(PermissionRequiredMixin, UpdateView): + model = Book + fields = '__all__' + permission_required = 'catalog.can_mark_returned' + +class BookDelete(PermissionRequiredMixin, DeleteView): + model = Book + success_url = reverse_lazy('books') + permission_required = 'catalog.can_mark_returned' diff --git a/locallibrary/__init__.py b/locallibrary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/locallibrary/settings.py b/locallibrary/settings.py new file mode 100644 index 0000000..5656c0d --- /dev/null +++ b/locallibrary/settings.py @@ -0,0 +1,155 @@ +""" +Django settings for locallibrary project. + +Generated by 'django-admin startproject' using Django 1.10. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +#SECRET_KEY = 'cg#p$g+j9tax!#a3cup@1$8obt2_+&k3q+pmu)5%asj6yjpkag' +import os +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'cg#p$g+j9tax!#a3cup@1$8obt2_+&k3q+pmu)5%asj6yjpkag') + +# SECURITY WARNING: don't run with debug turned on in production! +#DEBUG = True +DEBUG = bool( os.environ.get('DJANGO_DEBUG', True) ) + +#Set hosts to allow any app on Heroku and the local testing URL +ALLOWED_HOSTS = ['.herokuapp.com','127.0.0.1'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + #Add our new application + 'catalog.apps.CatalogConfig', #This object was created for us in /catalog/apps.py +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'locallibrary.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['./templates',], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'locallibrary.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + + +# Redirect to home URL after login (Default redirects to /accounts/profile/) +LOGIN_REDIRECT_URL = '/' + +# Add to test email: +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + + + +# Heroku: Update database configuration from $DATABASE_URL. +import dj_database_url +db_from_env = dj_database_url.config(conn_max_age=500) +DATABASES['default'].update(db_from_env) + +# import Django and setup applications +import django +django.setup() + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ +# The absolute path to the directory where collectstatic will collect static files for deployment. +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +# The URL to use when referring to static files (where they will be served from) +STATIC_URL = '/static/' + + +# Simplified static file serving. +# https://warehouse.python.org/project/whitenoise/ +# STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' + +TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' +TEST_OUTPUT_VERBOSE = 2 +TEST_OUTPUT_DIR = 'test-results' \ No newline at end of file diff --git a/locallibrary/urls.py b/locallibrary/urls.py new file mode 100644 index 0000000..d46dc3f --- /dev/null +++ b/locallibrary/urls.py @@ -0,0 +1,60 @@ +"""locallibrary URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] + + +from django.urls import path +from django.contrib import admin + +# Use include() to add URLS from the catalog application and authentication system +from django.urls import include + + +urlpatterns = [ + path('admin/', admin.site.urls), +] + + +urlpatterns += [ + path('catalog/', include('catalog.urls')), +] + + +# Use static() to add url mapping to serve static files during development (only) +from django.conf import settings +from django.conf.urls.static import static + + +urlpatterns+= static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + +#Add URL maps to redirect the base URL to our application +from django.views.generic import RedirectView +urlpatterns += [ + path('', RedirectView.as_view(url='/catalog/', permanent=True)), +] + + + +#Add Django site authentication urls (for login, logout, password management) +urlpatterns += [ + path('accounts/', include('django.contrib.auth.urls')), +] diff --git a/locallibrary/wsgi.py b/locallibrary/wsgi.py new file mode 100644 index 0000000..5d39ba3 --- /dev/null +++ b/locallibrary/wsgi.py @@ -0,0 +1,24 @@ +""" +WSGI config for locallibrary project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "locallibrary.settings") + +application = get_wsgi_application() + + +#Add static serving using whitenoise +from django.core.wsgi import get_wsgi_application +from whitenoise.django import DjangoWhiteNoise + +application = get_wsgi_application() +application = DjangoWhiteNoise(application) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..d23ac38 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "locallibrary.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7fa90c4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.2.10 +attrs==20.1.0 +dj-database-url==0.5.0 +Django==3.1.1 +iniconfig==1.0.1 +more-itertools==8.5.0 +packaging==20.4 +pluggy==0.13.1 +py==1.9.0 +pybuilder==0.12.8 +pyparsing==2.4.7 +pytest==6.0.1 +pytz==2020.1 +six==1.15.0 +sqlparse==0.3.1 +toml==0.10.1 +unittest-xml-reporting==3.0.4 +xmlrunner==1.7.7 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..09dac98 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.6.4 \ No newline at end of file diff --git a/templates/registration/logged_out.html b/templates/registration/logged_out.html new file mode 100644 index 0000000..adfeb3e --- /dev/null +++ b/templates/registration/logged_out.html @@ -0,0 +1,7 @@ +{% extends "base_generic.html" %} + +{% block content %} +

Logged out!

+ +Click here to login again. +{% endblock %} \ No newline at end of file diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..d09cd20 --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,38 @@ +{% extends "base_generic.html" %} + +{% block content %} + +{% if form.errors %} +

Your username and password didn't match. Please try again.

+{% endif %} + +{% if next %} + {% if user.is_authenticated %} +

Your account doesn't have access to this page. To proceed, + please login with an account that has access.

+ {% else %} +

Please login to see this page.

+ {% endif %} +{% endif %} + +
+{% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+ +{# Assumes you setup the password_reset view in your URLconf #} +

Lost password?

+ +{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..d1eb03b --- /dev/null +++ b/templates/registration/password_reset_complete.html @@ -0,0 +1,8 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

The password has been changed!

+

log in again?

+ +{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..b72a59e --- /dev/null +++ b/templates/registration/password_reset_confirm.html @@ -0,0 +1,33 @@ +{% extends "base_generic.html" %} + +{% block content %} + + {% if validlink %} +

Please enter (and confirm) your new password.

+
+
+ +
+ + + + + + + + + + + + + +
{{ form.new_password1.errors }} + {{ form.new_password1 }}
{{ form.new_password2.errors }} + {{ form.new_password2 }}
+
+ {% else %} +

Password reset failed

+

The password reset link was invalid, possibly because it has already been used. Please request a new password reset.

+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html new file mode 100644 index 0000000..7c6ef1c --- /dev/null +++ b/templates/registration/password_reset_done.html @@ -0,0 +1,7 @@ +{% extends "base_generic.html" %} + +{% block content %} + +

We've emailed you instructions for setting your password. If they haven't arrived in a few minutes, check your spam folder.

+ +{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html new file mode 100644 index 0000000..37467b8 --- /dev/null +++ b/templates/registration/password_reset_email.html @@ -0,0 +1,2 @@ +Someone asked for password reset for email {{ email }}. Follow the link below: +{{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html new file mode 100644 index 0000000..bd54b3c --- /dev/null +++ b/templates/registration/password_reset_form.html @@ -0,0 +1,11 @@ +{% extends "base_generic.html" %} + +{% block content %} + +
{% csrf_token %} + {% if form.email.errors %}{{ form.email.errors }}{% endif %} +

{{ form.email }}

+ +
+ +{% endblock %} \ No newline at end of file diff --git a/test-results/TEST-catalog.tests.test_forms.RenewBookFormTest-20200901215420.xml b/test-results/TEST-catalog.tests.test_forms.RenewBookFormTest-20200901215420.xml new file mode 100644 index 0000000..16e6328 --- /dev/null +++ b/test-results/TEST-catalog.tests.test_forms.RenewBookFormTest-20200901215420.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test-results/TEST-catalog.tests.test_models.AuthorModelTest-20200901215420.xml b/test-results/TEST-catalog.tests.test_models.AuthorModelTest-20200901215420.xml new file mode 100644 index 0000000..10acce8 --- /dev/null +++ b/test-results/TEST-catalog.tests.test_models.AuthorModelTest-20200901215420.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test-results/TEST-catalog.tests.test_views.AuthorCreateViewTest-20200901215420.xml b/test-results/TEST-catalog.tests.test_views.AuthorCreateViewTest-20200901215420.xml new file mode 100644 index 0000000..a2306d7 --- /dev/null +++ b/test-results/TEST-catalog.tests.test_views.AuthorCreateViewTest-20200901215420.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/test-results/TEST-catalog.tests.test_views.AuthorListViewTest-20200901215420.xml b/test-results/TEST-catalog.tests.test_views.AuthorListViewTest-20200901215420.xml new file mode 100644 index 0000000..0aecc2e --- /dev/null +++ b/test-results/TEST-catalog.tests.test_views.AuthorListViewTest-20200901215420.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test-results/TEST-catalog.tests.test_views.LoanedBookInstancesByUserListViewTest-20200901215420.xml b/test-results/TEST-catalog.tests.test_views.LoanedBookInstancesByUserListViewTest-20200901215420.xml new file mode 100644 index 0000000..75b778d --- /dev/null +++ b/test-results/TEST-catalog.tests.test_views.LoanedBookInstancesByUserListViewTest-20200901215420.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test-results/TEST-catalog.tests.test_views.RenewBookInstancesViewTest-20200901215420.xml b/test-results/TEST-catalog.tests.test_views.RenewBookInstancesViewTest-20200901215420.xml new file mode 100644 index 0000000..624c219 --- /dev/null +++ b/test-results/TEST-catalog.tests.test_views.RenewBookInstancesViewTest-20200901215420.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + +