This commit is contained in:
+17
-49
@@ -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/
|
||||
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
pipeline {
|
||||
agent { docker { image 'python:3-alpine' } }
|
||||
stages {
|
||||
stage('test') {
|
||||
steps {
|
||||
sh 'python --version'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,625 +1,116 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 <https://www.gnu.org/
|
||||
licenses /why-not-lgpl.html>.
|
||||
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
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>
|
||||
@@ -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]
|
||||
|
||||
Generated
+124
@@ -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": {}
|
||||
}
|
||||
@@ -1,3 +1,39 @@
|
||||
# python-test
|
||||
# CircleCI Demo Application: Python / Django
|
||||
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
@@ -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')
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CatalogConfig(AppConfig):
|
||||
name = 'catalog'
|
||||
@@ -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
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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 <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>', 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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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 <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>', 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'),)},
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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 = [
|
||||
]
|
||||
@@ -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']},
|
||||
),
|
||||
]
|
||||
@@ -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 <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
|
||||
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)
|
||||
@@ -0,0 +1,6 @@
|
||||
.sidebar-nav {
|
||||
margin-top: 20px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
{% block title %}<title>Local Library</title>{% endblock %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Add additional CSS in static file -->
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
{% block sidebar %}
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="{% url 'index' %}">Home</a></li>
|
||||
<li><a href="{% url 'books' %}">All books</a></li>
|
||||
<li><a href="{% url 'authors' %}">All authors</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="sidebar-nav">
|
||||
{% if user.is_authenticated %}
|
||||
<li>User: {{ user.get_username }}</li>
|
||||
<li><a href="{% url 'my-borrowed' %}">My Borrowed</a></li>
|
||||
<li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{% if user.is_staff %}
|
||||
<hr />
|
||||
<ul class="sidebar-nav">
|
||||
<li>Staff</li>
|
||||
{% if perms.catalog.can_mark_returned %}
|
||||
<li><a href="{% url 'all-borrowed' %}">All borrowed</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="col-sm-10 ">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
{% block pagination %}
|
||||
{% if is_paginated %}
|
||||
<div class="pagination">
|
||||
<span class="page-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
<span class="page-current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Delete Author</h1>
|
||||
|
||||
<p>Are you sure you want to delete the author: {{ author }}?</p>
|
||||
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="submit" action="" value="Yes, delete." />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Author: {{ author }} </h1>
|
||||
<p>{{author.date_of_birth}} - {% if author.date_of_death %}{{author.date_of_death}}{% endif %}</p>
|
||||
|
||||
<div style="margin-left:20px;margin-top:20px">
|
||||
<h4>Books</h4>
|
||||
|
||||
<dl>
|
||||
{% for book in author.book_set.all %}
|
||||
<dt><a href="{% url 'book-detail' book.pk %}">{{book}}</a> ({{book.bookinstance_set.all.count}})</dt>
|
||||
<dd>{{book.summary}}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit" />
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Author List</h1>
|
||||
|
||||
{% if author_list %}
|
||||
<ul>
|
||||
|
||||
{% for author in author_list %}
|
||||
<li>
|
||||
<a href="{{ author.get_absolute_url }}">
|
||||
{{ author }} ({{author.date_of_birth}} - {% if author.date_of_death %}{{author.date_of_death}}{% endif %})
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>There are no authors available.</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Delete Book</h1>
|
||||
|
||||
<p>Are you sure you want to delete the book: {{ book }}?</p>
|
||||
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="submit" action="" value="Yes, delete." />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Title: {{ book.title }}</h1>
|
||||
|
||||
<p><strong>Author:</strong> <a href="{% url 'author-detail' book.author.pk %}">{{ book.author }}</a></p>
|
||||
<p><strong>Summary:</strong> {{ book.summary }}</p>
|
||||
<p><strong>ISBN:</strong> {{ book.isbn }}</p>
|
||||
<p><strong>Language:</strong> {{ book.language }}</p>
|
||||
<p><strong>Genre:</strong> {% for genre in book.genre.all %}{{genre}}{% if not forloop.last %}, {% endif %}{% endfor %}</p>
|
||||
|
||||
<div style="margin-left:20px;margin-top:20px">
|
||||
<h4>Copies</h4>
|
||||
|
||||
{% for copy in book.bookinstance_set.all %}
|
||||
<hr>
|
||||
<p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'd' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
|
||||
{% if copy.status != 'a' %}<p><strong>Due to be returned:</strong> {{copy.due_back}}</p>{% endif %}
|
||||
<p><strong>Imprint:</strong> {{copy.imprint}}</p>
|
||||
<p class="text-muted"><strong>Id:</strong> {{copy.id}}</p>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit" />
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Book List</h1>
|
||||
|
||||
{% if book_list %}
|
||||
<ul>
|
||||
|
||||
{% for book in book_list %}
|
||||
<li>
|
||||
<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
|
||||
{% else %}
|
||||
<p>There are no books in the library.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Renew: {{bookinst.book.title}}</h1>
|
||||
<p>Borrower: {{bookinst.borrower}}</p>
|
||||
<p{% if bookinst.is_overdue %} class="text-danger"{% endif %}>Due date: {{bookinst.due_back}}</p>
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<input type="submit" value="Submit" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>All Borrowed Books</h1>
|
||||
|
||||
{% if bookinstance_list %}
|
||||
<ul>
|
||||
|
||||
{% for bookinst in bookinstance_list %}
|
||||
<li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
|
||||
<a href="{% url 'book-detail' bookinst.book.pk %}">{{bookinst.book.title}}</a> ({{ bookinst.due_back }}) {% if user.is_staff %}- {{ bookinst.borrower }}{% endif %} {% if perms.catalog.can_mark_returned %}- <a href="{% url 'renew-book-librarian' bookinst.id %}">Renew</a> {% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% else %}
|
||||
<p>There are no books borrowed.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Borrowed books</h1>
|
||||
|
||||
{% if bookinstance_list %}
|
||||
<ul>
|
||||
|
||||
{% for bookinst in bookinstance_list %}
|
||||
<li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
|
||||
<a href="{% url 'book-detail' bookinst.book.pk %}">{{bookinst.book.title}}</a> ({{ bookinst.due_back }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% else %}
|
||||
<p>There are no books borrowed.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Local Library Home</h1>
|
||||
|
||||
<p>Welcome to <em>LocalLibrary</em>, a very basic Django website developed as a <a href="https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Tutorial_local_library_website">tutorial example</a> on the Mozilla Developer Network.</p>
|
||||
<p>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.</p>
|
||||
|
||||
|
||||
<h2>UML Models</h2>
|
||||
<p>An UML diagram of the site's Django model structure is shown below. </p>
|
||||
|
||||
<div>
|
||||
{% load static %}
|
||||
<img src="{% static "images/local_library_model_uml.png" %}" alt="My image" style="width:555px;height:540px;"/>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Dynamic content</h2>
|
||||
|
||||
<p>The library has the following record counts:</p>
|
||||
<ul>
|
||||
<li><strong>Books:</strong> {{ num_books }}</li>
|
||||
<li><strong>Copies:</strong> {{ num_instances }}</li>
|
||||
<li><strong>Copies available:</strong> {{ num_instances_available }}</li>
|
||||
<li><strong>Authors:</strong> {{ num_authors }}</li>
|
||||
</ul>
|
||||
|
||||
|
||||
You have visited this page {{ num_visits }} times.
|
||||
|
||||
{% endblock %}
|
||||
@@ -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).')
|
||||
@@ -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')
|
||||
|
||||
@@ -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/') )
|
||||
@@ -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/<int:pk>', views.BookDetailView.as_view(), name='book-detail'),
|
||||
path('authors/', views.AuthorListView.as_view(), name='authors'),
|
||||
path('author/<int:pk>', 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/<uuid:pk>/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/<int:pk>/update/', views.AuthorUpdate.as_view(), name='author_update'),
|
||||
path('author/<int:pk>/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/<int:pk>/update/', views.BookUpdate.as_view(), name='book_update'),
|
||||
path('book/<int:pk>/delete/', views.BookDelete.as_view(), name='book_delete'),
|
||||
]
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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')),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
python-3.6.4
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>Logged out!</p>
|
||||
|
||||
<a href="{% url 'login'%}">Click here to login again.</a>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if form.errors %}
|
||||
<p>Your username and password didn't match. Please try again.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if next %}
|
||||
{% if user.is_authenticated %}
|
||||
<p>Your account doesn't have access to this page. To proceed,
|
||||
please login with an account that has access.</p>
|
||||
{% else %}
|
||||
<p>Please login to see this page.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ form.username.label_tag }}</td>
|
||||
<td>{{ form.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ form.password.label_tag }}</td>
|
||||
<td>{{ form.password }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="login" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</form>
|
||||
|
||||
{# Assumes you setup the password_reset view in your URLconf #}
|
||||
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>The password has been changed!</h1>
|
||||
<p><a href="{% url 'login' %}">log in again?</a></p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if validlink %}
|
||||
<p>Please enter (and confirm) your new password.</p>
|
||||
<form action="" method="post">
|
||||
<div style="display:none">
|
||||
<input type="hidden" value="{{ csrf_token }}" name="csrfmiddlewaretoken">
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ form.new_password1.errors }}
|
||||
<label for="id_new_password1">New password:</label></td>
|
||||
<td>{{ form.new_password1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ form.new_password2.errors }}
|
||||
<label for="id_new_password2">Confirm password:</label></td>
|
||||
<td>{{ form.new_password2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><input type="submit" value="Change my password" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
{% else %}
|
||||
<h1>Password reset failed</h1>
|
||||
<p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>We've emailed you instructions for setting your password. If they haven't arrived in a few minutes, check your spam folder.</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base_generic.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% if form.email.errors %}{{ form.email.errors }}{% endif %}
|
||||
<p>{{ form.email }}</p>
|
||||
<input type="submit" class='btn btn-default btn-lg' value="Reset password" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="catalog.tests.test_forms.RenewBookFormTest-20200901215420" tests="6" file="catalog/tests/test_forms.py" time="0.010" timestamp="2020-09-01T21:54:20" failures="0" errors="0" skipped="0">
|
||||
<testcase classname="catalog.tests.test_forms.RenewBookFormTest" name="test_renew_form_date_field_help_text" time="0.006" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_forms.py" line="56">
|
||||
<!--
|
||||
Test renewal_date help_text is as expected.
|
||||
-->
|
||||
</testcase>
|
||||
<testcase classname="catalog.tests.test_forms.RenewBookFormTest" name="test_renew_form_date_field_label" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_forms.py" line="48">
|
||||
<!--
|
||||
Test renewal_date label is "renewal date"
|
||||
-->
|
||||
</testcase>
|
||||
<testcase classname="catalog.tests.test_forms.RenewBookFormTest" name="test_renew_form_date_in_past" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_forms.py" line="11">
|
||||
<!--
|
||||
Test form is invalid if renewal_date is before today
|
||||
-->
|
||||
</testcase>
|
||||
<testcase classname="catalog.tests.test_forms.RenewBookFormTest" name="test_renew_form_date_max" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_forms.py" line="38">
|
||||
<!--
|
||||
Test form is valid if renewal_date is within 4 weeks
|
||||
-->
|
||||
</testcase>
|
||||
<testcase classname="catalog.tests.test_forms.RenewBookFormTest" name="test_renew_form_date_today" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_forms.py" line="29">
|
||||
<!--
|
||||
Test form is valid if renewal_date is today
|
||||
-->
|
||||
</testcase>
|
||||
<testcase classname="catalog.tests.test_forms.RenewBookFormTest" name="test_renew_form_date_too_far_in_future" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_forms.py" line="20">
|
||||
<!--
|
||||
Test form is invalid if renewal_date more than 4 weeks from today
|
||||
-->
|
||||
</testcase>
|
||||
</testsuite>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="catalog.tests.test_models.AuthorModelTest-20200901215420" tests="8" file="catalog/tests/test_models.py" time="0.017" timestamp="2020-09-01T21:54:20" failures="0" errors="0" skipped="0">
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_date_of_birth_label" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="24"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_date_of_death_label" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="29"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_first_name_label" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="14"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_first_name_max_length" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="34"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_get_absolute_url" time="0.009" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="51"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_last_name_label" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="19"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_last_name_max_length" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="39"/>
|
||||
<testcase classname="catalog.tests.test_models.AuthorModelTest" name="test_object_name_is_last_name_comma_first_name" time="0.001" timestamp="2020-09-01T21:54:20" file="catalog/tests/test_models.py" line="44"/>
|
||||
</testsuite>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="catalog.tests.test_views.AuthorCreateViewTest-20200901215420" tests="6" file="catalog/tests/test_views.py" time="1.984" timestamp="2020-09-01T21:54:22" failures="1" errors="0" skipped="0">
|
||||
<testcase classname="catalog.tests.test_views.AuthorCreateViewTest" name="test_form_date_of_death_initially_set_to_expected_date" time="0.427" timestamp="2020-09-01T21:54:21" file="catalog/tests/test_views.py" line="329"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorCreateViewTest" name="test_logged_in_with_permission" time="0.338" timestamp="2020-09-01T21:54:21" file="catalog/tests/test_views.py" line="318"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorCreateViewTest" name="test_redirect_if_not_logged_in" time="0.230" timestamp="2020-09-01T21:54:21" file="catalog/tests/test_views.py" line="309"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorCreateViewTest" name="test_redirects_to_detail_view_on_success" time="0.331" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="339"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorCreateViewTest" name="test_uses_correct_template" time="0.328" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="323"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorCreateViewTest" name="test_redirect_if_logged_in_but_not_correct_permission" time="0.329" timestamp="2020-09-01T21:54:21" file="catalog/tests/test_views.py" line="313">
|
||||
<failure type="AssertionError" message="403 != 302 : Response didn't redirect as expected: Response code was 403 (expected 302)"><![CDATA[Traceback (most recent call last):
|
||||
File "/Users/johannag/src/circleci-demo-python-django/catalog/tests/test_views.py", line 316, in test_redirect_if_logged_in_but_not_correct_permission
|
||||
self.assertRedirects(resp, '/accounts/login/?next=/catalog/author/create/' )
|
||||
File "/Users/johannag/src/circleci-demo-python-django/env/lib/python3.8/site-packages/django/test/testcases.py", line 356, in assertRedirects
|
||||
self.assertEqual(
|
||||
AssertionError: 403 != 302 : Response didn't redirect as expected: Response code was 403 (expected 302)
|
||||
]]></failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="catalog.tests.test_views.AuthorListViewTest-20200901215420" tests="5" file="catalog/tests/test_views.py" time="0.022" timestamp="2020-09-01T21:54:22" failures="0" errors="0" skipped="0">
|
||||
<testcase classname="catalog.tests.test_views.AuthorListViewTest" name="test_lists_all_authors" time="0.006" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="39"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorListViewTest" name="test_pagination_is_ten" time="0.004" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="32"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorListViewTest" name="test_view_url_accessible_by_name" time="0.003" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="23"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorListViewTest" name="test_view_url_exists_at_desired_location" time="0.003" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="19"/>
|
||||
<testcase classname="catalog.tests.test_views.AuthorListViewTest" name="test_view_uses_correct_template" time="0.006" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="27"/>
|
||||
</testsuite>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="catalog.tests.test_views.LoanedBookInstancesByUserListViewTest-20200901215420" tests="5" file="catalog/tests/test_views.py" time="1.626" timestamp="2020-09-01T21:54:24" failures="0" errors="0" skipped="0">
|
||||
<testcase classname="catalog.tests.test_views.LoanedBookInstancesByUserListViewTest" name="test_logged_in_uses_correct_template" time="0.343" timestamp="2020-09-01T21:54:22" file="catalog/tests/test_views.py" line="88"/>
|
||||
<testcase classname="catalog.tests.test_views.LoanedBookInstancesByUserListViewTest" name="test_only_borrowed_books_in_list" time="0.349" timestamp="2020-09-01T21:54:23" file="catalog/tests/test_views.py" line="100"/>
|
||||
<testcase classname="catalog.tests.test_views.LoanedBookInstancesByUserListViewTest" name="test_pages_ordered_by_due_date" time="0.355" timestamp="2020-09-01T21:54:23" file="catalog/tests/test_views.py" line="153"/>
|
||||
<testcase classname="catalog.tests.test_views.LoanedBookInstancesByUserListViewTest" name="test_pages_paginated_to_ten" time="0.351" timestamp="2020-09-01T21:54:24" file="catalog/tests/test_views.py" line="134"/>
|
||||
<testcase classname="catalog.tests.test_views.LoanedBookInstancesByUserListViewTest" name="test_redirect_if_not_logged_in" time="0.228" timestamp="2020-09-01T21:54:24" file="catalog/tests/test_views.py" line="84"/>
|
||||
</testsuite>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="catalog.tests.test_views.RenewBookInstancesViewTest-20200901215420" tests="10" file="catalog/tests/test_views.py" time="3.244" timestamp="2020-09-01T21:54:27" failures="0" errors="0" skipped="0">
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_HTTP404_for_invalid_book_if_logged_in" time="0.338" timestamp="2020-09-01T21:54:24" file="catalog/tests/test_views.py" line="279"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_form_invalid_renewal_date_future" time="0.341" timestamp="2020-09-01T21:54:24" file="catalog/tests/test_views.py" line="265"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_form_invalid_renewal_date_past" time="0.336" timestamp="2020-09-01T21:54:25" file="catalog/tests/test_views.py" line="257"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_form_renewal_date_initially_has_date_three_weeks_in_future" time="0.330" timestamp="2020-09-01T21:54:25" file="catalog/tests/test_views.py" line="249"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_logged_in_with_permission_another_users_borrowed_book" time="0.331" timestamp="2020-09-01T21:54:25" file="catalog/tests/test_views.py" line="234"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_logged_in_with_permission_borrowed_book" time="0.330" timestamp="2020-09-01T21:54:26" file="catalog/tests/test_views.py" line="227"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_redirect_if_logged_in_but_not_correct_permission" time="0.332" timestamp="2020-09-01T21:54:26" file="catalog/tests/test_views.py" line="219"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_redirect_if_not_logged_in" time="0.223" timestamp="2020-09-01T21:54:26" file="catalog/tests/test_views.py" line="213"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_redirects_to_all_borrowed_book_list_on_success" time="0.345" timestamp="2020-09-01T21:54:27" file="catalog/tests/test_views.py" line="273"/>
|
||||
<testcase classname="catalog.tests.test_views.RenewBookInstancesViewTest" name="test_uses_correct_template" time="0.336" timestamp="2020-09-01T21:54:27" file="catalog/tests/test_views.py" line="241"/>
|
||||
</testsuite>
|
||||
Reference in New Issue
Block a user