Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,33 @@ This web application creates an online catalog for a small local library, where

The main features that have currently been implemented are:

* There are models for books, book copies, genre, language and authors.
* Users can view list and detail information for books and authors.
* Admin users can create and manage models. The admin has been optimised (the basic registration is present in admin.py, but commented out).
* Librarians can renew reserved books.
- There are models for books, book copies, genre, language and authors.
- Users can view list and detail information for books and authors.
- Admin users can create and manage models. The admin has been optimised (the basic registration is present in admin.py, but commented out).
- Librarians can renew reserved books.

![Local Library Model](https://raw.githubusercontent.com/mdn/django-locallibrary-tutorial/master/catalog/static/images/local_library_model_uml.png)


## Quick Start

To get this project up and running locally on your computer:
1. Set up the [Python development environment](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/development_environment).
We recommend using a Python virtual environment.
> **Note:** This has been tested against Django 3.10 (and may not work or be "optimal" for other versions).
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):

1. Set up the [Python development environment](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Server-side/Django/development_environment).
> **Note:** This has been tested against Python 3.10 (and may not work or be "optimal" for other versions).
2. Create and activate a Python virtual environment for the project using [venv](https://docs.python.org/3/library/venv.html), the virtual environment tool built into Python:

```
# Linux/macOS
python3 -m venv django6_env
source django6_env/bin/activate

# Windows
py -3 -m venv django6_env
django6_env\Scripts\activate
```

3. Assuming your virtual environment is active, run the following commands (if you're on Windows you may use `py` or `py -3` instead of `python` to start Python):

```
pip3 install -r requirements.txt
python3 manage.py makemigrations
Expand All @@ -34,6 +46,7 @@ To get this project up and running locally on your computer:
python3 manage.py createsuperuser # Create a superuser
python3 manage.py runserver
```
1. Open a browser to `http://127.0.0.1:8000/admin/` to open the admin site
1. Create a few test objects of each type.
1. Open tab to `http://127.0.0.1:8000` to see the main site, with your new objects.

4. Open a browser to `http://127.0.0.1:8000/admin/` to open the admin site
5. Create a few test objects of each type.
6. Open tab to `http://127.0.0.1:8000` to see the main site, with your new objects.
19 changes: 7 additions & 12 deletions catalog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@

from .models import Author, Genre, Book, BookInstance, Language

"""Minimal registration of Models.
admin.site.register(Book)
admin.site.register(Author)
admin.site.register(BookInstance)
admin.site.register(Genre)
admin.site.register(Language)
"""

# admin.site.register(Book)
# admin.site.register(Author)
# admin.site.register(BookInstance)
admin.site.register(Genre)
admin.site.register(Language)

Expand All @@ -21,7 +16,6 @@ class BooksInline(admin.TabularInline):
model = Book


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
"""Administration object for Author models.
Defines:
Expand All @@ -36,11 +30,15 @@ class AuthorAdmin(admin.ModelAdmin):
inlines = [BooksInline]


admin.site.register(Author, AuthorAdmin)


class BooksInstanceInline(admin.TabularInline):
"""Defines format of inline book instance insertion (used in BookAdmin)"""
model = BookInstance


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
"""Administration object for Book models.
Defines:
Expand All @@ -51,9 +49,6 @@ class BookAdmin(admin.ModelAdmin):
inlines = [BooksInstanceInline]


admin.site.register(Book, BookAdmin)


@admin.register(BookInstance)
class BookInstanceAdmin(admin.ModelAdmin):
"""Administration object for BookInstance models.
Expand Down
1 change: 0 additions & 1 deletion catalog/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@


class CatalogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'catalog'
18 changes: 18 additions & 0 deletions catalog/migrations/0028_alter_bookinstance_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.1.1 on 2026-09-04 06:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('catalog', '0027_genre_genre_name_case_insensitive_unique_and_more'),
]

operations = [
migrations.AlterField(
model_name='bookinstance',
name='status',
field=models.CharField(blank=True, choices=[('m', 'Maintenance'), ('o', 'On loan'), ('a', 'Available'), ('r', 'Reserved')], default='m', help_text='Book availability', max_length=1),
),
]
15 changes: 7 additions & 8 deletions catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,17 @@ def is_overdue(self):
"""Determines if the book is overdue based on due date and current date."""
return bool(self.due_back and date.today() > self.due_back)

LOAN_STATUS = (
('d', 'Maintenance'),
('o', 'On loan'),
('a', 'Available'),
('r', 'Reserved'),
)
class LoanStatus(models.TextChoices):
MAINTENANCE = 'm', 'Maintenance'
ON_LOAN = 'o', 'On loan'
AVAILABLE = 'a', 'Available'
RESERVED = 'r', 'Reserved'

status = models.CharField(
max_length=1,
choices=LOAN_STATUS,
choices=LoanStatus,
blank=True,
default='d',
default=LoanStatus.MAINTENANCE,
help_text='Book availability')

class Meta:
Expand Down
4 changes: 2 additions & 2 deletions catalog/templates/catalog/book_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ <h4>Copies</h4>

{% for copy in book.bookinstance_set.all %}
<hr>
<p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'd' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
{% if copy.status != 'a' %}<p><strong>Due to be returned:</strong> {{copy.due_back}}</p>{% endif %}
<p class="{% if copy.status == copy.LoanStatus.AVAILABLE %}text-success{% elif copy.status == copy.LoanStatus.MAINTENANCE %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
{% if copy.status != copy.LoanStatus.AVAILABLE %}<p><strong>Due to be returned:</strong> {{copy.due_back}}</p>{% endif %}
<p><strong>Imprint:</strong> {{copy.imprint}}</p>
<p class="text-muted"><strong>Id:</strong> <a href="{{ copy.get_absolute_url }}">{{copy.id}}</a></p>
{% empty %}
Expand Down
2 changes: 1 addition & 1 deletion catalog/templates/catalog/bookinstance_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ <h1>BookInstance: {{ bookinstance.book.title }}</h1>
<p><strong>Author:</strong> <a href="{{ bookinstance.book.author.get_absolute_url }}">{{ bookinstance.book.author }}</a></p>

<p><strong>Imprint:</strong> {{ bookinstance.imprint }}</p>
<p><strong>Status:</strong> {{ bookinstance.get_status_display }} {% if bookinstance.status != 'a' %} (Due: {{bookinstance.due_back}}){% endif %}</p>
<p><strong>Status:</strong> {{ bookinstance.get_status_display }} {% if bookinstance.status != bookinstance.LoanStatus.AVAILABLE %} (Due: {{bookinstance.due_back}}){% endif %}</p>

<hr>
<ul>
Expand Down
4 changes: 2 additions & 2 deletions catalog/templates/catalog/bookinstance_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ <h1>Book Copies in Library</h1>
{% for bookinst in bookinstance_list %}
<li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
<a href="{% url 'bookinstance-detail' bookinst.pk %}">{{bookinst.book.title}}</a> ({{ bookinst.get_status_display }})
{% if bookinst.status != 'a' %}: {{ bookinst.due_back }} {% endif %}
{% if bookinst.status == 'o' %}
{% if bookinst.status != bookinst.LoanStatus.AVAILABLE %}: {{ bookinst.due_back }} {% endif %}
{% if bookinst.status == bookinst.LoanStatus.ON_LOAN %}
{% if user.is_staff %}- {{ bookinst.borrower }}{% endif %} {% if perms.catalog.can_mark_returned %}- <a href="{% url 'renew-book-librarian' bookinst.id %}">Renew</a> {% endif %}
{% endif %}
</li>
Expand Down
14 changes: 7 additions & 7 deletions catalog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def setUp(self):
the_borrower = test_user1
else:
the_borrower = test_user2
status = 'm'
status = BookInstance.LoanStatus.MAINTENANCE
BookInstance.objects.create(book=test_book, imprint='Unlikely Imprint, 2016', due_back=return_date,
borrower=the_borrower, status=status)

Expand Down Expand Up @@ -134,7 +134,7 @@ def test_only_borrowed_books_in_list(self):
get_ten_books = BookInstance.objects.all()[:10]

for copy in get_ten_books:
copy.status = 'o'
copy.status = BookInstance.LoanStatus.ON_LOAN
copy.save()

# Check that now we have borrowed books in the list
Expand All @@ -149,14 +149,14 @@ def test_only_borrowed_books_in_list(self):
# Confirm all books belong to testuser1 and are on loan
for book_item in response.context['bookinstance_list']:
self.assertEqual(response.context['user'], book_item.borrower)
self.assertEqual(book_item.status, 'o')
self.assertEqual(book_item.status, BookInstance.LoanStatus.ON_LOAN)

def test_pages_paginated_to_ten(self):

# Change all books to be on loan.
# This should make 15 test user ones.
for copy in BookInstance.objects.all():
copy.status = 'o'
copy.status = BookInstance.LoanStatus.ON_LOAN
copy.save()

login = self.client.login(
Expand All @@ -176,7 +176,7 @@ def test_pages_ordered_by_due_date(self):

# Change all books to be on loan
for copy in BookInstance.objects.all():
copy.status = 'o'
copy.status = BookInstance.LoanStatus.ON_LOAN
copy.save()

login = self.client.login(
Expand Down Expand Up @@ -233,12 +233,12 @@ def setUp(self):
return_date = datetime.date.today() + datetime.timedelta(days=5)
self.test_bookinstance1 = BookInstance.objects.create(book=test_book,
imprint='Unlikely Imprint, 2016', due_back=return_date,
borrower=test_user1, status='o')
borrower=test_user1, status=BookInstance.LoanStatus.ON_LOAN)

# Create a BookInstance object for test_user2
return_date = datetime.date.today() + datetime.timedelta(days=5)
self.test_bookinstance2 = BookInstance.objects.create(book=test_book, imprint='Unlikely Imprint, 2016',
due_back=return_date, borrower=test_user2, status='o')
due_back=return_date, borrower=test_user2, status=BookInstance.LoanStatus.ON_LOAN)

def test_redirect_if_not_logged_in(self):
response = self.client.get(
Expand Down
6 changes: 3 additions & 3 deletions catalog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def index(request):
num_instances = BookInstance.objects.all().count()
# Available copies of books
num_instances_available = BookInstance.objects.filter(
status__exact='a').count()
status__exact=BookInstance.LoanStatus.AVAILABLE).count()
num_authors = Author.objects.count() # The 'all()' is implied by default.

# Number of visits to this view, as counted in the session variable.
Expand Down Expand Up @@ -88,7 +88,7 @@ class LoanedBooksByUserListView(LoginRequiredMixin, generic.ListView):
def get_queryset(self):
return (
BookInstance.objects.filter(borrower=self.request.user)
.filter(status__exact='o')
.filter(status__exact=BookInstance.LoanStatus.ON_LOAN)
.order_by('due_back')
)

Expand All @@ -104,7 +104,7 @@ class LoanedBooksAllListView(PermissionRequiredMixin, generic.ListView):
paginate_by = 10

def get_queryset(self):
return BookInstance.objects.filter(status__exact='o').order_by('due_back')
return BookInstance.objects.filter(status__exact=BookInstance.LoanStatus.ON_LOAN).order_by('due_back')

from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
Expand Down
2 changes: 1 addition & 1 deletion locallibrary/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
https://docs.djangoproject.com/en/6.1/howto/deployment/asgi/
"""

import os
Expand Down
27 changes: 13 additions & 14 deletions locallibrary/settings.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""
Django settings for locallibrary project.

Generated by 'django-admin startproject' using Django 5.0.2.
Generated by 'django-admin startproject' using Django 6.1.1.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
https://docs.djangoproject.com/en/6.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
https://docs.djangoproject.com/en/6.1/ref/settings/
"""

from pathlib import Path
Expand All @@ -27,7 +27,7 @@
load_dotenv(env_file)

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
# SECRET_KEY = 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87'
Expand Down Expand Up @@ -92,7 +92,7 @@


# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases

DATABASES = {
'default': {
Expand All @@ -103,7 +103,7 @@


# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
Expand All @@ -122,7 +122,7 @@


# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
# https://docs.djangoproject.com/en/6.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

Expand All @@ -137,7 +137,11 @@
LOGIN_REDIRECT_URL = '/'

# Add to test email:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.console.EmailBackend',
},
}

# Update database configuration from $DATABASE_URL environment variable (if defined)
import dj_database_url
Expand All @@ -149,7 +153,7 @@


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
# https://docs.djangoproject.com/en/6.1/howto/static-files/
# The absolute path to the directory where collectstatic will collect static files for deployment.
STATIC_ROOT = BASE_DIR / 'staticfiles'
# The URL to use when referring to static files (where they will be served from)
Expand All @@ -164,8 +168,3 @@
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}

# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
2 changes: 1 addition & 1 deletion locallibrary/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
URL configuration for locallibrary project.

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
https://docs.djangoproject.com/en/6.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
Expand Down
2 changes: 1 addition & 1 deletion locallibrary/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
https://docs.djangoproject.com/en/6.1/howto/deployment/wsgi/
"""

import os
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Django==5.1.15
Django==6.1.1
dj-database-url==2.1.0
gunicorn==22.0.0
psycopg2-binary==2.9.9
psycopg2-binary==2.9.12
wheel==0.46.2
whitenoise==6.6.0
python-dotenv==1.0.1