Welcome to LocalLibrary, a very basic Django website developed as a tutorial example on the Mozilla Developer Network.
+
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.
+
+
+
UML Models
+
An UML diagram of the site's Django model structure is shown below.
+
+
+{% load static %}
+
+
+
+
+
Dynamic content
+
+
The library has the following record counts:
+
+
Books: {{ num_books }}
+
Copies: {{ num_instances }}
+
Copies available: {{ num_instances_available }}
+
Authors: {{ num_authors }}
+
+
+
+You have visited this page {{ num_visits }} times.
+
+{% endblock %}
\ No newline at end of file
diff --git a/catalog/tests/__init__.py b/catalog/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/catalog/tests/test_forms.py b/catalog/tests/test_forms.py
new file mode 100644
index 0000000..52cf8ac
--- /dev/null
+++ b/catalog/tests/test_forms.py
@@ -0,0 +1,61 @@
+from django.test import TestCase
+
+# Create your tests here.
+
+import datetime
+from django.utils import timezone
+from catalog.forms import RenewBookForm
+
+class RenewBookFormTest(TestCase):
+
+ def test_renew_form_date_in_past(self):
+ """
+ Test form is invalid if renewal_date is before today
+ """
+ date = datetime.date.today() - datetime.timedelta(days=1)
+ form_data = {'renewal_date': date}
+ form = RenewBookForm(data=form_data)
+ self.assertFalse(form.is_valid())
+
+ def test_renew_form_date_too_far_in_future(self):
+ """
+ Test form is invalid if renewal_date more than 4 weeks from today
+ """
+ date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
+ form_data = {'renewal_date': date}
+ form = RenewBookForm(data=form_data)
+ self.assertFalse(form.is_valid())
+
+ def test_renew_form_date_today(self):
+ """
+ Test form is valid if renewal_date is today
+ """
+ date = datetime.date.today()
+ form_data = {'renewal_date': date}
+ form = RenewBookForm(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_renew_form_date_max(self):
+ """
+ Test form is valid if renewal_date is within 4 weeks
+ """
+ date = timezone.now() + datetime.timedelta(weeks=4)
+ form_data = {'renewal_date': date}
+ form = RenewBookForm(data=form_data)
+ self.assertTrue(form.is_valid())
+
+
+ def test_renew_form_date_field_label(self):
+ """
+ Test renewal_date label is "renewal date"
+ """
+ form = RenewBookForm()
+ self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date')
+
+
+ def test_renew_form_date_field_help_text(self):
+ """
+ Test renewal_date help_text is as expected.
+ """
+ form = RenewBookForm()
+ self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).')
\ No newline at end of file
diff --git a/catalog/tests/test_models.py b/catalog/tests/test_models.py
new file mode 100644
index 0000000..3e5fed9
--- /dev/null
+++ b/catalog/tests/test_models.py
@@ -0,0 +1,56 @@
+from django.test import TestCase
+
+# Create your tests here.
+
+import django.db
+from ..models import Author
+
+class AuthorModelTest(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ #Set up non-modified objects used by all test methods
+ Author.objects.create(first_name='Big', last_name='Bob')
+
+ def test_first_name_label(self):
+ author=Author.objects.get(id=1)
+ field_label = author._meta.get_field('first_name').verbose_name
+ self.assertEquals(field_label,'first name')
+
+ def test_last_name_label(self):
+ author=Author.objects.get(id=1)
+ field_label = author._meta.get_field('last_name').verbose_name
+ self.assertEquals(field_label,'last name')
+
+ def test_date_of_birth_label(self):
+ author=Author.objects.get(id=1)
+ field_label = author._meta.get_field('date_of_birth').verbose_name
+ self.assertEquals(field_label,'date of birth')
+
+ def test_date_of_death_label(self):
+ author=Author.objects.get(id=1)
+ field_label = author._meta.get_field('date_of_death').verbose_name
+ self.assertEquals(field_label,'died')
+
+ def test_first_name_max_length(self):
+ author=Author.objects.get(id=1)
+ max_length = author._meta.get_field('first_name').max_length
+ self.assertEquals(max_length,100)
+
+ def test_last_name_max_length(self):
+ author=Author.objects.get(id=1)
+ max_length = author._meta.get_field('last_name').max_length
+ self.assertEquals(max_length,100)
+
+ def test_object_name_is_last_name_comma_first_name(self):
+ author=Author.objects.get(id=1)
+ #expected_object_name = '%s, %s' % (author.last_name, author.first_name)
+ expected_object_name = '{0}, {1}'.format(author.last_name,author.first_name)
+
+ self.assertEquals(expected_object_name,str(author))
+
+ def test_get_absolute_url(self):
+ author=Author.objects.get(id=1)
+ #This will also fail if the urlconf is not defined.
+ self.assertEquals(author.get_absolute_url(),'/catalog/author/1')
+
diff --git a/catalog/tests/test_views.py b/catalog/tests/test_views.py
new file mode 100644
index 0000000..7a5e40e
--- /dev/null
+++ b/catalog/tests/test_views.py
@@ -0,0 +1,343 @@
+from django.test import TestCase
+
+# Create your tests here.
+import django.db
+from ..models import Author
+from django.urls import reverse
+
+class AuthorListViewTest(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ #Create authors for pagination tests
+ number_of_authors = 13
+ for author_num in range(number_of_authors):
+ #Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,)
+ Author.objects.create(first_name='Christian {0}'.format(author_num), last_name = 'Surname {0}'.format(author_num) )
+
+ def test_view_url_exists_at_desired_location(self):
+ resp = self.client.get('/catalog/authors/')
+ self.assertEqual(resp.status_code, 200)
+
+ def test_view_url_accessible_by_name(self):
+ resp = self.client.get(reverse('authors'))
+ self.assertEqual(resp.status_code, 200)
+
+ def test_view_uses_correct_template(self):
+ resp = self.client.get(reverse('authors'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertTemplateUsed(resp, 'catalog/author_list.html')
+
+ def test_pagination_is_ten(self):
+ resp = self.client.get(reverse('authors'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue('is_paginated' in resp.context)
+ self.assertTrue(resp.context['is_paginated'] == True)
+ self.assertTrue( len(resp.context['author_list']) == 10)
+
+ def test_lists_all_authors(self):
+ #Get second page and confirm it has (exactly) the remaining 3 items
+ resp = self.client.get(reverse('authors')+'?page=2')
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue('is_paginated' in resp.context)
+ self.assertTrue(resp.context['is_paginated'] == True)
+ self.assertTrue( len(resp.context['author_list']) == 3)
+
+
+import datetime
+from django.utils import timezone
+
+from catalog.models import BookInstance, Book, Genre, Language
+from django.contrib.auth.models import User #Required to assign User as a borrower
+
+class LoanedBookInstancesByUserListViewTest(TestCase):
+
+ def setUp(self):
+ #Create two users
+ test_user1 = User.objects.create_user(username='testuser1', password='12345')
+ test_user1.save()
+ test_user2 = User.objects.create_user(username='testuser2', password='12345')
+ test_user2.save()
+
+ #Create a book
+ test_author = Author.objects.create(first_name='John', last_name='Smith')
+ test_genre = Genre.objects.create(name='Fantasy')
+ test_language = Language.objects.create(name='English')
+ test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
+ # Create genre as a post-step
+ genre_objects_for_book = Genre.objects.all()
+ test_book.genre.set(genre_objects_for_book)
+ test_book.save()
+
+ #Create 30 BookInstance objects
+ number_of_book_copies = 30
+ for book_copy in range(number_of_book_copies):
+ return_date= timezone.now() + datetime.timedelta(days=book_copy%5)
+ if book_copy % 2:
+ the_borrower=test_user1
+ else:
+ the_borrower=test_user2
+ status='m'
+ BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status)
+
+ def test_redirect_if_not_logged_in(self):
+ resp = self.client.get(reverse('my-borrowed'))
+ self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/')
+
+ def test_logged_in_uses_correct_template(self):
+ login = self.client.login(username='testuser1', password='12345')
+ resp = self.client.get(reverse('my-borrowed'))
+
+ #Check our user is logged in
+ self.assertEqual(str(resp.context['user']), 'testuser1')
+ #Check that we got a response "success"
+ self.assertEqual(resp.status_code, 200)
+
+ #Check we used correct template
+ self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html')
+
+ def test_only_borrowed_books_in_list(self):
+ login = self.client.login(username='testuser1', password='12345')
+ resp = self.client.get(reverse('my-borrowed'))
+
+ #Check our user is logged in
+ self.assertEqual(str(resp.context['user']), 'testuser1')
+ #Check that we got a response "success"
+ self.assertEqual(resp.status_code, 200)
+
+ #Check that initially we don't have any books in list (none on loan)
+ self.assertTrue('bookinstance_list' in resp.context)
+ self.assertEqual( len(resp.context['bookinstance_list']),0)
+
+ #Now change all books to be on loan
+ get_ten_books = BookInstance.objects.all()[:10]
+
+ for copy in get_ten_books:
+ copy.status='o'
+ copy.save()
+
+ #Check that now we have borrowed books in the list
+ resp = self.client.get(reverse('my-borrowed'))
+ #Check our user is logged in
+ self.assertEqual(str(resp.context['user']), 'testuser1')
+ #Check that we got a response "success"
+ self.assertEqual(resp.status_code, 200)
+
+ self.assertTrue('bookinstance_list' in resp.context)
+
+ #Confirm all books belong to testuser1 and are on loan
+ for bookitem in resp.context['bookinstance_list']:
+ self.assertEqual(resp.context['user'], bookitem.borrower)
+ self.assertEqual('o', bookitem.status)
+
+ def test_pages_paginated_to_ten(self):
+
+ #Change all books to be on loan.
+ #This should make 15 test user ones.
+ for copy in BookInstance.objects.all():
+ copy.status='o'
+ copy.save()
+
+ login = self.client.login(username='testuser1', password='12345')
+ resp = self.client.get(reverse('my-borrowed'))
+
+ #Check our user is logged in
+ self.assertEqual(str(resp.context['user']), 'testuser1')
+ #Check that we got a response "success"
+ self.assertEqual(resp.status_code, 200)
+
+ #Confirm that only 10 items are displayed due to pagination (if pagination not enabled, there would be 15 returned)
+ self.assertEqual( len(resp.context['bookinstance_list']),10)
+
+ def test_pages_ordered_by_due_date(self):
+
+ #Change all books to be on loan
+ for copy in BookInstance.objects.all():
+ copy.status='o'
+ copy.save()
+
+ login = self.client.login(username='testuser1', password='12345')
+ resp = self.client.get(reverse('my-borrowed'))
+
+ #Check our user is logged in
+ self.assertEqual(str(resp.context['user']), 'testuser1')
+ #Check that we got a response "success"
+ self.assertEqual(resp.status_code, 200)
+
+ #Confirm that of the items, only 10 are displayed due to pagination.
+ self.assertEqual( len(resp.context['bookinstance_list']),10)
+
+ last_date=0
+ for copy in resp.context['bookinstance_list']:
+ if last_date==0:
+ last_date=copy.due_back
+ else:
+ self.assertTrue(last_date <= copy.due_back)
+
+
+
+from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned.
+
+class RenewBookInstancesViewTest(TestCase):
+
+ def setUp(self):
+ #Create a user
+ test_user1 = User.objects.create_user(username='testuser1', password='12345')
+ test_user1.save()
+
+ test_user2 = User.objects.create_user(username='testuser2', password='12345')
+ test_user2.save()
+ permission = Permission.objects.get(name='Set book as returned')
+ test_user2.user_permissions.add(permission)
+ test_user2.save()
+
+ #Create a book
+ test_author = Author.objects.create(first_name='John', last_name='Smith')
+ test_genre = Genre.objects.create(name='Fantasy')
+ test_language = Language.objects.create(name='English')
+ test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
+ # Create genre as a post-step
+ genre_objects_for_book = Genre.objects.all()
+ test_book.genre.set(genre_objects_for_book)
+ test_book.save()
+
+ #Create a BookInstance object for test_user1
+ return_date= datetime.date.today() + datetime.timedelta(days=5)
+ self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o')
+
+ #Create a BookInstance object for test_user2
+ return_date= datetime.date.today() + datetime.timedelta(days=5)
+ self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o')
+
+ def test_redirect_if_not_logged_in(self):
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
+ #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
+ self.assertEqual( resp.status_code,302)
+ self.assertTrue( resp.url.startswith('/accounts/login/') )
+
+ def test_redirect_if_logged_in_but_not_correct_permission(self):
+ login = self.client.login(username='testuser1', password='12345')
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
+
+ #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
+ self.assertEqual( resp.status_code,302)
+ self.assertTrue( resp.url.startswith('/accounts/login/') )
+
+ def test_logged_in_with_permission_borrowed_book(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance2.pk,}) )
+
+ #Check that it lets us login - this is our book and we have the right permissions.
+ self.assertEqual( resp.status_code,200)
+
+ def test_logged_in_with_permission_another_users_borrowed_book(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
+
+ #Check that it lets us login. We're a librarian, so we can view any users book
+ self.assertEqual( resp.status_code,200)
+
+ def test_uses_correct_template(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
+ self.assertEqual( resp.status_code,200)
+
+ #Check we used correct template
+ self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html')
+
+ def test_form_renewal_date_initially_has_date_three_weeks_in_future(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
+ self.assertEqual( resp.status_code,200)
+
+ date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3)
+ self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future )
+
+ def test_form_invalid_renewal_date_past(self):
+ login = self.client.login(username='testuser2', password='12345')
+
+ date_in_past = datetime.date.today() - datetime.timedelta(weeks=1)
+ resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':date_in_past} )
+ self.assertEqual( resp.status_code,200)
+ self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past')
+
+ def test_form_invalid_renewal_date_future(self):
+ login = self.client.login(username='testuser2', password='12345')
+
+ invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5)
+ resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':invalid_date_in_future} )
+ self.assertEqual( resp.status_code,200)
+ self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')
+
+ def test_redirects_to_all_borrowed_book_list_on_success(self):
+ login = self.client.login(username='testuser2', password='12345')
+ valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2)
+ resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future} )
+ self.assertRedirects(resp, reverse('all-borrowed') )
+
+ def test_HTTP404_for_invalid_book_if_logged_in(self):
+ import uuid
+ test_uid = uuid.uuid4() #unlikely UID to match our bookinstance!
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid,}) )
+ self.assertEqual( resp.status_code,404)
+
+
+
+
+class AuthorCreateViewTest(TestCase):
+ """
+ Test case for the AuthorCreate view (Created as Challenge!)
+ """
+
+ def setUp(self):
+ #Create a user
+ test_user1 = User.objects.create_user(username='testuser1', password='12345')
+ test_user1.save()
+
+ test_user2 = User.objects.create_user(username='testuser2', password='12345')
+ test_user2.save()
+ permission = Permission.objects.get(name='Set book as returned')
+ test_user2.user_permissions.add(permission)
+ test_user2.save()
+
+ #Create a book
+ test_author = Author.objects.create(first_name='John', last_name='Smith')
+
+
+ def test_redirect_if_not_logged_in(self):
+ resp = self.client.get(reverse('author_create') )
+ self.assertRedirects(resp, '/accounts/login/?next=/catalog/author/create/' )
+
+ def test_redirect_if_logged_in_but_not_correct_permission(self):
+ login = self.client.login(username='testuser1', password='12345')
+ resp = self.client.get(reverse('author_create') )
+ self.assertRedirects(resp, '/accounts/login/?next=/catalog/author/create/' )
+
+ def test_logged_in_with_permission(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('author_create') )
+ self.assertEqual( resp.status_code,200)
+
+ def test_uses_correct_template(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('author_create') )
+ self.assertEqual( resp.status_code,200)
+ self.assertTemplateUsed(resp, 'catalog/author_form.html')
+
+ def test_form_date_of_death_initially_set_to_expected_date(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.get(reverse('author_create') )
+ self.assertEqual( resp.status_code,200)
+
+ expected_initial_date = datetime.date(2018, 1, 5)
+ response_date=resp.context['form'].initial['date_of_death']
+ response_date=datetime.datetime.strptime(response_date, "%d/%m/%Y").date()
+ self.assertEqual(response_date, expected_initial_date )
+
+ def test_redirects_to_detail_view_on_success(self):
+ login = self.client.login(username='testuser2', password='12345')
+ resp = self.client.post(reverse('author_create'),{'first_name':'Christian Name','last_name':'Surname',} )
+ #Manually check redirect because we don't know what author was created
+ self.assertEqual( resp.status_code,302)
+ self.assertTrue( resp.url.startswith('/catalog/author/') )
diff --git a/catalog/urls.py b/catalog/urls.py
new file mode 100644
index 0000000..0668c19
--- /dev/null
+++ b/catalog/urls.py
@@ -0,0 +1,39 @@
+from django.urls import path
+
+from . import views
+
+
+urlpatterns = [
+ path('', views.index, name='index'),
+ path('books/', views.BookListView.as_view(), name='books'),
+ path('book/', views.BookDetailView.as_view(), name='book-detail'),
+ path('authors/', views.AuthorListView.as_view(), name='authors'),
+ path('author/', views.AuthorDetailView.as_view(), name='author-detail'),
+]
+
+
+urlpatterns += [
+ path('mybooks/', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'),
+ path(r'borrowed/', views.LoanedBooksAllListView.as_view(), name='all-borrowed'), #Added for challenge
+]
+
+
+# Add URLConf for librarian to renew a book.
+urlpatterns += [
+ path('book//renew/', views.renew_book_librarian, name='renew-book-librarian'),
+]
+
+
+# Add URLConf to create, update, and delete authors
+urlpatterns += [
+ path('author/create/', views.AuthorCreate.as_view(), name='author_create'),
+ path('author//update/', views.AuthorUpdate.as_view(), name='author_update'),
+ path('author//delete/', views.AuthorDelete.as_view(), name='author_delete'),
+]
+
+# Add URLConf to create, update, and delete books
+urlpatterns += [
+ path('book/create/', views.BookCreate.as_view(), name='book_create'),
+ path('book//update/', views.BookUpdate.as_view(), name='book_update'),
+ path('book//delete/', views.BookDelete.as_view(), name='book_delete'),
+]
diff --git a/catalog/views.py b/catalog/views.py
new file mode 100644
index 0000000..13bc289
--- /dev/null
+++ b/catalog/views.py
@@ -0,0 +1,166 @@
+from django.shortcuts import render
+
+# Create your views here.
+
+from .models import Book, Author, BookInstance, Genre
+
+def index(request):
+ """
+ View function for home page of site.
+ """
+ # Generate counts of some of the main objects
+ num_books=Book.objects.all().count()
+ num_instances=BookInstance.objects.all().count()
+ # Available copies of books
+ num_instances_available=BookInstance.objects.filter(status__exact='a').count()
+ num_authors=Author.objects.count() # The 'all()' is implied by default.
+
+ # Number of visits to this view, as counted in the session variable.
+ num_visits=request.session.get('num_visits', 0)
+ request.session['num_visits'] = num_visits+1
+
+ # Render the HTML template index.html with the data in the context variable.
+ return render(
+ request,
+ 'index.html',
+ context={'num_books':num_books,'num_instances':num_instances,'num_instances_available':num_instances_available,'num_authors':num_authors,
+ 'num_visits':num_visits},
+ )
+
+from django.views import generic
+
+
+class BookListView(generic.ListView):
+ """
+ Generic class-based view for a list of books.
+ """
+ model = Book
+ paginate_by = 10
+
+class BookDetailView(generic.DetailView):
+ """
+ Generic class-based detail view for a book.
+ """
+ model = Book
+
+class AuthorListView(generic.ListView):
+ """
+ Generic class-based list view for a list of authors.
+ """
+ model = Author
+ paginate_by = 10
+
+
+class AuthorDetailView(generic.DetailView):
+ """
+ Generic class-based detail view for an author.
+ """
+ model = Author
+
+
+from django.contrib.auth.mixins import LoginRequiredMixin
+
+class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
+ """
+ Generic class-based view listing books on loan to current user.
+ """
+ model = BookInstance
+ template_name ='catalog/bookinstance_list_borrowed_user.html'
+ paginate_by = 10
+
+ def get_queryset(self):
+ return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')
+
+
+# Added as part of challenge!
+from django.contrib.auth.mixins import PermissionRequiredMixin
+
+class LoanedBooksAllListView(PermissionRequiredMixin,generic.ListView):
+ """
+ Generic class-based view listing all books on loan. Only visible to users with can_mark_returned permission.
+ """
+ model = BookInstance
+ permission_required = 'catalog.can_mark_returned'
+ template_name ='catalog/bookinstance_list_borrowed_all.html'
+ paginate_by = 10
+
+ def get_queryset(self):
+ return BookInstance.objects.filter(status__exact='o').order_by('due_back')
+
+
+from django.shortcuts import get_object_or_404
+from django.http import HttpResponseRedirect
+from django.urls import reverse
+import datetime
+from django.contrib.auth.decorators import permission_required
+
+from .forms import RenewBookForm
+
+@permission_required('catalog.can_mark_returned')
+def renew_book_librarian(request, pk):
+ """
+ View function for renewing a specific BookInstance by librarian
+ """
+ book_inst=get_object_or_404(BookInstance, pk = pk)
+
+ # If this is a POST request then process the Form data
+ if request.method == 'POST':
+
+ # Create a form instance and populate it with data from the request (binding):
+ form = RenewBookForm(request.POST)
+
+ # Check if the form is valid:
+ if form.is_valid():
+ # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
+ book_inst.due_back = form.cleaned_data['renewal_date']
+ book_inst.save()
+
+ # redirect to a new URL:
+ return HttpResponseRedirect(reverse('all-borrowed') )
+
+ # If this is a GET (or any other method) create the default form
+ else:
+ proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
+ form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,})
+
+ return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})
+
+
+
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.urls import reverse_lazy
+from .models import Author
+
+
+class AuthorCreate(PermissionRequiredMixin, CreateView):
+ model = Author
+ fields = '__all__'
+ initial={'date_of_death':'05/01/2018',}
+ permission_required = 'catalog.can_mark_returned'
+
+class AuthorUpdate(PermissionRequiredMixin, UpdateView):
+ model = Author
+ fields = ['first_name','last_name','date_of_birth','date_of_death']
+ permission_required = 'catalog.can_mark_returned'
+
+class AuthorDelete(PermissionRequiredMixin, DeleteView):
+ model = Author
+ success_url = reverse_lazy('authors')
+ permission_required = 'catalog.can_mark_returned'
+
+
+#Classes created for the forms challenge
+class BookCreate(PermissionRequiredMixin, CreateView):
+ model = Book
+ fields = '__all__'
+ permission_required = 'catalog.can_mark_returned'
+
+class BookUpdate(PermissionRequiredMixin, UpdateView):
+ model = Book
+ fields = '__all__'
+ permission_required = 'catalog.can_mark_returned'
+
+class BookDelete(PermissionRequiredMixin, DeleteView):
+ model = Book
+ success_url = reverse_lazy('books')
+ permission_required = 'catalog.can_mark_returned'
diff --git a/locallibrary/__init__.py b/locallibrary/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/locallibrary/settings.py b/locallibrary/settings.py
new file mode 100644
index 0000000..5656c0d
--- /dev/null
+++ b/locallibrary/settings.py
@@ -0,0 +1,155 @@
+"""
+Django settings for locallibrary project.
+
+Generated by 'django-admin startproject' using Django 1.10.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.10/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+#SECRET_KEY = 'cg#p$g+j9tax!#a3cup@1$8obt2_+&k3q+pmu)5%asj6yjpkag'
+import os
+SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'cg#p$g+j9tax!#a3cup@1$8obt2_+&k3q+pmu)5%asj6yjpkag')
+
+# SECURITY WARNING: don't run with debug turned on in production!
+#DEBUG = True
+DEBUG = bool( os.environ.get('DJANGO_DEBUG', True) )
+
+#Set hosts to allow any app on Heroku and the local testing URL
+ALLOWED_HOSTS = ['.herokuapp.com','127.0.0.1']
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ #Add our new application
+ 'catalog.apps.CatalogConfig', #This object was created for us in /catalog/apps.py
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'locallibrary.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': ['./templates',],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'locallibrary.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.10/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+
+# Redirect to home URL after login (Default redirects to /accounts/profile/)
+LOGIN_REDIRECT_URL = '/'
+
+# Add to test email:
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
+
+
+# Heroku: Update database configuration from $DATABASE_URL.
+import dj_database_url
+db_from_env = dj_database_url.config(conn_max_age=500)
+DATABASES['default'].update(db_from_env)
+
+# import Django and setup applications
+import django
+django.setup()
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+# The absolute path to the directory where collectstatic will collect static files for deployment.
+STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
+# The URL to use when referring to static files (where they will be served from)
+STATIC_URL = '/static/'
+
+
+# Simplified static file serving.
+# https://warehouse.python.org/project/whitenoise/
+# STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage'
+
+TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
+TEST_OUTPUT_VERBOSE = 2
+TEST_OUTPUT_DIR = 'test-results'
\ No newline at end of file
diff --git a/locallibrary/urls.py b/locallibrary/urls.py
new file mode 100644
index 0000000..d46dc3f
--- /dev/null
+++ b/locallibrary/urls.py
@@ -0,0 +1,60 @@
+"""locallibrary URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/2.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+]
+
+
+from django.urls import path
+from django.contrib import admin
+
+# Use include() to add URLS from the catalog application and authentication system
+from django.urls import include
+
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+]
+
+
+urlpatterns += [
+ path('catalog/', include('catalog.urls')),
+]
+
+
+# Use static() to add url mapping to serve static files during development (only)
+from django.conf import settings
+from django.conf.urls.static import static
+
+
+urlpatterns+= static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+
+
+#Add URL maps to redirect the base URL to our application
+from django.views.generic import RedirectView
+urlpatterns += [
+ path('', RedirectView.as_view(url='/catalog/', permanent=True)),
+]
+
+
+
+#Add Django site authentication urls (for login, logout, password management)
+urlpatterns += [
+ path('accounts/', include('django.contrib.auth.urls')),
+]
diff --git a/locallibrary/wsgi.py b/locallibrary/wsgi.py
new file mode 100644
index 0000000..5d39ba3
--- /dev/null
+++ b/locallibrary/wsgi.py
@@ -0,0 +1,24 @@
+"""
+WSGI config for locallibrary project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "locallibrary.settings")
+
+application = get_wsgi_application()
+
+
+#Add static serving using whitenoise
+from django.core.wsgi import get_wsgi_application
+from whitenoise.django import DjangoWhiteNoise
+
+application = get_wsgi_application()
+application = DjangoWhiteNoise(application)
diff --git a/manage.py b/manage.py
new file mode 100644
index 0000000..d23ac38
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "locallibrary.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django
+ except ImportError:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ )
+ raise
+ execute_from_command_line(sys.argv)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..7fa90c4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,18 @@
+asgiref==3.2.10
+attrs==20.1.0
+dj-database-url==0.5.0
+Django==3.1.1
+iniconfig==1.0.1
+more-itertools==8.5.0
+packaging==20.4
+pluggy==0.13.1
+py==1.9.0
+pybuilder==0.12.8
+pyparsing==2.4.7
+pytest==6.0.1
+pytz==2020.1
+six==1.15.0
+sqlparse==0.3.1
+toml==0.10.1
+unittest-xml-reporting==3.0.4
+xmlrunner==1.7.7
diff --git a/runtime.txt b/runtime.txt
new file mode 100644
index 0000000..09dac98
--- /dev/null
+++ b/runtime.txt
@@ -0,0 +1 @@
+python-3.6.4
\ No newline at end of file
diff --git a/templates/registration/logged_out.html b/templates/registration/logged_out.html
new file mode 100644
index 0000000..adfeb3e
--- /dev/null
+++ b/templates/registration/logged_out.html
@@ -0,0 +1,7 @@
+{% extends "base_generic.html" %}
+
+{% block content %}
+
Logged out!
+
+Click here to login again.
+{% endblock %}
\ No newline at end of file
diff --git a/templates/registration/login.html b/templates/registration/login.html
new file mode 100644
index 0000000..d09cd20
--- /dev/null
+++ b/templates/registration/login.html
@@ -0,0 +1,38 @@
+{% extends "base_generic.html" %}
+
+{% block content %}
+
+{% if form.errors %}
+
Your username and password didn't match. Please try again.
+{% endif %}
+
+{% if next %}
+ {% if user.is_authenticated %}
+
Your account doesn't have access to this page. To proceed,
+ please login with an account that has access.
+ {% else %}
+
Please login to see this page.
+ {% endif %}
+{% endif %}
+
+
+
+{# Assumes you setup the password_reset view in your URLconf #}
+