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 |
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 |