├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE └── workflows │ ├── auto-merge.yml │ ├── codeql.yml │ ├── idle.yml │ └── welcome-bot.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── REVIEWING.md ├── catalog ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160921_1401.py │ ├── 0003_auto_20160921_1420.py │ ├── 0004_auto_20160921_1422.py │ ├── 0005_auto_20160921_1433.py │ ├── 0006_auto_20160921_1439.py │ ├── 0007_auto_20160921_1444.py │ ├── 0008_auto_20160921_1511.py │ ├── 0009_remove_bookinstance_summary.py │ ├── 0010_auto_20160921_1527.py │ ├── 0011_auto_20160922_1029.py │ ├── 0012_bookinstance_date_acquired.py │ ├── 0013_auto_20160926_1901.py │ ├── 0014_remove_bookinstance_date_acquired.py │ ├── 0015_auto_20160927_1808.py │ ├── 0016_auto_20160927_1947.py │ ├── 0017_language.py │ ├── 0018_book_language.py │ ├── 0019_bookinstance_borrower.py │ ├── 0020_auto_20161012_1044.py │ ├── 0021_auto_20171229_1056.py │ ├── 0022_auto_20181028_1731.py │ ├── 0023_auto_20201201_0238.py │ ├── 0024_auto_20210302_0630.py │ ├── 0025_auto_20220222_0623.py │ ├── 0026_alter_book_author_alter_genre_name_and_more.py │ ├── 0027_genre_genre_name_case_insensitive_unique_and_more.py │ └── __init__.py ├── models.py ├── static │ ├── css │ │ └── styles.css │ └── images │ │ └── local_library_model_uml.png ├── templates │ ├── base_generic.html │ ├── catalog │ │ ├── author_confirm_delete.html │ │ ├── author_detail.html │ │ ├── author_form.html │ │ ├── author_list.html │ │ ├── book_confirm_delete.html │ │ ├── book_detail.html │ │ ├── book_form.html │ │ ├── book_list.html │ │ ├── book_renew_librarian.html │ │ ├── bookinstance_confirm_delete.html │ │ ├── bookinstance_detail.html │ │ ├── bookinstance_form.html │ │ ├── bookinstance_list.html │ │ ├── bookinstance_list_borrowed_all.html │ │ ├── bookinstance_list_borrowed_user.html │ │ ├── genre_confirm_delete.html │ │ ├── genre_detail.html │ │ ├── genre_form.html │ │ ├── genre_list.html │ │ ├── language_confirm_delete.html │ │ ├── language_detail.html │ │ ├── language_form.html │ │ └── language_list.html │ └── index.html ├── tests │ ├── __init__.py │ ├── test_forms.py │ ├── test_models.py │ └── test_views.py ├── urls.py └── views.py ├── locallibrary ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── requirements.txt ├── runtime.txt └── templates └── registration ├── logged_out.html ├── login.html ├── password_reset_complete.html ├── password_reset_confirm.html ├── password_reset_done.html ├── password_reset_email.html └── password_reset_form.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: "Issue report" 2 | description: Report an unexpected problem or unintended behavior. 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Want to fix the problem yourself?** This project is open source and we welcome fixes and improvements from the community! 9 | 10 | ↩ Check the project [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md) guide to see how to get started. 11 | - type: checkboxes 12 | id: checklist 13 | attributes: 14 | label: "Before submitting the issue, confirm that you've checked everything in the following list, and mark them as completed here:" 15 | description: Make sure you have provided all the information below so we can help resolve your issue. 16 | options: 17 | - label: Issue is reproducible on this repository by following the [quick-start instructions](https://github.com/mdn/django-locallibrary-tutorial#quick-start). 18 | required: true 19 | - label: A clean/fresh database was used when the issue was encountered. 20 | required: true 21 | - label: Information about the environment setup when the issue was encountered is included in the issue. 22 | required: true 23 | - label: Output logs are included in the issue. 24 | required: true 25 | - type: textarea 26 | id: problem 27 | attributes: 28 | label: What was incorrect, unhelpful, or unexpected? 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: expected 33 | attributes: 34 | label: What did you expect to see? 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: logs 39 | attributes: 40 | label: Output logs 41 | description: | 42 | Share any logs that describe the problem you are seeing. If you are seeing an error, please include the full error message. 43 | placeholder: | 44 | You can paste the output directly or upload it as an attachment. 45 | - type: textarea 46 | id: more-info 47 | attributes: 48 | label: Do you have anything more you want to share? 49 | description: For example, steps to reproduce, screenshots, screen recordings, or sample code. 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Content or feature request 4 | url: https://github.com/mdn/mdn/issues/new/choose 5 | about: Propose new content for MDN Web Docs or submit a feature request using this link. 6 | - name: MDN GitHub Discussions 7 | url: https://github.com/orgs/mdn/discussions 8 | about: Does the issue involve a lot of changes, or is it hard to split it into actionable tasks? Start a discussion before opening an issue. 9 | - name: MDN Web Docs on Discourse 10 | url: https://discourse.mozilla.org/c/mdn/learn/250 11 | about: Need help with assessments on MDN Web Docs? We have a support community for this purpose on Discourse. 12 | - name: Help with code 13 | url: https://stackoverflow.com/ 14 | about: If you are stuck and need help with code, StackOverflow is a great resource. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### Motivation 8 | 9 | 10 | 11 | ### Additional details 12 | 13 | 14 | 15 | ### Related issues and pull requests 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: auto-merge 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | auto-merge: 8 | uses: mdn/workflows/.github/workflows/auto-merge.yml@main 9 | with: 10 | target-repo: "mdn/django-locallibrary-tutorial" 11 | secrets: 12 | GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | paths-ignore: 7 | - "**.md" 8 | pull_request: 9 | # The branches below must be a subset of the branches above 10 | branches: ["main"] 11 | paths-ignore: 12 | - "**.md" 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | strategy: 24 | matrix: 25 | # Add the language(s) you want to analyze here as an array of strings 26 | # for example: ['javascript'] or ['python', 'javascript'] 27 | language: ["python"] 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v3 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v2 36 | with: 37 | languages: ${{ matrix.language }} 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v2 41 | with: 42 | category: "/language:${{matrix.language}}" 43 | -------------------------------------------------------------------------------- /.github/workflows/idle.yml: -------------------------------------------------------------------------------- 1 | # This workflow is hosted at: https://github.com/mdn/workflows/blob/main/.github/workflows/idle.yml 2 | # Docs for this workflow: https://github.com/mdn/workflows/blob/main/README.md#idle 3 | name: "Label idle issues" 4 | 5 | on: 6 | schedule: 7 | - cron: "0 8 * * *" 8 | 9 | jobs: 10 | mark-as-idle: 11 | uses: mdn/workflows/.github/workflows/idle.yml@main 12 | with: 13 | target-repo: "mdn/django-locallibrary-tutorial" 14 | -------------------------------------------------------------------------------- /.github/workflows/welcome-bot.yml: -------------------------------------------------------------------------------- 1 | # This workflow is hosted at: https://github.com/mdn/workflows/blob/main/.github/workflows/allo-allo.yml 2 | # Docs for this workflow: https://github.com/mdn/workflows/blob/main/README.md#allo-allo 3 | name: "AlloAllo" 4 | 5 | on: 6 | issues: 7 | types: 8 | - opened 9 | pull_request_target: 10 | branches: 11 | - main 12 | types: 13 | - opened 14 | - closed 15 | 16 | jobs: 17 | allo-allo: 18 | uses: mdn/workflows/.github/workflows/allo-allo.yml@main 19 | with: 20 | target-repo: "mdn/django-locallibrary-tutorial" 21 | issue-welcome: > 22 | It looks like this is your first issue. Welcome! 👋 23 | One of the project maintainers will be with you as soon as possible. We 24 | appreciate your patience. To safeguard the health of the project, please 25 | take a moment to read our [code of conduct](../blob/main/CODE_OF_CONDUCT.md). 26 | pr-welcome: > 27 | It looks like this is your first pull request. 🎉 28 | Thank you for your contribution! One of the project maintainers will triage 29 | and assign the pull request for review. We appreciate your patience. To 30 | safeguard the health of the project, please take a moment to read our 31 | [code of conduct](../blob/main/CODE_OF_CONDUCT.md). 32 | pr-merged: > 33 | Congratulations on your first merged pull request. 🎉 Thank you for your contribution! 34 | Did you know we have a [project board](https://github.com/orgs/mdn/projects/25) with high-impact contribution opportunities? 35 | We look forward to your next contribution. 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Text backup files 2 | *.bak 3 | 4 | #Database 5 | *.sqlite3 6 | 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | staticfiles/ 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # IPython Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, read [Mozilla's Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 5 | 6 | ## Reporting violations 7 | 8 | For more information on how to report violations of the Community Participation Guidelines, read the [How to report](https://www.mozilla.org/about/governance/policies/participation/reporting/) page. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | ![github-profile](https://user-images.githubusercontent.com/10350960/166113119-629295f6-c282-42c9-9379-af2de5ad4338.png) 4 | 5 | - [Ways to contribute](#ways-to-contribute) 6 | - [Finding an issue](#finding-an-issue) 7 | - [Asking for help](#asking-for-help) 8 | - [Pull request process](#pull-request-process) 9 | - [Forking and cloning the project](#forking-and-cloning-the-project) 10 | - [Signing commits](#signing-commits) 11 | 12 | Welcome 👋 Thank you for your interest in contributing to MDN Web Docs. We are happy to have you join us! 💖 13 | 14 | As you get started, you are in the best position to give us feedback on project areas we might have forgotten about or assumed to work well. 15 | These include, but are not limited to: 16 | 17 | - Problems found while setting up a new developer environment 18 | - Gaps in our documentation 19 | - Bugs in our automation scripts 20 | 21 | If anything doesn't make sense or work as expected, please open an issue and let us know! 22 | 23 | ## Ways to contribute 24 | 25 | We welcome many different types of contributions including: 26 | 27 | - New features and content suggestions. 28 | - Identifying and filing issues. 29 | - Providing feedback on existing issues. 30 | - Engaging with the community and answering questions. 31 | - Contributing documentation or code. 32 | - Promoting the project in personal circles and social media. 33 | 34 | ## Finding an issue 35 | 36 | We have issues labeled `good first issue` for new contributors and `help wanted` suitable for any contributor. 37 | Good first issues have extra information to help you make your first contribution a success. 38 | Help wanted issues are ideal when you feel a bit more comfortable with the project details. 39 | 40 | Sometimes there won't be any issues with these labels, but there is likely still something for you to work on. 41 | If you want to contribute but don't know where to start or can't find a suitable issue, speak to us on [Matrix](https://matrix.to/#/#mdn:mozilla.org), and we will be happy to help. 42 | 43 | Once you find an issue you'd like to work on, please post a comment saying you want to work on it. 44 | Something like "I want to work on this" is fine. 45 | Also, mention the community team using the `@mdn/mdn-community-engagement` handle to ensure someone will get back to you. 46 | 47 | ## Asking for help 48 | 49 | The best way to reach us with a question when contributing is to use the following channels in the following order of precedence: 50 | 51 | - [Start a discussion](https://github.com/orgs/mdn/discussions) 52 | - Ask your question or highlight your discussion on [Matrix](https://matrix.to/#/#mdn:mozilla.org). 53 | - File an issue and tag the community team using the `@mdn/mdn-community-engagement` handle. 54 | 55 | ## Pull request process 56 | 57 | The MDN Web Docs project has a well-defined pull request process which is documented in the [Pull request guidelines](https://developer.mozilla.org/en-US/docs/MDN/Community/Pull_requests). 58 | Make sure you read and understand this process before you start working on a pull request. 59 | 60 | ### Forking and cloning the project 61 | 62 | The first step in setting up your development environment is to [fork the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and [clone](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository) the repository to your local machine. 63 | 64 | ## Signing commits 65 | 66 | We require all commits to be signed to verify the author's identity. 67 | GitHub has a detailed guide on [setting up signed commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). 68 | If you get stuck, please [ask for help](#asking-for-help). 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python manage.py migrate && python manage.py collectstatic --no-input && gunicorn locallibrary.wsgi 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Local Library 2 | 3 | Tutorial "Local Library" website written in Django. 4 | 5 | For detailed information about this project see the associated [MDN tutorial home page](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Tutorial_local_library_website). 6 | 7 | ## Overview 8 | 9 | This web application creates an online catalog for a small local library, where users can browse available books and manage their accounts. 10 | 11 | The main features that have currently been implemented are: 12 | 13 | * There are models for books, book copies, genre, language and authors. 14 | * Users can view list and detail information for books and authors. 15 | * Admin users can create and manage models. The admin has been optimised (the basic registration is present in admin.py, but commented out). 16 | * Librarians can renew reserved books. 17 | 18 | ![Local Library Model](https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/master/catalog/static/images/local_library_model_uml.png) 19 | 20 | 21 | ## Quick Start 22 | 23 | To get this project up and running locally on your computer: 24 | 1. Set up the [Python development environment](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/development_environment). 25 | We recommend using a Python virtual environment. 26 | > **Note:** This has been tested against Django 3.10 (and may not work or be "optimal" for other versions). 27 | 1. Assuming you have Python setup, run the following commands (if you're on Windows you may use `py` or `py -3` instead of `python` to start Python): 28 | ``` 29 | pip3 install -r requirements.txt 30 | python3 manage.py makemigrations 31 | python3 manage.py migrate 32 | python3 manage.py collectstatic 33 | python3 manage.py test # Run the standard tests. These should all pass. 34 | python3 manage.py createsuperuser # Create a superuser 35 | python3 manage.py runserver 36 | ``` 37 | 1. Open a browser to `http://127.0.0.1:8000/admin/` to open the admin site 38 | 1. Create a few test objects of each type. 39 | 1. Open tab to `http://127.0.0.1:8000` to see the main site, with your new objects. 40 | -------------------------------------------------------------------------------- /REVIEWING.md: -------------------------------------------------------------------------------- 1 | # Reviewing guide 2 | 3 | All reviewers must abide by the [code of conduct](CODE_OF_CONDUCT.md); they are also protected by the code of conduct. 4 | A reviewer should not tolerate poor behavior and is encouraged to [report any behavior](CODE_OF_CONDUCT.md#Reporting_violations) that violates the code of conduct. 5 | 6 | ## Review process 7 | 8 | The MDN Web Docs team has a well-defined review process that must be followed by reviewers in all repositories under the GitHub MDN organization. 9 | This process is described in detail on the [Pull request guidelines](https://developer.mozilla.org/en-US/docs/MDN/Community/Pull_requests) page. 10 | -------------------------------------------------------------------------------- /catalog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/a4ac27b88fc15c0b4526dddca8e980355b327479/catalog/__init__.py -------------------------------------------------------------------------------- /catalog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | 5 | from .models import Author, Genre, Book, BookInstance, Language 6 | 7 | """Minimal registration of Models. 8 | admin.site.register(Book) 9 | admin.site.register(Author) 10 | admin.site.register(BookInstance) 11 | admin.site.register(Genre) 12 | admin.site.register(Language) 13 | """ 14 | 15 | admin.site.register(Genre) 16 | admin.site.register(Language) 17 | 18 | 19 | class BooksInline(admin.TabularInline): 20 | """Defines format of inline book insertion (used in AuthorAdmin)""" 21 | model = Book 22 | 23 | 24 | @admin.register(Author) 25 | class AuthorAdmin(admin.ModelAdmin): 26 | """Administration object for Author models. 27 | Defines: 28 | - fields to be displayed in list view (list_display) 29 | - orders fields in detail view (fields), 30 | grouping the date fields horizontally 31 | - adds inline addition of books in author view (inlines) 32 | """ 33 | list_display = ('last_name', 34 | 'first_name', 'date_of_birth', 'date_of_death') 35 | fields = ['first_name', 'last_name', ('date_of_birth', 'date_of_death')] 36 | inlines = [BooksInline] 37 | 38 | 39 | class BooksInstanceInline(admin.TabularInline): 40 | """Defines format of inline book instance insertion (used in BookAdmin)""" 41 | model = BookInstance 42 | 43 | 44 | class BookAdmin(admin.ModelAdmin): 45 | """Administration object for Book models. 46 | Defines: 47 | - fields to be displayed in list view (list_display) 48 | - adds inline addition of book instances in book view (inlines) 49 | """ 50 | list_display = ('title', 'author', 'display_genre') 51 | inlines = [BooksInstanceInline] 52 | 53 | 54 | admin.site.register(Book, BookAdmin) 55 | 56 | 57 | @admin.register(BookInstance) 58 | class BookInstanceAdmin(admin.ModelAdmin): 59 | """Administration object for BookInstance models. 60 | Defines: 61 | - fields to be displayed in list view (list_display) 62 | - filters that will be displayed in sidebar (list_filter) 63 | - grouping of fields into sections (fieldsets) 64 | """ 65 | list_display = ('book', 'status', 'borrower', 'due_back', 'id') 66 | list_filter = ('status', 'due_back') 67 | 68 | fieldsets = ( 69 | (None, { 70 | 'fields': ('book', 'imprint', 'id') 71 | }), 72 | ('Availability', { 73 | 'fields': ('status', 'due_back', 'borrower') 74 | }), 75 | ) 76 | -------------------------------------------------------------------------------- /catalog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CatalogConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'catalog' 7 | -------------------------------------------------------------------------------- /catalog/forms.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.utils.translation import gettext_lazy as _ 3 | import datetime # for checking renewal date range. 4 | 5 | from django import forms 6 | 7 | 8 | class RenewBookForm(forms.Form): 9 | """Form for a librarian to renew books.""" 10 | renewal_date = forms.DateField( 11 | help_text="Enter a date between now and 4 weeks (default 3).") 12 | 13 | def clean_renewal_date(self): 14 | data = self.cleaned_data['renewal_date'] 15 | 16 | # Check date is not in past. 17 | if data < datetime.date.today(): 18 | raise ValidationError(_('Invalid date - renewal in past')) 19 | # Check date is in range librarian allowed to change (+4 weeks) 20 | if data > datetime.date.today() + datetime.timedelta(weeks=4): 21 | raise ValidationError( 22 | _('Invalid date - renewal more than 4 weeks ahead')) 23 | 24 | # Remember to always return the cleaned data. 25 | return data 26 | -------------------------------------------------------------------------------- /catalog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 03:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Author', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=200)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Book', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('title', models.CharField(max_length=200)), 29 | ('summary', models.CharField(max_length=200)), 30 | ('imprint', models.CharField(max_length=200)), 31 | ('isbn', models.CharField(max_length=13)), 32 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalog.Author')), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='Subject', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('subject_name', models.CharField(max_length=200)), 40 | ], 41 | ), 42 | migrations.AddField( 43 | model_name='book', 44 | name='subject', 45 | field=models.ManyToManyField(to='catalog.Subject'), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /catalog/migrations/0002_auto_20160921_1401.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 04:01 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('catalog', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='book', 18 | name='author', 19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Author'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /catalog/migrations/0003_auto_20160921_1420.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 04:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0002_auto_20160921_1401'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='book', 17 | name='summary', 18 | field=models.TextField(help_text='Enter a brief description of the book', max_length=200), 19 | ), 20 | migrations.AlterField( 21 | model_name='subject', 22 | name='subject_name', 23 | field=models.CharField(help_text='Enter a book category - e.g. Science Fiction, Non Fiction etc.', max_length=200), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /catalog/migrations/0004_auto_20160921_1422.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 04:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0003_auto_20160921_1420'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='book', 17 | name='summary', 18 | field=models.TextField(help_text='Enter a brief description of the book', max_length=1000), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /catalog/migrations/0005_auto_20160921_1433.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 04:33 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0004_auto_20160921_1422'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='author', 17 | name='name', 18 | ), 19 | migrations.AddField( 20 | model_name='author', 21 | name='first_name', 22 | field=models.CharField(default='Ben', max_length=100), 23 | preserve_default=False, 24 | ), 25 | migrations.AddField( 26 | model_name='author', 27 | name='last_name', 28 | field=models.CharField(default='Bova', max_length=100), 29 | preserve_default=False, 30 | ), 31 | migrations.AlterField( 32 | model_name='book', 33 | name='isbn', 34 | field=models.CharField(help_text='13 Character ISBN number', max_length=13), 35 | ), 36 | migrations.AlterField( 37 | model_name='book', 38 | name='subject', 39 | field=models.ManyToManyField(help_text='Select a grouping category for this book', to='catalog.Subject', verbose_name='Category'), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /catalog/migrations/0006_auto_20160921_1439.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 04:39 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0005_auto_20160921_1433'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='author', 17 | name='date_of_birth', 18 | field=models.DateField(null=True, verbose_name='D.O.B'), 19 | ), 20 | migrations.AddField( 21 | model_name='author', 22 | name='date_of_death', 23 | field=models.DateField(null=True, verbose_name='Died'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /catalog/migrations/0007_auto_20160921_1444.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 04:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0006_auto_20160921_1439'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='author', 17 | name='date_of_birth', 18 | field=models.DateField(blank=True, null=True, verbose_name='D.O.B'), 19 | ), 20 | migrations.AlterField( 21 | model_name='author', 22 | name='date_of_death', 23 | field=models.DateField(blank=True, null=True, verbose_name='Died'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /catalog/migrations/0008_auto_20160921_1511.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 05:11 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('catalog', '0007_auto_20160921_1444'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='BookInstance', 19 | fields=[ 20 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique ID for this particular book across whole library', primary_key=True, serialize=False)), 21 | ('summary', models.TextField(help_text='Enter a brief description of the book', max_length=1000)), 22 | ('imprint', models.CharField(max_length=200)), 23 | ('due_back', models.DateField(blank=True, null=True)), 24 | ('status', models.CharField(blank=True, choices=[('d', 'maintenance'), ('o', 'on loan'), ('a', 'available'), ('r', 'reserved')], default='d', help_text='Book availability', max_length=1)), 25 | ], 26 | ), 27 | migrations.RemoveField( 28 | model_name='book', 29 | name='imprint', 30 | ), 31 | migrations.AlterField( 32 | model_name='author', 33 | name='date_of_birth', 34 | field=models.DateField(blank=True, null=True), 35 | ), 36 | migrations.AddField( 37 | model_name='bookinstance', 38 | name='book', 39 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Book'), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /catalog/migrations/0009_remove_bookinstance_summary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 05:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0008_auto_20160921_1511'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='bookinstance', 17 | name='summary', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /catalog/migrations/0010_auto_20160921_1527.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-21 05:27 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('catalog', '0009_remove_bookinstance_summary'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='bookinstance', 18 | name='book', 19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='Fishcakes instance+', to='catalog.Book'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /catalog/migrations/0011_auto_20160922_1029.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-22 00:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('catalog', '0010_auto_20160921_1527'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterModelOptions( 18 | name='bookinstance', 19 | options={'ordering': ['due_back']}, 20 | ), 21 | migrations.AlterField( 22 | model_name='bookinstance', 23 | name='book', 24 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Book'), 25 | ), 26 | migrations.AlterField( 27 | model_name='bookinstance', 28 | name='id', 29 | field=models.UUIDField(default=uuid.uuid4, help_text='Unique ID for this particular book across whole library', primary_key=True, serialize=False), 30 | ), 31 | migrations.AlterField( 32 | model_name='bookinstance', 33 | name='status', 34 | field=models.CharField(blank=True, choices=[('d', 'Maintenance'), ('o', 'On loan'), ('a', 'Available'), ('r', 'Reserved')], default='d', help_text='Book availability', max_length=1), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /catalog/migrations/0012_bookinstance_date_acquired.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-26 08:27 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('catalog', '0011_auto_20160922_1029'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='bookinstance', 18 | name='date_acquired', 19 | field=models.DateField(auto_now_add=True, default=django.utils.timezone.now), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /catalog/migrations/0013_auto_20160926_1901.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-26 09:01 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('catalog', '0012_bookinstance_date_acquired'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='bookinstance', 18 | name='date_acquired', 19 | field=models.DateField(default=datetime.date.today), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /catalog/migrations/0014_remove_bookinstance_date_acquired.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-26 09:07 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0013_auto_20160926_1901'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='bookinstance', 17 | name='date_acquired', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /catalog/migrations/0015_auto_20160927_1808.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-27 08:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0014_remove_bookinstance_date_acquired'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='subject', 17 | name='subject_name', 18 | ), 19 | migrations.AddField( 20 | model_name='subject', 21 | name='name', 22 | field=models.CharField(default='Fantasy', help_text='Enter a book category - e.g. Science Fiction, French Poetry etc.', max_length=200), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /catalog/migrations/0016_auto_20160927_1947.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-27 09:47 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0015_auto_20160927_1808'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Genre', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(help_text='Enter a book genre (e.g. Science Fiction, French Poetry etc.)', max_length=200)), 20 | ], 21 | ), 22 | migrations.RemoveField( 23 | model_name='book', 24 | name='subject', 25 | ), 26 | migrations.AlterField( 27 | model_name='book', 28 | name='isbn', 29 | field=models.CharField(help_text='13 Character ISBN number', max_length=13, verbose_name='ISBN'), 30 | ), 31 | migrations.DeleteModel( 32 | name='Subject', 33 | ), 34 | migrations.AddField( 35 | model_name='book', 36 | name='genre', 37 | field=models.ManyToManyField(help_text='Select a genre for this book', to='catalog.Genre'), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /catalog/migrations/0017_language.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-10-05 10:12 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0016_auto_20160927_1947'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Language', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(help_text="Enter a the book's natural language (e.g. English, French, Japanese etc.)", max_length=200)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /catalog/migrations/0018_book_language.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-10-05 10:23 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('catalog', '0017_language'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='book', 18 | name='language', 19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Language'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /catalog/migrations/0019_bookinstance_borrower.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-10-11 09:40 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('catalog', '0018_book_language'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='bookinstance', 20 | name='borrower', 21 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /catalog/migrations/0020_auto_20161012_1044.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-10-11 23:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('catalog', '0019_bookinstance_borrower'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='bookinstance', 17 | options={'ordering': ['due_back'], 'permissions': (('can_mark_returned', 'Set book as returned'),)}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /catalog/migrations/0021_auto_20171229_1056.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2017-12-29 10:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('catalog', '0020_auto_20161012_1044'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='author', 15 | name='date_of_death', 16 | field=models.DateField(blank=True, null=True, verbose_name='died'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /catalog/migrations/0022_auto_20181028_1731.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-10-28 06:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('catalog', '0021_auto_20171229_1056'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='author', 15 | options={'ordering': ['last_name', 'first_name']}, 16 | ), 17 | migrations.AlterField( 18 | model_name='language', 19 | name='name', 20 | field=models.CharField(help_text="Enter the book's natural language (e.g. English, French, Japanese etc.)", max_length=200), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /catalog/migrations/0023_auto_20201201_0238.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-12-01 02:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('catalog', '0022_auto_20181028_1731'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='book', 15 | options={'ordering': ['title', 'author']}, 16 | ), 17 | migrations.AlterField( 18 | model_name='book', 19 | name='isbn', 20 | field=models.CharField(help_text='13 Character ISBN number', max_length=13, unique=True, verbose_name='ISBN'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /catalog/migrations/0024_auto_20210302_0630.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2021-03-02 06:30 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('catalog', '0023_auto_20201201_0238'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='bookinstance', 16 | name='book', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, to='catalog.book'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /catalog/migrations/0025_auto_20220222_0623.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-02-22 06:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('catalog', '0024_auto_20210302_0630'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='author', 15 | name='id', 16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 17 | ), 18 | migrations.AlterField( 19 | model_name='book', 20 | name='id', 21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 22 | ), 23 | migrations.AlterField( 24 | model_name='genre', 25 | name='id', 26 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 27 | ), 28 | migrations.AlterField( 29 | model_name='language', 30 | name='id', 31 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /catalog/migrations/0026_alter_book_author_alter_genre_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-02-23 01:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('catalog', '0025_auto_20220222_0623'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='book', 16 | name='author', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, to='catalog.author'), 18 | ), 19 | migrations.AlterField( 20 | model_name='genre', 21 | name='name', 22 | field=models.CharField(help_text='Enter a book genre (e.g. Science Fiction, French Poetry etc.)', max_length=200, unique=True), 23 | ), 24 | migrations.AlterField( 25 | model_name='language', 26 | name='name', 27 | field=models.CharField(help_text="Enter the book's natural language (e.g. English, French, Japanese etc.)", max_length=200, unique=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /catalog/migrations/0027_genre_genre_name_case_insensitive_unique_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-12 23:52 2 | 3 | import django.db.models.functions.text 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('catalog', '0026_alter_book_author_alter_genre_name_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddConstraint( 15 | model_name='genre', 16 | constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), name='genre_name_case_insensitive_unique', violation_error_message='Genre already exists (case insensitive match)'), 17 | ), 18 | migrations.AddConstraint( 19 | model_name='language', 20 | constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), name='language_name_case_insensitive_unique', violation_error_message='Language already exists (case insensitive match)'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /catalog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/a4ac27b88fc15c0b4526dddca8e980355b327479/catalog/migrations/__init__.py -------------------------------------------------------------------------------- /catalog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | 5 | from django.urls import reverse # To generate URLS by reversing URL patterns 6 | from django.db.models import UniqueConstraint 7 | from django.db.models.functions import Lower 8 | 9 | class Genre(models.Model): 10 | """Model representing a book genre (e.g. Science Fiction, Non Fiction).""" 11 | name = models.CharField( 12 | max_length=200, 13 | unique=True, 14 | help_text="Enter a book genre (e.g. Science Fiction, French Poetry etc.)" 15 | ) 16 | 17 | def __str__(self): 18 | """String for representing the Model object (in Admin site etc.)""" 19 | return self.name 20 | 21 | def get_absolute_url(self): 22 | """Returns the url to access a particular genre instance.""" 23 | return reverse('genre-detail', args=[str(self.id)]) 24 | 25 | class Meta: 26 | constraints = [ 27 | UniqueConstraint( 28 | Lower('name'), 29 | name='genre_name_case_insensitive_unique', 30 | violation_error_message = "Genre already exists (case insensitive match)" 31 | ), 32 | ] 33 | 34 | class Language(models.Model): 35 | """Model representing a Language (e.g. English, French, Japanese, etc.)""" 36 | name = models.CharField(max_length=200, 37 | unique=True, 38 | help_text="Enter the book's natural language (e.g. English, French, Japanese etc.)") 39 | 40 | def get_absolute_url(self): 41 | """Returns the url to access a particular language instance.""" 42 | return reverse('language-detail', args=[str(self.id)]) 43 | 44 | def __str__(self): 45 | """String for representing the Model object (in Admin site etc.)""" 46 | return self.name 47 | 48 | class Meta: 49 | constraints = [ 50 | UniqueConstraint( 51 | Lower('name'), 52 | name='language_name_case_insensitive_unique', 53 | violation_error_message = "Language already exists (case insensitive match)" 54 | ), 55 | ] 56 | 57 | class Book(models.Model): 58 | """Model representing a book (but not a specific copy of a book).""" 59 | title = models.CharField(max_length=200) 60 | author = models.ForeignKey('Author', on_delete=models.RESTRICT, null=True) 61 | # Foreign Key used because book can only have one author, but authors can have multiple books. 62 | # Author as a string rather than object because it hasn't been declared yet in file. 63 | summary = models.TextField( 64 | max_length=1000, help_text="Enter a brief description of the book") 65 | isbn = models.CharField('ISBN', max_length=13, 66 | unique=True, 67 | help_text='13 Character ISBN number') 69 | genre = models.ManyToManyField( 70 | Genre, help_text="Select a genre for this book") 71 | # ManyToManyField used because a genre can contain many books and a Book can cover many genres. 72 | # Genre class has already been defined so we can specify the object above. 73 | language = models.ForeignKey( 74 | 'Language', on_delete=models.SET_NULL, null=True) 75 | 76 | class Meta: 77 | ordering = ['title', 'author'] 78 | 79 | def display_genre(self): 80 | """Creates a string for the Genre. This is required to display genre in Admin.""" 81 | return ', '.join([genre.name for genre in self.genre.all()[:3]]) 82 | 83 | display_genre.short_description = 'Genre' 84 | 85 | def get_absolute_url(self): 86 | """Returns the url to access a particular book record.""" 87 | return reverse('book-detail', args=[str(self.id)]) 88 | 89 | def __str__(self): 90 | """String for representing the Model object.""" 91 | return self.title 92 | 93 | 94 | import uuid # Required for unique book instances 95 | from datetime import date 96 | 97 | from django.conf import settings # Required to assign User as a borrower 98 | 99 | 100 | class BookInstance(models.Model): 101 | """Model representing a specific copy of a book (i.e. that can be borrowed from the library).""" 102 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, 103 | help_text="Unique ID for this particular book across whole library") 104 | book = models.ForeignKey('Book', on_delete=models.RESTRICT, null=True) 105 | imprint = models.CharField(max_length=200) 106 | due_back = models.DateField(null=True, blank=True) 107 | borrower = models.ForeignKey( 108 | settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) 109 | 110 | @property 111 | def is_overdue(self): 112 | """Determines if the book is overdue based on due date and current date.""" 113 | return bool(self.due_back and date.today() > self.due_back) 114 | 115 | LOAN_STATUS = ( 116 | ('d', 'Maintenance'), 117 | ('o', 'On loan'), 118 | ('a', 'Available'), 119 | ('r', 'Reserved'), 120 | ) 121 | 122 | status = models.CharField( 123 | max_length=1, 124 | choices=LOAN_STATUS, 125 | blank=True, 126 | default='d', 127 | help_text='Book availability') 128 | 129 | class Meta: 130 | ordering = ['due_back'] 131 | permissions = (("can_mark_returned", "Set book as returned"),) 132 | 133 | def get_absolute_url(self): 134 | """Returns the url to access a particular book instance.""" 135 | return reverse('bookinstance-detail', args=[str(self.id)]) 136 | 137 | def __str__(self): 138 | """String for representing the Model object.""" 139 | return f'{self.id} ({self.book.title})' 140 | 141 | 142 | class Author(models.Model): 143 | """Model representing an author.""" 144 | first_name = models.CharField(max_length=100) 145 | last_name = models.CharField(max_length=100) 146 | date_of_birth = models.DateField(null=True, blank=True) 147 | date_of_death = models.DateField('died', null=True, blank=True) 148 | 149 | class Meta: 150 | ordering = ['last_name', 'first_name'] 151 | 152 | def get_absolute_url(self): 153 | """Returns the url to access a particular author instance.""" 154 | return reverse('author-detail', args=[str(self.id)]) 155 | 156 | def __str__(self): 157 | """String for representing the Model object.""" 158 | return f'{self.last_name}, {self.first_name}' 159 | -------------------------------------------------------------------------------- /catalog/static/css/styles.css: -------------------------------------------------------------------------------- 1 | .sidebar-nav { 2 | margin-top: 20px; 3 | padding: 0; 4 | list-style: none; 5 | } 6 | 7 | #logout-form { 8 | display: inline; 9 | } 10 | #logout-form button { 11 | padding: 0; 12 | margin: 0; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /catalog/static/images/local_library_model_uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/a4ac27b88fc15c0b4526dddca8e980355b327479/catalog/static/images/local_library_model_uml.png -------------------------------------------------------------------------------- /catalog/templates/base_generic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Local Library{% endblock %} 6 | 7 | 8 | 9 | 10 | {% load static %} 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | {% block sidebar %} 20 | 28 | 29 | 43 | 44 | {% if user.is_staff %} 45 |
46 | 65 | {% endif %} 66 | 67 | {% endblock %} 68 |
69 |
70 | {% block content %}{% endblock %} 71 | 72 | {% block pagination %} 73 | {% if is_paginated %} 74 | 87 | {% endif %} 88 | {% endblock %} 89 |
90 |
91 | 92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /catalog/templates/catalog/author_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Delete Author: {{ author }}

6 | 7 | {% if author.book_set.all %} 8 | 9 |

You can't delete this author until all their books have been deleted:

10 | 15 | 16 | {% else %} 17 |

Are you sure you want to delete the author?

18 | 19 |
20 | {% csrf_token %} 21 | 22 |
23 | {% endif %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /catalog/templates/catalog/author_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Author: {{ author }}

6 |

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

7 | 8 |
9 |

Books

10 | 11 |
12 | {% for book in author.book_set.all %} 13 |
{{book}} ({{book.bookinstance_set.all.count}})
14 |
{{book.summary}}
15 | {% empty %} 16 |

This author has no books.

17 | {% endfor %} 18 |
19 | 20 |
21 | {% endblock %} 22 | 23 | {% block sidebar %} 24 | {{ block.super }} 25 | 26 | {% if perms.catalog.change_author or perms.catalog.delete_author %} 27 |
28 | 36 | {% endif %} 37 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /catalog/templates/catalog/author_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 | 8 | {{ form.as_table }} 9 |
10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /catalog/templates/catalog/author_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Author List

6 | 7 | {% if author_list %} 8 | 19 | {% else %} 20 |

There are no authors available.

21 | {% endif %} 22 | 23 | 24 | 25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /catalog/templates/catalog/book_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Delete Book

6 | 7 | {% if book.bookinstance_set.all %} 8 |

You can't delete this book until all copies have been deleted:

9 | 10 | 15 | 16 | {% else %} 17 |

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

18 | 19 |
20 | {% csrf_token %} 21 | 22 |
23 | {% endif %} 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /catalog/templates/catalog/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Title: {{ book.title }}

6 | 7 |

Author: {{ book.author }}

8 |

Summary: {{ book.summary }}

9 |

ISBN: {{ book.isbn }}

10 |

Language: {{ book.language }}

11 |

Genre: {{ book.genre.all|join:", " }}

12 | 13 |
14 |

Copies

15 | 16 | {% for copy in book.bookinstance_set.all %} 17 |
18 |

{{ copy.get_status_display }}

19 | {% if copy.status != 'a' %}

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

{% endif %} 20 |

Imprint: {{copy.imprint}}

21 |

Id: {{copy.id}}

22 | {% empty %} 23 |

The library has no copies of this book.

24 | {% endfor %} 25 |
26 | {% endblock %} 27 | 28 | 29 | {% block sidebar %} 30 | {{ block.super }} 31 | 32 | {% if perms.catalog.change_book or perms.catalog.delete_book %} 33 |
34 | 42 | {% endif %} 43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /catalog/templates/catalog/book_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 | 8 | {{ form.as_table }} 9 |
10 | 11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /catalog/templates/catalog/book_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

Book List

5 | 6 | {% if book_list %} 7 | 16 | 17 | {% else %} 18 |

There are no books in the library.

19 | {% endif %} 20 | {% endblock %} 21 | 22 | -------------------------------------------------------------------------------- /catalog/templates/catalog/book_renew_librarian.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

Renew: {{book_instance.book.title}}

5 |

Borrower: {{book_instance.borrower}}

6 | Due date: {{book_instance.due_back}}

7 | 8 |
9 | {% csrf_token %} 10 | 11 | {{ form.as_table }} 12 |
13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /catalog/templates/catalog/bookinstance_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Delete Book Copy: {{ bookinstance }}

6 | 7 |

Are you sure you want to delete this copy of the book?

8 | 9 |
10 | {% csrf_token %} 11 | 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /catalog/templates/catalog/bookinstance_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

BookInstance: {{ bookinstance.book.title }}

6 | 7 |

Author: {{ bookinstance.book.author }}

8 | 9 |

Imprint: {{ bookinstance.imprint }}

10 |

Status: {{ bookinstance.get_status_display }} {% if bookinstance.status != 'a' %} (Due: {{bookinstance.due_back}}){% endif %}

11 | 12 |
13 | 18 | {% endblock %} 19 | 20 | 21 | {% block sidebar %} 22 | {{ block.super }} 23 | 24 | {% if perms.catalog.change_bookinstance or perms.catalog.delete_bookinstance %} 25 |
26 | 34 | {% endif %} 35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /catalog/templates/catalog/bookinstance_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 | 8 | {{ form.as_table }} 9 |
10 | 11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /catalog/templates/catalog/bookinstance_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

Book Copies in Library

5 | 6 | 19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /catalog/templates/catalog/bookinstance_list_borrowed_all.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

All Borrowed Books

5 | 6 | {% if bookinstance_list %} 7 | 15 | 16 | {% else %} 17 |

There are no books borrowed.

18 | {% endif %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /catalog/templates/catalog/bookinstance_list_borrowed_user.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

Borrowed books

5 | 6 | {% if bookinstance_list %} 7 | 15 | 16 | {% else %} 17 |

There are no books borrowed.

18 | {% endif %} 19 | {% endblock %} 20 | 21 | -------------------------------------------------------------------------------- /catalog/templates/catalog/genre_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Delete Genre: {{ genre }}

6 | 7 |

Are you sure you want to delete the genre?

8 | 9 |
10 | {% csrf_token %} 11 | 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /catalog/templates/catalog/genre_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Genre: {{ genre.name }}

6 | 7 |
8 |

Books in genre

9 | 10 |
    11 | {% for copy in genre.book_set.all %} 12 |
  • 13 | {{ copy.title }} ({{copy.author}}) 14 |
  • 15 | {% empty %} 16 |
  • There are no books in this genre.
  • 17 | {% endfor %} 18 |
19 | 20 | {% endblock %} 21 | 22 | 23 | {% block sidebar %} 24 | {{ block.super }} 25 | 26 | {% if perms.catalog.change_genre or perms.catalog.delete_genre %} 27 |
28 | 36 | {% endif %} 37 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /catalog/templates/catalog/genre_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 | 8 | {{ form.as_table }} 9 |
10 | 11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /catalog/templates/catalog/genre_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Genre List

6 | 7 | {% if genre_list %} 8 | 19 | {% else %} 20 |

There are no genres available.

21 | {% endif %} 22 | 23 | 24 | {% endblock %} 25 | 26 | -------------------------------------------------------------------------------- /catalog/templates/catalog/language_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Delete Language: {{ language }}

6 | 7 |

Are you sure you want to delete the language?

8 | 9 |
10 | {% csrf_token %} 11 | 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /catalog/templates/catalog/language_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Language: {{ language.name }}

6 | 7 |
8 |

Books in language

9 | 10 |
    11 | {% for copy in language.book_set.all %} 12 |
  • 13 | {{ copy.title }} 14 |
  • 15 | {% empty %} 16 |
  • There are no books in this language.
  • 17 | {% endfor %} 18 |
19 | 20 | {% endblock %} 21 | 22 | 23 | {% block sidebar %} 24 | {{ block.super }} 25 | 26 | {% if perms.catalog.change_language or perms.catalog.delete_language %} 27 |
28 | 36 | {% endif %} 37 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /catalog/templates/catalog/language_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 | 8 | {{ form.as_table }} 9 |
10 | 11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /catalog/templates/catalog/language_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

Language List

6 | 7 |
    8 | {% for language in language_list %} 9 |
  • 10 | {{ language }} 11 |
  • 12 | {% empty %} 13 |
  • There are no languages available.
  • 14 | {% endfor %} 15 |
16 | 17 | {% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /catalog/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

Local Library Home

5 | 6 |

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

7 |

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.

8 | 9 | 10 |

UML Models

11 |

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

12 | 13 |
14 | {% load static %} 15 | My image 16 |
17 | 18 | 19 |

Dynamic content

20 | 21 |

The library has the following record counts:

22 |
    23 |
  • Books: {{ num_books }}
  • 24 |
  • Copies: {{ num_instances }}
  • 25 |
  • Copies available: {{ num_instances_available }}
  • 26 |
  • Authors: {{ num_authors }}
  • 27 |
28 | 29 | 30 |

You have visited this page {{ num_visits }} time{{ num_visits|pluralize }}.

31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /catalog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/a4ac27b88fc15c0b4526dddca8e980355b327479/catalog/tests/__init__.py -------------------------------------------------------------------------------- /catalog/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | 5 | import datetime 6 | from catalog.forms import RenewBookForm 7 | 8 | 9 | class RenewBookFormTest(TestCase): 10 | 11 | def test_renew_form_date_in_past(self): 12 | """Test form is invalid if renewal_date is before today.""" 13 | date = datetime.date.today() - datetime.timedelta(days=1) 14 | form = RenewBookForm(data={'renewal_date': date}) 15 | self.assertFalse(form.is_valid()) 16 | 17 | def test_renew_form_date_too_far_in_future(self): 18 | """Test form is invalid if renewal_date more than 4 weeks from today.""" 19 | date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1) 20 | form = RenewBookForm(data={'renewal_date': date}) 21 | self.assertFalse(form.is_valid()) 22 | 23 | def test_renew_form_date_today(self): 24 | """Test form is valid if renewal_date is today.""" 25 | date = datetime.date.today() 26 | form = RenewBookForm(data={'renewal_date': date}) 27 | self.assertTrue(form.is_valid()) 28 | 29 | def test_renew_form_date_max(self): 30 | """Test form is valid if renewal_date is within 4 weeks.""" 31 | date = datetime.date.today() + datetime.timedelta(weeks=4) 32 | form = RenewBookForm(data={'renewal_date': date}) 33 | self.assertTrue(form.is_valid()) 34 | 35 | def test_renew_form_date_field_label(self): 36 | """Test renewal_date label is 'renewal date'.""" 37 | form = RenewBookForm() 38 | self.assertTrue( 39 | form.fields['renewal_date'].label is None or 40 | form.fields['renewal_date'].label == 'renewal date') 41 | 42 | def test_renew_form_date_field_help_text(self): 43 | """Test renewal_date help_text is as expected.""" 44 | form = RenewBookForm() 45 | self.assertEqual( 46 | form.fields['renewal_date'].help_text, 47 | 'Enter a date between now and 4 weeks (default 3).') 48 | -------------------------------------------------------------------------------- /catalog/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | 5 | from catalog.models import Author 6 | 7 | 8 | class AuthorModelTest(TestCase): 9 | 10 | @classmethod 11 | def setUpTestData(cls): 12 | """Set up non-modified objects used by all test methods.""" 13 | Author.objects.create(first_name='Big', last_name='Bob') 14 | 15 | def test_first_name_label(self): 16 | author = Author.objects.get(id=1) 17 | field_label = author._meta.get_field('first_name').verbose_name 18 | self.assertEqual(field_label, 'first name') 19 | 20 | def test_last_name_label(self): 21 | author = Author.objects.get(id=1) 22 | field_label = author._meta.get_field('last_name').verbose_name 23 | self.assertEqual(field_label, 'last name') 24 | 25 | def test_date_of_birth_label(self): 26 | author = Author.objects.get(id=1) 27 | field_label = author._meta.get_field('date_of_birth').verbose_name 28 | self.assertEqual(field_label, 'date of birth') 29 | 30 | def test_date_of_death_label(self): 31 | author = Author.objects.get(id=1) 32 | field_label = author._meta.get_field('date_of_death').verbose_name 33 | self.assertEqual(field_label, 'died') 34 | 35 | def test_first_name_max_length(self): 36 | author = Author.objects.get(id=1) 37 | max_length = author._meta.get_field('first_name').max_length 38 | self.assertEqual(max_length, 100) 39 | 40 | def test_last_name_max_length(self): 41 | author = Author.objects.get(id=1) 42 | max_length = author._meta.get_field('last_name').max_length 43 | self.assertEqual(max_length, 100) 44 | 45 | def test_object_name_is_last_name_comma_first_name(self): 46 | author = Author.objects.get(id=1) 47 | expected_object_name = '{0}, {1}'.format(author.last_name, author.first_name) 48 | 49 | self.assertEqual(str(author), expected_object_name) 50 | 51 | def test_get_absolute_url(self): 52 | author = Author.objects.get(id=1) 53 | # This will also fail if the urlconf is not defined. 54 | self.assertEqual(author.get_absolute_url(), '/catalog/author/1') 55 | -------------------------------------------------------------------------------- /catalog/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | 5 | 6 | from catalog.models import Author 7 | from django.urls import reverse 8 | 9 | 10 | class AuthorListViewTest(TestCase): 11 | 12 | @classmethod 13 | def setUpTestData(cls): 14 | # Create authors for pagination tests 15 | number_of_authors = 13 16 | for author_id in range(number_of_authors): 17 | Author.objects.create(first_name='Christian {0}'.format(author_id), 18 | last_name='Surname {0}'.format(author_id)) 19 | 20 | def test_view_url_exists_at_desired_location(self): 21 | response = self.client.get('/catalog/authors/') 22 | self.assertEqual(response.status_code, 200) 23 | 24 | def test_view_url_accessible_by_name(self): 25 | response = self.client.get(reverse('authors')) 26 | self.assertEqual(response.status_code, 200) 27 | 28 | def test_view_uses_correct_template(self): 29 | response = self.client.get(reverse('authors')) 30 | self.assertEqual(response.status_code, 200) 31 | self.assertTemplateUsed(response, 'catalog/author_list.html') 32 | 33 | def test_pagination_is_ten(self): 34 | response = self.client.get(reverse('authors')) 35 | self.assertEqual(response.status_code, 200) 36 | self.assertTrue('is_paginated' in response.context) 37 | self.assertTrue(response.context['is_paginated'] is True) 38 | self.assertEqual(len(response.context['author_list']), 10) 39 | 40 | def test_lists_all_authors(self): 41 | # Get second page and confirm it has (exactly) the remaining 3 items 42 | response = self.client.get(reverse('authors')+'?page=2') 43 | self.assertEqual(response.status_code, 200) 44 | self.assertTrue('is_paginated' in response.context) 45 | self.assertTrue(response.context['is_paginated'] is True) 46 | self.assertEqual(len(response.context['author_list']), 3) 47 | 48 | 49 | import datetime 50 | from django.utils import timezone 51 | 52 | from catalog.models import BookInstance, Book, Genre, Language 53 | 54 | # Get user model from settings 55 | from django.contrib.auth import get_user_model 56 | User = get_user_model() 57 | 58 | 59 | class LoanedBookInstancesByUserListViewTest(TestCase): 60 | 61 | def setUp(self): 62 | # Create two users 63 | test_user1 = User.objects.create_user( 64 | username='testuser1', password='1X', views.BookDetailView.as_view(), name='book-detail'), 10 | path('authors/', views.AuthorListView.as_view(), name='authors'), 11 | path('author/', 12 | views.AuthorDetailView.as_view(), name='author-detail'), 13 | ] 14 | 15 | 16 | urlpatterns += [ 17 | path('mybooks/', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'), 18 | path(r'borrowed/', views.LoanedBooksAllListView.as_view(), name='all-borrowed'), # Added for challenge 19 | ] 20 | 21 | 22 | # Add URLConf for librarian to renew a book. 23 | urlpatterns += [ 24 | path('book//renew/', views.renew_book_librarian, name='renew-book-librarian'), 25 | ] 26 | 27 | 28 | # Add URLConf to create, update, and delete authors 29 | urlpatterns += [ 30 | path('author/create/', views.AuthorCreate.as_view(), name='author-create'), 31 | path('author//update/', views.AuthorUpdate.as_view(), name='author-update'), 32 | path('author//delete/', views.AuthorDelete.as_view(), name='author-delete'), 33 | ] 34 | 35 | # Add URLConf to create, update, and delete books 36 | urlpatterns += [ 37 | path('book/create/', views.BookCreate.as_view(), name='book-create'), 38 | path('book//update/', views.BookUpdate.as_view(), name='book-update'), 39 | path('book//delete/', views.BookDelete.as_view(), name='book-delete'), 40 | ] 41 | 42 | 43 | # Add URLConf to list, view, create, update, and delete genre 44 | urlpatterns += [ 45 | path('genres/', views.GenreListView.as_view(), name='genres'), 46 | path('genre/', views.GenreDetailView.as_view(), name='genre-detail'), 47 | path('genre/create/', views.GenreCreate.as_view(), name='genre-create'), 48 | path('genre//update/', views.GenreUpdate.as_view(), name='genre-update'), 49 | path('genre//delete/', views.GenreDelete.as_view(), name='genre-delete'), 50 | ] 51 | 52 | # Add URLConf to list, view, create, update, and delete languages 53 | urlpatterns += [ 54 | path('languages/', views.LanguageListView.as_view(), name='languages'), 55 | path('language/', views.LanguageDetailView.as_view(), 56 | name='language-detail'), 57 | path('language/create/', views.LanguageCreate.as_view(), name='language-create'), 58 | path('language//update/', 59 | views.LanguageUpdate.as_view(), name='language-update'), 60 | path('language//delete/', 61 | views.LanguageDelete.as_view(), name='language-delete'), 62 | ] 63 | 64 | # Add URLConf to list, view, create, update, and delete bookinstances 65 | urlpatterns += [ 66 | path('bookinstances/', views.BookInstanceListView.as_view(), name='bookinstances'), 67 | path('bookinstance/', views.BookInstanceDetailView.as_view(), 68 | name='bookinstance-detail'), 69 | path('bookinstance/create/', views.BookInstanceCreate.as_view(), 70 | name='bookinstance-create'), 71 | path('bookinstance//update/', 72 | views.BookInstanceUpdate.as_view(), name='bookinstance-update'), 73 | path('bookinstance//delete/', 74 | views.BookInstanceDelete.as_view(), name='bookinstance-delete'), 75 | ] 76 | -------------------------------------------------------------------------------- /catalog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | 5 | from .models import Book, Author, BookInstance, Genre, Language 6 | 7 | def index(request): 8 | """View function for home page of site.""" 9 | # Generate counts of some of the main objects 10 | num_books = Book.objects.all().count() 11 | num_instances = BookInstance.objects.all().count() 12 | # Available copies of books 13 | num_instances_available = BookInstance.objects.filter( 14 | status__exact='a').count() 15 | num_authors = Author.objects.count() # The 'all()' is implied by default. 16 | 17 | # Number of visits to this view, as counted in the session variable. 18 | num_visits = request.session.get('num_visits', 0) 19 | num_visits += 1 20 | request.session['num_visits'] = num_visits 21 | 22 | # Render the HTML template index.html with the data in the context variable. 23 | return render( 24 | request, 25 | 'index.html', 26 | context={'num_books': num_books, 'num_instances': num_instances, 27 | 'num_instances_available': num_instances_available, 'num_authors': num_authors, 28 | 'num_visits': num_visits}, 29 | ) 30 | 31 | from django.views import generic 32 | 33 | 34 | class BookListView(generic.ListView): 35 | """Generic class-based view for a list of books.""" 36 | model = Book 37 | paginate_by = 10 38 | 39 | class BookDetailView(generic.DetailView): 40 | """Generic class-based detail view for a book.""" 41 | model = Book 42 | 43 | class AuthorListView(generic.ListView): 44 | """Generic class-based list view for a list of authors.""" 45 | model = Author 46 | paginate_by = 10 47 | 48 | class AuthorDetailView(generic.DetailView): 49 | """Generic class-based detail view for an author.""" 50 | model = Author 51 | 52 | 53 | class GenreDetailView(generic.DetailView): 54 | """Generic class-based detail view for a genre.""" 55 | model = Genre 56 | 57 | class GenreListView(generic.ListView): 58 | """Generic class-based list view for a list of genres.""" 59 | model = Genre 60 | paginate_by = 10 61 | 62 | class LanguageDetailView(generic.DetailView): 63 | """Generic class-based detail view for a genre.""" 64 | model = Language 65 | 66 | class LanguageListView(generic.ListView): 67 | """Generic class-based list view for a list of genres.""" 68 | model = Language 69 | paginate_by = 10 70 | 71 | class BookInstanceListView(generic.ListView): 72 | """Generic class-based view for a list of books.""" 73 | model = BookInstance 74 | paginate_by = 10 75 | 76 | class BookInstanceDetailView(generic.DetailView): 77 | """Generic class-based detail view for a book.""" 78 | model = BookInstance 79 | 80 | from django.contrib.auth.mixins import LoginRequiredMixin 81 | 82 | class LoanedBooksByUserListView(LoginRequiredMixin, generic.ListView): 83 | """Generic class-based view listing books on loan to current user.""" 84 | model = BookInstance 85 | template_name = 'catalog/bookinstance_list_borrowed_user.html' 86 | paginate_by = 10 87 | 88 | def get_queryset(self): 89 | return ( 90 | BookInstance.objects.filter(borrower=self.request.user) 91 | .filter(status__exact='o') 92 | .order_by('due_back') 93 | ) 94 | 95 | # Added as part of challenge! 96 | from django.contrib.auth.mixins import PermissionRequiredMixin 97 | 98 | 99 | class LoanedBooksAllListView(PermissionRequiredMixin, generic.ListView): 100 | """Generic class-based view listing all books on loan. Only visible to users with can_mark_returned permission.""" 101 | model = BookInstance 102 | permission_required = 'catalog.can_mark_returned' 103 | template_name = 'catalog/bookinstance_list_borrowed_all.html' 104 | paginate_by = 10 105 | 106 | def get_queryset(self): 107 | return BookInstance.objects.filter(status__exact='o').order_by('due_back') 108 | 109 | from django.shortcuts import get_object_or_404 110 | from django.http import HttpResponseRedirect 111 | from django.urls import reverse 112 | import datetime 113 | from django.contrib.auth.decorators import login_required, permission_required 114 | from catalog.forms import RenewBookForm 115 | 116 | 117 | @login_required 118 | @permission_required('catalog.can_mark_returned', raise_exception=True) 119 | def renew_book_librarian(request, pk): 120 | """View function for renewing a specific BookInstance by librarian.""" 121 | book_instance = get_object_or_404(BookInstance, pk=pk) 122 | 123 | # If this is a POST request then process the Form data 124 | if request.method == 'POST': 125 | 126 | # Create a form instance and populate it with data from the request (binding): 127 | form = RenewBookForm(request.POST) 128 | 129 | # Check if the form is valid: 130 | if form.is_valid(): 131 | # process the data in form.cleaned_data as required (here we just write it to the model due_back field) 132 | book_instance.due_back = form.cleaned_data['renewal_date'] 133 | book_instance.save() 134 | 135 | # redirect to a new URL: 136 | return HttpResponseRedirect(reverse('all-borrowed')) 137 | 138 | # If this is a GET (or any other method) create the default form 139 | else: 140 | proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) 141 | form = RenewBookForm(initial={'renewal_date': proposed_renewal_date}) 142 | 143 | context = { 144 | 'form': form, 145 | 'book_instance': book_instance, 146 | } 147 | 148 | return render(request, 'catalog/book_renew_librarian.html', context) 149 | 150 | 151 | from django.views.generic.edit import CreateView, UpdateView, DeleteView 152 | from django.urls import reverse_lazy 153 | from .models import Author 154 | 155 | 156 | class AuthorCreate(PermissionRequiredMixin, CreateView): 157 | model = Author 158 | fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death'] 159 | initial = {'date_of_death': '11/11/2023'} 160 | permission_required = 'catalog.add_author' 161 | 162 | class AuthorUpdate(PermissionRequiredMixin, UpdateView): 163 | model = Author 164 | # Not recommended (potential security issue if more fields added) 165 | fields = '__all__' 166 | permission_required = 'catalog.change_author' 167 | 168 | class AuthorDelete(PermissionRequiredMixin, DeleteView): 169 | model = Author 170 | success_url = reverse_lazy('authors') 171 | permission_required = 'catalog.delete_author' 172 | 173 | def form_valid(self, form): 174 | try: 175 | self.object.delete() 176 | return HttpResponseRedirect(self.success_url) 177 | except Exception as e: 178 | return HttpResponseRedirect( 179 | reverse("author-delete", kwargs={"pk": self.object.pk}) 180 | ) 181 | 182 | # Classes created for the forms challenge 183 | 184 | 185 | class BookCreate(PermissionRequiredMixin, CreateView): 186 | model = Book 187 | fields = ['title', 'author', 'summary', 'isbn', 'genre', 'language'] 188 | permission_required = 'catalog.add_book' 189 | 190 | 191 | class BookUpdate(PermissionRequiredMixin, UpdateView): 192 | model = Book 193 | fields = ['title', 'author', 'summary', 'isbn', 'genre', 'language'] 194 | permission_required = 'catalog.change_book' 195 | 196 | 197 | class BookDelete(PermissionRequiredMixin, DeleteView): 198 | model = Book 199 | success_url = reverse_lazy('books') 200 | permission_required = 'catalog.delete_book' 201 | 202 | def form_valid(self, form): 203 | try: 204 | self.object.delete() 205 | return HttpResponseRedirect(self.success_url) 206 | except Exception as e: 207 | return HttpResponseRedirect( 208 | reverse("book-delete", kwargs={"pk": self.object.pk}) 209 | ) 210 | 211 | 212 | class GenreCreate(PermissionRequiredMixin, CreateView): 213 | model = Genre 214 | fields = ['name', ] 215 | permission_required = 'catalog.add_genre' 216 | 217 | 218 | class GenreUpdate(PermissionRequiredMixin, UpdateView): 219 | model = Genre 220 | fields = ['name', ] 221 | permission_required = 'catalog.change_genre' 222 | 223 | 224 | class GenreDelete(PermissionRequiredMixin, DeleteView): 225 | model = Genre 226 | success_url = reverse_lazy('genres') 227 | permission_required = 'catalog.delete_genre' 228 | 229 | 230 | class LanguageCreate(PermissionRequiredMixin, CreateView): 231 | model = Language 232 | fields = ['name', ] 233 | permission_required = 'catalog.add_language' 234 | 235 | 236 | class LanguageUpdate(PermissionRequiredMixin, UpdateView): 237 | model = Language 238 | fields = ['name', ] 239 | permission_required = 'catalog.change_language' 240 | 241 | 242 | class LanguageDelete(PermissionRequiredMixin, DeleteView): 243 | model = Language 244 | success_url = reverse_lazy('languages') 245 | permission_required = 'catalog.delete_language' 246 | 247 | 248 | class BookInstanceCreate(PermissionRequiredMixin, CreateView): 249 | model = BookInstance 250 | fields = ['book', 'imprint', 'due_back', 'borrower', 'status'] 251 | permission_required = 'catalog.add_bookinstance' 252 | 253 | 254 | class BookInstanceUpdate(PermissionRequiredMixin, UpdateView): 255 | model = BookInstance 256 | # fields = "__all__" 257 | fields = ['imprint', 'due_back', 'borrower', 'status'] 258 | permission_required = 'catalog.change_bookinstance' 259 | 260 | 261 | class BookInstanceDelete(PermissionRequiredMixin, DeleteView): 262 | model = BookInstance 263 | success_url = reverse_lazy('bookinstances') 264 | permission_required = 'catalog.delete_bookinstance' 265 | -------------------------------------------------------------------------------- /locallibrary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/a4ac27b88fc15c0b4526dddca8e980355b327479/locallibrary/__init__.py -------------------------------------------------------------------------------- /locallibrary/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for locallibrary project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'locallibrary.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /locallibrary/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for locallibrary project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Add support for env variables from file if defined 20 | from dotenv import load_dotenv 21 | import os 22 | env_path = load_dotenv(os.path.join(BASE_DIR, '.env')) 23 | load_dotenv(env_path) 24 | 25 | # Quick-start development settings - unsuitable for production 26 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 27 | 28 | # SECURITY WARNING: keep the secret key used in production secret! 29 | # SECRET_KEY = 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87' 30 | SECRET_KEY = os.environ.get( 31 | 'DJANGO_SECRET_KEY', 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87') 32 | 33 | # SECURITY WARNING: don't run with debug turned on in production! 34 | DEBUG = True 35 | # DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False' 36 | 37 | # Set hosts to allow any app on Railway and the local testing URL 38 | ALLOWED_HOSTS = ['.railway.app', '.pythonanywhere.com', '127.0.0.1'] 39 | 40 | # Set CSRF trusted origins to allow any app on Railway and the local testing URL 41 | CSRF_TRUSTED_ORIGINS = ['https://*.railway.app', 42 | 'https://*.pythonanywhere.com'] 43 | 44 | 45 | # Application definition 46 | 47 | INSTALLED_APPS = [ 48 | 'django.contrib.admin', 49 | 'django.contrib.auth', 50 | 'django.contrib.contenttypes', 51 | 'django.contrib.sessions', 52 | 'django.contrib.messages', 53 | 'django.contrib.staticfiles', 54 | # Add our new application 55 | 'catalog.apps.CatalogConfig', # This object was created for us in /catalog/apps.py 56 | ] 57 | 58 | MIDDLEWARE = [ 59 | 'django.middleware.security.SecurityMiddleware', 60 | 'whitenoise.middleware.WhiteNoiseMiddleware', 61 | 'django.contrib.sessions.middleware.SessionMiddleware', 62 | 'django.middleware.common.CommonMiddleware', 63 | 'django.middleware.csrf.CsrfViewMiddleware', 64 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 65 | 'django.contrib.messages.middleware.MessageMiddleware', 66 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 67 | ] 68 | 69 | ROOT_URLCONF = 'locallibrary.urls' 70 | 71 | TEMPLATES = [ 72 | { 73 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 74 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 75 | 'APP_DIRS': True, 76 | 'OPTIONS': { 77 | 'context_processors': [ 78 | 'django.template.context_processors.debug', 79 | 'django.template.context_processors.request', 80 | 'django.contrib.auth.context_processors.auth', 81 | 'django.contrib.messages.context_processors.messages', 82 | ], 83 | }, 84 | }, 85 | ] 86 | 87 | WSGI_APPLICATION = 'locallibrary.wsgi.application' 88 | 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 92 | 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.sqlite3', 96 | 'NAME': BASE_DIR / 'db.sqlite3', 97 | } 98 | } 99 | 100 | 101 | # Password validation 102 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 103 | 104 | AUTH_PASSWORD_VALIDATORS = [ 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 116 | }, 117 | ] 118 | 119 | 120 | # Internationalization 121 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 122 | 123 | LANGUAGE_CODE = 'en-us' 124 | 125 | TIME_ZONE = 'UTC' 126 | 127 | USE_I18N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Redirect to home URL after login (Default redirects to /accounts/profile/) 133 | LOGIN_REDIRECT_URL = '/' 134 | 135 | # Add to test email: 136 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 137 | 138 | # Update database configuration from $DATABASE_URL environment variable (if defined) 139 | import dj_database_url 140 | if 'DATABASE_URL' in os.environ: 141 | DATABASES['default'] = dj_database_url.config( 142 | conn_max_age=500, 143 | conn_health_checks=True, 144 | ) 145 | 146 | 147 | # Static files (CSS, JavaScript, Images) 148 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 149 | # The absolute path to the directory where collectstatic will collect static files for deployment. 150 | STATIC_ROOT = BASE_DIR / 'staticfiles' 151 | # The URL to use when referring to static files (where they will be served from) 152 | STATIC_URL = '/static/' 153 | 154 | 155 | # Static file serving. 156 | # https://whitenoise.readthedocs.io/en/stable/django.html#add-compression-and-caching-support 157 | STORAGES = { 158 | # ... 159 | "staticfiles": { 160 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 161 | }, 162 | } 163 | 164 | # Default primary key field type 165 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 166 | 167 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 168 | -------------------------------------------------------------------------------- /locallibrary/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for locallibrary project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | # Use include() to add URLS from the catalog application and authentication system 21 | from django.urls import include 22 | 23 | 24 | urlpatterns = [ 25 | path('admin/', admin.site.urls), 26 | ] 27 | 28 | 29 | urlpatterns += [ 30 | path('catalog/', include('catalog.urls')), 31 | ] 32 | 33 | 34 | # Use static() to add url mapping to serve static files during development (only) 35 | from django.conf import settings 36 | from django.conf.urls.static import static 37 | 38 | 39 | urlpatterns+= static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 40 | 41 | 42 | # Add URL maps to redirect the base URL to our application 43 | from django.views.generic import RedirectView 44 | urlpatterns += [ 45 | path('', RedirectView.as_view(url='/catalog/', permanent=True)), 46 | ] 47 | 48 | 49 | 50 | # Add Django site authentication urls (for login, logout, password management) 51 | urlpatterns += [ 52 | path('accounts/', include('django.contrib.auth.urls')), 53 | ] 54 | -------------------------------------------------------------------------------- /locallibrary/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for locallibrary project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'locallibrary.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'locallibrary.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==5.1.10 2 | dj-database-url==2.1.0 3 | gunicorn==22.0.0 4 | psycopg2-binary==2.9.9 5 | wheel==0.38.1 6 | whitenoise==6.6.0 7 | python-dotenv==1.0.1 8 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.2 2 | -------------------------------------------------------------------------------- /templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 |

Logged out!

5 | 6 | Click here to login again. 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if form.errors %} 6 |

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

7 | {% endif %} 8 | 9 | {% if next %} 10 | {% if user.is_authenticated %} 11 |

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

13 | {% else %} 14 |

Please login to see this page.

15 | {% endif %} 16 | {% endif %} 17 | 18 |
19 | {% csrf_token %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
30 | 31 | 32 | 33 |
34 | 35 | {# Assumes you setup the password_reset view in your URLconf #} 36 |

Lost password?

37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

The password has been changed!

6 |

log in again?

7 | 8 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if validlink %} 6 |

Please enter (and confirm) your new password.

7 |
8 |
9 | 10 |
11 | 12 | 13 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
{{ form.new_password1.errors }} 14 | {{ form.new_password1 }}
{{ form.new_password2.errors }} 19 | {{ form.new_password2 }}
27 |
28 | {% else %} 29 |

Password reset failed

30 |

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

31 | {% endif %} 32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |

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

6 | 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | Someone asked for password reset for email {{ email }}. Follow the link below: 2 | {{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 3 | -------------------------------------------------------------------------------- /templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block content %} 4 | 5 |
{% csrf_token %} 6 | {% if form.email.errors %}{{ form.email.errors }}{% endif %} 7 |

{{ form.email }}

8 | 9 |
10 | 11 | {% endblock %} --------------------------------------------------------------------------------