diff --git a/README.md b/README.md
index ad7474a9..abc096bc 100644
--- a/README.md
+++ b/README.md
@@ -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.

-
## 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
@@ -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.
diff --git a/catalog/admin.py b/catalog/admin.py
index b7d14731..5e0cc70c 100644
--- a/catalog/admin.py
+++ b/catalog/admin.py
@@ -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)
@@ -21,7 +16,6 @@ class BooksInline(admin.TabularInline):
model = Book
-@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
"""Administration object for Author models.
Defines:
@@ -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:
@@ -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.
diff --git a/catalog/apps.py b/catalog/apps.py
index a5993c68..ac42aa1b 100644
--- a/catalog/apps.py
+++ b/catalog/apps.py
@@ -2,5 +2,4 @@
class CatalogConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
name = 'catalog'
diff --git a/catalog/migrations/0028_alter_bookinstance_status.py b/catalog/migrations/0028_alter_bookinstance_status.py
new file mode 100644
index 00000000..4d939617
--- /dev/null
+++ b/catalog/migrations/0028_alter_bookinstance_status.py
@@ -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),
+ ),
+ ]
diff --git a/catalog/models.py b/catalog/models.py
index c55bb95c..1eda562f 100644
--- a/catalog/models.py
+++ b/catalog/models.py
@@ -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:
diff --git a/catalog/templates/catalog/book_detail.html b/catalog/templates/catalog/book_detail.html
index e1f3ed7e..8446f333 100644
--- a/catalog/templates/catalog/book_detail.html
+++ b/catalog/templates/catalog/book_detail.html
@@ -15,8 +15,8 @@
Copies
{% for copy in book.bookinstance_set.all %}
- {{ copy.get_status_display }}
- {% if copy.status != 'a' %}Due to be returned: {{copy.due_back}}
{% endif %}
+ {{ copy.get_status_display }}
+ {% if copy.status != copy.LoanStatus.AVAILABLE %}Due to be returned: {{copy.due_back}}
{% endif %}
Imprint: {{copy.imprint}}
Id: {{copy.id}}
{% empty %}
diff --git a/catalog/templates/catalog/bookinstance_detail.html b/catalog/templates/catalog/bookinstance_detail.html
index 6e003db2..8859fa24 100644
--- a/catalog/templates/catalog/bookinstance_detail.html
+++ b/catalog/templates/catalog/bookinstance_detail.html
@@ -7,7 +7,7 @@ BookInstance: {{ bookinstance.book.title }}
Author: {{ bookinstance.book.author }}
Imprint: {{ bookinstance.imprint }}
-Status: {{ bookinstance.get_status_display }} {% if bookinstance.status != 'a' %} (Due: {{bookinstance.due_back}}){% endif %}
+Status: {{ bookinstance.get_status_display }} {% if bookinstance.status != bookinstance.LoanStatus.AVAILABLE %} (Due: {{bookinstance.due_back}}){% endif %}
diff --git a/catalog/templates/catalog/bookinstance_list.html b/catalog/templates/catalog/bookinstance_list.html
index dcfe0aa6..b11fe93c 100644
--- a/catalog/templates/catalog/bookinstance_list.html
+++ b/catalog/templates/catalog/bookinstance_list.html
@@ -7,8 +7,8 @@ Book Copies in Library
{% for bookinst in bookinstance_list %}
-
{{bookinst.book.title}} ({{ 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 %}- Renew {% endif %}
{% endif %}
diff --git a/catalog/tests/test_views.py b/catalog/tests/test_views.py
index bb694ef8..7b797adb 100644
--- a/catalog/tests/test_views.py
+++ b/catalog/tests/test_views.py
@@ -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)
@@ -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
@@ -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(
@@ -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(
@@ -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(
diff --git a/catalog/views.py b/catalog/views.py
index fe1fab38..b68e00bf 100644
--- a/catalog/views.py
+++ b/catalog/views.py
@@ -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.
@@ -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')
)
@@ -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
diff --git a/locallibrary/asgi.py b/locallibrary/asgi.py
index 7b41a25a..26358a54 100644
--- a/locallibrary/asgi.py
+++ b/locallibrary/asgi.py
@@ -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
diff --git a/locallibrary/settings.py b/locallibrary/settings.py
index d97b90aa..e1dfad33 100644
--- a/locallibrary/settings.py
+++ b/locallibrary/settings.py
@@ -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
@@ -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'
@@ -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': {
@@ -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 = [
{
@@ -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'
@@ -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
@@ -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)
@@ -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'
diff --git a/locallibrary/urls.py b/locallibrary/urls.py
index 1dd9547f..5ae86ece 100644
--- a/locallibrary/urls.py
+++ b/locallibrary/urls.py
@@ -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
diff --git a/locallibrary/wsgi.py b/locallibrary/wsgi.py
index d5e23e87..0f233098 100644
--- a/locallibrary/wsgi.py
+++ b/locallibrary/wsgi.py
@@ -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
diff --git a/requirements.txt b/requirements.txt
index 51bf6bba..5abb47ee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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