15 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/migrations/0004_auto_20190706_1425.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.1 on 2019-07-06 17:25
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('catalog', '0003_auto_20190706_1353'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='bookinstance',
15 | options={'ordering': ['due_back'], 'permissions': (('can_mark_returned', 'Set book as returned'),)},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/locallibrary/locallibrary/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path, include
3 | from django.views.generic import RedirectView
4 | from django.conf import settings
5 | from django.conf.urls.static import static
6 |
7 | urlpatterns = [
8 | path('admin/', admin.site.urls),
9 | path('catalog/', include('catalog.urls')),
10 | path('', RedirectView.as_view(url='/catalog/', permanent=True)),
11 | path('accounts/', include('django.contrib.auth.urls')),
12 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
13 |
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/book_renew_librarian.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
Renovar: {{ book_instance.book.title }}
5 |
Receptor: {{ book_instance.borrower }}
6 |
Data de entrega: {{ book_instance.due_back }}
7 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/author_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
Autores
6 |
7 | {% if author_list %}
8 |
15 | {% else %}
16 |
There are no books in the library.
17 | {% endif %}
18 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/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 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'locallibrary.settings')
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError as exc:
12 | raise ImportError(
13 | "Couldn't import Django. Are you sure it's installed and "
14 | "available on your PYTHONPATH environment variable? Did you "
15 | "forget to activate a virtual environment?"
16 | ) from exc
17 | execute_from_command_line(sys.argv)
18 |
19 |
20 | if __name__ == '__main__':
21 | main()
22 |
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/search.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
Livros
6 |
9 |
10 | {% if books %}
11 |
12 | {% for book in books %}
13 | -
14 | {{ book.title }} ({{book.author}})
15 |
{{ book.summary }}
16 |
17 | {% endfor %}
18 |
19 | {% else %}
20 |
Livro não encontrado.
21 | {% endif %}
22 |
23 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/book_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
Livros
6 |
9 |
10 | {% if book_list %}
11 |
12 | {% for book in book_list %}
13 | -
14 | {{ book.title }} ({{book.author}})
15 |
16 | {% endfor %}
17 |
18 | {% else %}
19 |
There are no books in the library.
20 | {% endif %}
21 |
22 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/author_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
{{ author.first_name }} {{ author.last_name }}
7 | {% if perms.catalog.can_mark_returned %}
8 |
Atualizar
9 |
Remover
10 | {% endif %}
11 |
12 |
13 |
19 |
20 |
21 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/bookinstance_list_borrowed_user.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
Livros Emprestados
6 |
7 | {% if bookinstance_list %}
8 |
9 | {% for bookinst in bookinstance_list %}
10 | -
11 | {{bookinst.book.title}} ({{ bookinst.due_back }})
12 | {% if perms.catalog.can_mark_returned %}- Renew {% endif %}
13 |
14 | {% endfor %}
15 |
16 | {% else %}
17 |
There are no books borrowed.
18 | {% endif %}
19 |
20 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/list_borrowed.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
Livros Emprestados
6 |
7 | {% if bookinstance_list %}
8 |
9 | {% for bookinst in bookinstance_list %}
10 | -
11 | {{bookinst.book.title}} ({{ bookinst.due_back }}) - {{ bookinst.borrower }}
12 | {% if perms.catalog.can_mark_returned %}- Renew {% endif %}
13 |
14 | {% endfor %}
15 |
16 | {% else %}
17 |
There are no books borrowed.
18 | {% endif %}
19 |
20 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/migrations/0003_auto_20190706_1353.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.1 on 2019-07-06 16:53
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('catalog', '0002_auto_20190705_1527'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterModelOptions(
17 | name='author',
18 | options={'ordering': ['first_name']},
19 | ),
20 | migrations.AddField(
21 | model_name='bookinstance',
22 | name='borrower',
23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/locallibrary/catalog/forms.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from django import forms
3 | from django.core.exceptions import ValidationError
4 | from django.utils.translation import ugettext_lazy as _
5 |
6 | class RenewBookForm(forms.Form):
7 | renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")
8 |
9 | def clean_renewal_date(self):
10 | data = self.cleaned_data['renewal_date']
11 |
12 | # Check if a date is not in the past.
13 | if data < datetime.date.today():
14 | raise ValidationError(_('Invalid date - renewal in past'))
15 |
16 | # Check if a date is in the allowed range (+4 weeks from today).
17 | if data > datetime.date.today() + datetime.timedelta(weeks=4):
18 | raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))
19 |
20 | # Remember to always return the cleaned data.
21 | return data
--------------------------------------------------------------------------------
/locallibrary/catalog/migrations/0002_auto_20190705_1527.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.3 on 2019-07-05 18:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('catalog', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Language',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('name', models.CharField(help_text="Enter the book's natural language (e.g. English, French, Japanese etc.)", max_length=200)),
19 | ],
20 | ),
21 | migrations.AddField(
22 | model_name='book',
23 | name='language',
24 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Language'),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 | {% load static %}
3 |
4 | {% block content %}
5 |
6 |
Biblioteca Secreta
7 |
8 |

9 |
10 |
"Seja muito bem vindo à Grande Biblioteca Secreta!"
11 |
Conteúdo Dinâmico
12 |
Nossa biblioteca conta atualmente com os seguintes contadores de registros:
13 |
14 | - Livros: {{ num_books }}
15 | - Cópias: {{ num_instances }}
16 | - Cópias disponíveis: {{ num_instances_available }}
17 | - Autores: {{ num_authors }}
18 |
19 |
20 |
Você visitou a biblioteca secreta {{ num_visits }}{% if num_visits == 1 %} vez{% else %} vezes{% endif %}.
21 |
22 | {% endblock %}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django Secret Library
2 |
3 | 
4 |
5 | Full Featured Library System made with Django.
6 |
7 | ## Some features included
8 |
9 | - User Registration
10 | - Books CRUD
11 | - Authors CRUD
12 | - Search System
13 | - Loan System
14 | - Admin Interface provided by Django
15 |
16 | ## Installation
17 |
18 | ### Clone the Repository
19 |
20 | ```
21 | git clone https://github.com/the-akira/Django-Secret-Library.git
22 | ```
23 |
24 | ### Inside the Main Directory
25 |
26 | Create a Virtual Environment
27 |
28 | ```
29 | python -m venv myvenv
30 | ```
31 |
32 | Activate the Virtual Environment
33 |
34 | ```
35 | source myvenv/bin/activate
36 | ```
37 |
38 | Install Requirements
39 |
40 | ```
41 | pip install -r requirements.txt
42 | ```
43 |
44 | Navigate to `locallibrary` and Run the Application
45 |
46 | ```
47 | python manage.py runserver
48 | ```
49 |
50 | You can now open your Web Browser and navigate to `http://127.0.0.1:8000/` to see the Web Application.
51 |
52 | Inspired by the great tutorial of **[MDN Web Docs](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Tutorial_local_library_website)**.
--------------------------------------------------------------------------------
/locallibrary/catalog/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | urlpatterns = [
5 | path('', views.index, name='index'),
6 | path('books/', views.BookListView.as_view(), name='books'),
7 | path('book/
', views.BookDetailView.as_view(), name='book-detail'),
8 | path('authors/', views.AuthorListView.as_view(), name='authors'),
9 | path('authors/', views.AuthorDetailView.as_view(), name='author-detail'),
10 | path('mybooks/', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'),
11 | path('book//renew/', views.renew_book_librarian, name='renew-book-librarian'),
12 | path('author/create/', views.AuthorCreate.as_view(), name='author_create'),
13 | path('author//update/', views.AuthorUpdate.as_view(), name='author_update'),
14 | path('author//delete/', views.AuthorDelete.as_view(), name='author_delete'),
15 | path('book/create/', views.BookCreate.as_view(), name='book_create'),
16 | path('book//update/', views.BookUpdate.as_view(), name='book_update'),
17 | path('book//delete/', views.BookDelete.as_view(), name='book_delete'),
18 | path('search/', views.search_screen_view, name='search'),
19 | path('borrowed/', views.BooksBorrowed.as_view(), name='borrowed'),
20 | ]
--------------------------------------------------------------------------------
/locallibrary/templates/registration/password_reset_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 | {% if validlink %}
6 |
Please enter (and confirm) your new password.
7 |
26 | {% else %}
27 |
Password reset failed
28 |
The password reset link was invalid, possibly because it has already been used. Please request a new password reset.
29 | {% endif %}
30 |
31 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
35 |
36 | {# Assumes you setup the password_reset view in your URLconf #}
37 |
38 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/catalog/book_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
Título: {{ book.title }}
6 |
7 |
Autor: {{ book.author }}
8 |
Sumário: {{ book.summary }}
9 |
ISBN: {{ book.isbn }}
10 |
Idioma: {{ book.language }}
11 |
Gênero: {% for genre in book.genre.all %} {{ genre }}{% if not forloop.last %}, {% endif %}{% endfor %}
12 | {% if perms.catalog.can_mark_returned %}
13 |
Atualizar
14 |
Remover
15 | {% endif %}
16 |
17 |
18 |
Cópias:
19 | {% for copy in book.bookinstance_set.all %}
20 |
{{ copy.get_status_display }}
21 | {% if copy.status != 'a' %}
22 |
Due to be returned: {{copy.due_back}}
23 | {% endif %}
24 |
Imprint: {{copy.imprint}}
25 |
Id: {{copy.id}}
26 | {% endfor %}
27 |
28 |
29 | {% endblock %}
--------------------------------------------------------------------------------
/locallibrary/catalog/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from catalog.models import Author
4 |
5 | class AuthorModelTest(TestCase):
6 | @classmethod
7 | def setUpTestData(cls):
8 | # Set up non-modified objects used by all test methods
9 | Author.objects.create(first_name='Big', last_name='Bob')
10 |
11 | def test_first_name_label(self):
12 | author = Author.objects.get(id=1)
13 | field_label = author._meta.get_field('first_name').verbose_name
14 | self.assertEquals(field_label, 'first name')
15 |
16 | def test_date_of_death_label(self):
17 | author=Author.objects.get(id=1)
18 | field_label = author._meta.get_field('date_of_death').verbose_name
19 | self.assertEquals(field_label, 'died')
20 |
21 | def test_first_name_max_length(self):
22 | author = Author.objects.get(id=1)
23 | max_length = author._meta.get_field('first_name').max_length
24 | self.assertEquals(max_length, 100)
25 |
26 | def test_object_name_is_last_name_comma_first_name(self):
27 | author = Author.objects.get(id=1)
28 | expected_object_name = f'{author.last_name}, {author.first_name}'
29 | self.assertEquals(expected_object_name, str(author))
30 |
31 | def test_get_absolute_url(self):
32 | author = Author.objects.get(id=1)
33 | # This will also fail if the urlconf is not defined.
34 | self.assertEquals(author.get_absolute_url(), '/catalog/authors/1')
--------------------------------------------------------------------------------
/locallibrary/catalog/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from django.test import TestCase
4 | from django.utils import timezone
5 |
6 | from catalog.forms import RenewBookForm
7 |
8 | class RenewBookFormTest(TestCase):
9 | def test_renew_form_date_field_label(self):
10 | form = RenewBookForm()
11 | self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date')
12 |
13 | def test_renew_form_date_field_help_text(self):
14 | form = RenewBookForm()
15 | self.assertEqual(form.fields['renewal_date'].help_text, 'Enter a date between now and 4 weeks (default 3).')
16 |
17 | def test_renew_form_date_in_past(self):
18 | date = datetime.date.today() - datetime.timedelta(days=1)
19 | form = RenewBookForm(data={'renewal_date': date})
20 | self.assertFalse(form.is_valid())
21 |
22 | def test_renew_form_date_too_far_in_future(self):
23 | date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
24 | form = RenewBookForm(data={'renewal_date': date})
25 | self.assertFalse(form.is_valid())
26 |
27 | def test_renew_form_date_today(self):
28 | date = datetime.date.today()
29 | form = RenewBookForm(data={'renewal_date': date})
30 | self.assertTrue(form.is_valid())
31 |
32 | def test_renew_form_date_max(self):
33 | date = timezone.now() + datetime.timedelta(weeks=4)
34 | form = RenewBookForm(data={'renewal_date': date})
35 | self.assertTrue(form.is_valid())
36 |
--------------------------------------------------------------------------------
/locallibrary/catalog/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from catalog.models import Author, Genre, Book, BookInstance, Language
3 |
4 | # Register your models here.
5 | #admin.site.register(Book)
6 | #admin.site.register(Author)
7 | admin.site.register(Genre)
8 | #admin.site.register(BookInstance)
9 | admin.site.register(Language)
10 |
11 | class AuthorInstanceInline(admin.TabularInline):
12 | model = Book
13 | extra = 0
14 |
15 | # Define the admin class
16 | class AuthorAdmin(admin.ModelAdmin):
17 | list_display = ('last_name', 'first_name', 'date_of_birth', 'date_of_death')
18 | fields = ['first_name', 'last_name', ('date_of_birth', 'date_of_death')]
19 | inlines = [AuthorInstanceInline]
20 |
21 | # Register the admin class with the associated model
22 | admin.site.register(Author, AuthorAdmin)
23 |
24 | class BooksInstanceInline(admin.TabularInline):
25 | model = BookInstance
26 | extra = 0
27 |
28 | # Register the Admin classes for Book using the decorator
29 | @admin.register(Book)
30 | class BookAdmin(admin.ModelAdmin):
31 | list_display = ('title', 'author', 'display_genre')
32 | inlines = [BooksInstanceInline]
33 |
34 | # Register the Admin classes for BookInstance using the decorator
35 | @admin.register(BookInstance)
36 | class BookInstanceAdmin(admin.ModelAdmin):
37 | list_display = ('book', 'status', 'borrower', 'due_back', 'id')
38 | list_filter = ('status', 'due_back')
39 |
40 | fieldsets = (
41 | (None, {
42 | 'fields': ('book','imprint', 'id')
43 | }),
44 | ('Availability', {
45 | 'fields': ('status', 'due_back','borrower')
46 | }),
47 | )
--------------------------------------------------------------------------------
/locallibrary/catalog/static/css/styles.css:
--------------------------------------------------------------------------------
1 | .nav{
2 | padding: 20px;
3 | border: 1px solid white;
4 | margin: auto;
5 | background-color: #222222;
6 | border-radius: 10px;
7 | }
8 |
9 | a{
10 | color: white !important;
11 | }
12 |
13 | strong {
14 | color: white;
15 | }
16 |
17 | b{
18 | color: black;
19 | }
20 |
21 | .container{
22 | padding: 20px;
23 | margin-top: 20px;
24 | background-color:#100109;
25 | margin-bottom: 20px;
26 | }
27 |
28 | .caixa-principal{
29 | border: 1px solid white;
30 | box-shadow: 5px 5px 5px #222222;
31 | border-radius: 14px;
32 | }
33 |
34 | .titulo{
35 | text-shadow:2px 2px #222222, 0 0 4px black;
36 | color:#908d94;
37 | font-weight: bold;
38 | font-style: oblique;
39 | }
40 |
41 | .list-group-item{
42 | margin-bottom:4px;
43 | }
44 |
45 | hr{
46 | background-color: white;
47 | }
48 |
49 | .btn-primary{
50 | margin-top: 10px;
51 | background-color: gray !important;
52 | border: 1px solid white !important;
53 | }
54 |
55 | .page-current{
56 | background-color:#222222 !important;
57 | }
58 |
59 | .livro{
60 | color: white;
61 | }
62 |
63 | .skull{
64 | display: block;
65 | margin-left: auto;
66 | margin-right: auto;
67 | width: 50%;
68 | }
69 |
70 | .list-group-horizontal .list-group-item {
71 | margin-bottom: 4px !important;
72 | }
73 |
74 | form{
75 | padding: 20px;
76 | border: 1px solid white;
77 | background-color: #222222;
78 | }
79 |
80 | input {
81 | width: 100%;
82 | padding: 6px 12px;
83 | border: 1px solid black;
84 | border-radius: 10px;
85 | color: black;
86 | }
87 |
88 | input:focus{
89 | background-color: gray;
90 | }
91 |
92 | textarea{
93 | resize: none;
94 | display: block;
95 | }
96 |
97 | .form-group > label:nth-child(6){
98 | display: block;
99 | }
100 |
101 | #id_genre, #id_language, #id_author{
102 | display: block;
103 | }
104 |
105 | .form-group > label:nth-child(14){
106 | display: block;
107 | margin-top: 5px;
108 | }
--------------------------------------------------------------------------------
/locallibrary/catalog/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.3 on 2019-07-05 18:20
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import uuid
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Author',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('first_name', models.CharField(max_length=100)),
21 | ('last_name', models.CharField(max_length=100)),
22 | ('date_of_birth', models.DateField(blank=True, null=True)),
23 | ('date_of_death', models.DateField(blank=True, null=True, verbose_name='Died')),
24 | ],
25 | options={
26 | 'ordering': ['last_name', 'first_name'],
27 | },
28 | ),
29 | migrations.CreateModel(
30 | name='Book',
31 | fields=[
32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
33 | ('title', models.CharField(max_length=200)),
34 | ('summary', models.TextField(help_text='Enter a brief description of the book', max_length=1000)),
35 | ('isbn', models.CharField(help_text='13 Character ISBN number', max_length=13, verbose_name='ISBN')),
36 | ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Author')),
37 | ],
38 | ),
39 | migrations.CreateModel(
40 | name='Genre',
41 | fields=[
42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
43 | ('name', models.CharField(help_text='Enter a book genre (e.g. Science Fiction)', max_length=200)),
44 | ],
45 | ),
46 | migrations.CreateModel(
47 | name='BookInstance',
48 | fields=[
49 | ('id', models.UUIDField(default=uuid.uuid4, help_text='Unique ID for this particular book across whole library', primary_key=True, serialize=False)),
50 | ('imprint', models.CharField(max_length=200)),
51 | ('due_back', models.DateField(blank=True, null=True)),
52 | ('status', models.CharField(blank=True, choices=[('m', 'Maintenance'), ('o', 'On loan'), ('a', 'Available'), ('r', 'Reserved')], default='m', help_text='Book availability', max_length=1)),
53 | ('book', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalog.Book')),
54 | ],
55 | options={
56 | 'ordering': ['due_back'],
57 | },
58 | ),
59 | migrations.AddField(
60 | model_name='book',
61 | name='genre',
62 | field=models.ManyToManyField(help_text='Select a genre for this book', to='catalog.Genre'),
63 | ),
64 | ]
65 |
--------------------------------------------------------------------------------
/locallibrary/catalog/templates/base_generic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block title %}Biblioteca Secreta{% endblock %}
5 |
6 |
7 |
8 | {% load static %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | {% block sidebar %}
16 |
33 | {% endblock %}
34 |
35 |
36 | {% block content %}{% endblock %}
37 | {% block pagination %}
38 | {% if is_paginated %}
39 |
52 | {% endif %}
53 | {% endblock %}
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/locallibrary/locallibrary/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for locallibrary project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.2.3.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.2/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '_7q^ww*$e@(^u2*47+&9%#_l#i7kc=s^h9b0ozx7&6(#9%pigb'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'catalog',
41 | ]
42 |
43 | MIDDLEWARE = [
44 | 'django.middleware.security.SecurityMiddleware',
45 | 'django.contrib.sessions.middleware.SessionMiddleware',
46 | 'django.middleware.common.CommonMiddleware',
47 | 'django.middleware.csrf.CsrfViewMiddleware',
48 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
49 | 'django.contrib.messages.middleware.MessageMiddleware',
50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
51 | ]
52 |
53 | ROOT_URLCONF = 'locallibrary.urls'
54 |
55 | TEMPLATES = [
56 | {
57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
58 | 'DIRS': [os.path.join(BASE_DIR, 'templates')],
59 | 'APP_DIRS': True,
60 | 'OPTIONS': {
61 | 'context_processors': [
62 | 'django.template.context_processors.debug',
63 | 'django.template.context_processors.request',
64 | 'django.contrib.auth.context_processors.auth',
65 | 'django.contrib.messages.context_processors.messages',
66 | ],
67 | },
68 | },
69 | ]
70 |
71 | WSGI_APPLICATION = 'locallibrary.wsgi.application'
72 |
73 |
74 | # Database
75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
76 |
77 | DATABASES = {
78 | 'default': {
79 | 'ENGINE': 'django.db.backends.sqlite3',
80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
81 | }
82 | }
83 |
84 |
85 | # Password validation
86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
87 |
88 | AUTH_PASSWORD_VALIDATORS = [
89 | {
90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
91 | },
92 | {
93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
94 | },
95 | {
96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
97 | },
98 | {
99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
100 | },
101 | ]
102 |
103 |
104 | # Internationalization
105 | # https://docs.djangoproject.com/en/2.2/topics/i18n/
106 |
107 | LANGUAGE_CODE = 'en-us'
108 |
109 | TIME_ZONE = 'America/Sao_Paulo'
110 |
111 | USE_I18N = True
112 |
113 | USE_L10N = True
114 |
115 | USE_TZ = True
116 |
117 |
118 | # Static files (CSS, JavaScript, Images)
119 | # https://docs.djangoproject.com/en/2.2/howto/static-files/
120 |
121 | STATIC_URL = '/static/'
122 |
123 | # Redirect to home URL after login (Default redirects to /accounts/profile/)
124 | LOGIN_REDIRECT_URL = '/'
125 |
126 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
--------------------------------------------------------------------------------
/locallibrary/catalog/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.urls import reverse
3 | from django.contrib.auth.models import User
4 | from datetime import date
5 | import uuid
6 |
7 | class Genre(models.Model):
8 | """Model representing a book genre."""
9 | name = models.CharField(max_length=200, help_text='Enter a book genre (e.g. Science Fiction)')
10 |
11 | def __str__(self):
12 | """String for representing the Model object."""
13 | return self.name
14 |
15 | class Language(models.Model):
16 | """Model representing a Language (e.g. English, French, Japanese, etc.)"""
17 | name = models.CharField(max_length=200, help_text="Enter the book's natural language (e.g. English, French, Japanese etc.)")
18 |
19 | def __str__(self):
20 | return self.name
21 |
22 | class Book(models.Model):
23 | """Model representing a book (but not a specific copy of a book)."""
24 | title = models.CharField(max_length=200)
25 |
26 | # Foreign Key used because book can only have one author, but authors can have multiple books
27 | # Author as a string rather than object because it hasn't been declared yet in the file
28 | author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
29 |
30 | summary = models.TextField(max_length=1000, help_text='Enter a brief description of the book')
31 | isbn = models.CharField('ISBN', max_length=13, help_text='13 Character ISBN number')
32 |
33 | # ManyToManyField used because genre can contain many books. Books can cover many genres.
34 | # Genre class has already been defined so we can specify the object above.
35 | genre = models.ManyToManyField(Genre, help_text='Select a genre for this book')
36 | language = models.ForeignKey('Language', on_delete=models.SET_NULL, null=True)
37 |
38 | def __str__(self):
39 | """String for representing the Model object."""
40 | return self.title
41 |
42 | def get_absolute_url(self):
43 | """Returns the url to access a detail record for this book."""
44 | return reverse('book-detail', args=[str(self.id)])
45 |
46 | def display_genre(self):
47 | """Create a string for the Genre. This is required to display genre in Admin."""
48 | return ', '.join(genre.name for genre in self.genre.all()[:3])
49 |
50 | display_genre.short_description = 'Genre'
51 |
52 | class BookInstance(models.Model):
53 | """Model representing a specific copy of a book (i.e. that can be borrowed from the library)."""
54 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text='Unique ID for this particular book across whole library')
55 | book = models.ForeignKey('Book', on_delete=models.SET_NULL, null=True)
56 | imprint = models.CharField(max_length=200)
57 | due_back = models.DateField(null=True, blank=True)
58 | borrower = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
59 |
60 | LOAN_STATUS = (
61 | ('m', 'Maintenance'),
62 | ('o', 'On loan'),
63 | ('a', 'Available'),
64 | ('r', 'Reserved'),
65 | )
66 |
67 | status = models.CharField(
68 | max_length=1,
69 | choices=LOAN_STATUS,
70 | blank=True,
71 | default='m',
72 | help_text='Book availability',
73 | )
74 |
75 | class Meta:
76 | ordering = ['due_back']
77 | permissions = (("can_mark_returned", "Set book as returned"),)
78 |
79 | @property
80 | def is_overdue(self):
81 | if self.due_back and date.today() > self.due_back:
82 | return True
83 | return False
84 |
85 | def __str__(self):
86 | """String for representing the Model object."""
87 | return f'{self.id} ({self.book.title})'
88 |
89 | class Author(models.Model):
90 | """Model representing an author."""
91 | first_name = models.CharField(max_length=100)
92 | last_name = models.CharField(max_length=100)
93 | date_of_birth = models.DateField(null=True, blank=True)
94 | date_of_death = models.DateField('died', null=True, blank=True)
95 |
96 | class Meta:
97 | ordering = ['last_name', 'first_name']
98 |
99 | def get_absolute_url(self):
100 | """Returns the url to access a particular author instance."""
101 | return reverse('author-detail', args=[str(self.id)])
102 |
103 | def __str__(self):
104 | """String for representing the Model object."""
105 | return f'{self.last_name}, {self.first_name}'
106 |
107 | class Meta:
108 | ordering = ['first_name']
--------------------------------------------------------------------------------
/locallibrary/catalog/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, get_object_or_404
2 | from django.contrib.auth.decorators import permission_required
3 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
4 | import datetime
5 | from operator import attrgetter
6 | from django.http import HttpResponseRedirect
7 | from django.db.models import Q
8 | from django.urls import reverse, reverse_lazy
9 | from catalog.forms import RenewBookForm
10 | from django.views import generic
11 | from catalog.models import Book, Author, BookInstance, Genre
12 | from django.contrib.auth.mixins import LoginRequiredMixin
13 | from django.views.generic.edit import CreateView, UpdateView, DeleteView
14 | from catalog.models import Author
15 |
16 | def index(request):
17 | """View function for home page of site."""
18 | # Generate counts of some of the main objects
19 | num_books = Book.objects.all().count()
20 | num_instances = BookInstance.objects.all().count()
21 |
22 | # Available books (status = 'a')
23 | num_instances_available = BookInstance.objects.filter(status__exact='a').count()
24 |
25 | # The 'all()' is implied by default.
26 | num_authors = Author.objects.count()
27 |
28 | # Number of visits to this view, as counted in the session variable.
29 | num_visits = request.session.get('num_visits', 0)
30 | request.session['num_visits'] = num_visits + 1
31 |
32 | context = {
33 | 'num_books': num_books,
34 | 'num_instances': num_instances,
35 | 'num_instances_available': num_instances_available,
36 | 'num_authors': num_authors,
37 | 'num_visits': num_visits,
38 | }
39 |
40 | # Render the HTML template index.html with the data in the context variable
41 | return render(request, 'index.html', context=context)
42 |
43 | class BookListView(generic.ListView):
44 | model = Book
45 | paginate_by = 8
46 |
47 | class BookDetailView(generic.DetailView):
48 | model = Book
49 |
50 | class AuthorListView(generic.ListView):
51 | model = Author
52 | paginate_by = 8
53 |
54 | class AuthorDetailView(generic.DetailView):
55 | model = Author
56 |
57 | class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
58 | """Generic class-based view listing books on loan to current user."""
59 | model = BookInstance
60 | template_name ='catalog/bookinstance_list_borrowed_user.html'
61 | paginate_by = 10
62 |
63 | def get_queryset(self):
64 | return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back') # 'o' é o código para on loan
65 |
66 | @permission_required('catalog.can_mark_returned')
67 | def renew_book_librarian(request, pk):
68 | """View function for renewing a specific BookInstance by librarian."""
69 | book_instance = get_object_or_404(BookInstance, pk=pk)
70 |
71 | # If this is a POST request then process the Form data
72 | if request.method == 'POST':
73 |
74 | # Create a form instance and populate it with data from the request (binding):
75 | form = RenewBookForm(request.POST)
76 |
77 | # Check if the form is valid:
78 | if form.is_valid():
79 | # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
80 | book_instance.due_back = form.cleaned_data['renewal_date']
81 | book_instance.save()
82 |
83 | # redirect to a new URL:
84 | return HttpResponseRedirect(reverse('my-borrowed') )
85 |
86 | # If this is a GET (or any other method) create the default form.
87 | else:
88 | proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
89 | form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})
90 |
91 | context = {
92 | 'form': form,
93 | 'book_instance': book_instance,
94 | }
95 |
96 | return render(request, 'catalog/book_renew_librarian.html', context)
97 |
98 | class AuthorCreate(LoginRequiredMixin, CreateView):
99 | model = Author
100 | fields = '__all__'
101 | initial = {'date_of_death': '05/01/2018'}
102 |
103 | class AuthorUpdate(LoginRequiredMixin, UpdateView):
104 | model = Author
105 | fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death']
106 |
107 | class AuthorDelete(LoginRequiredMixin, DeleteView):
108 | model = Author
109 | success_url = reverse_lazy('authors')
110 |
111 | class BookCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
112 | permission_required = 'catalog.can_mark_returned'
113 | model = Book
114 | fields = '__all__'
115 |
116 | class BookUpdate(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
117 | permission_required = 'catalog.can_mark_returned'
118 | model = Book
119 | fields = ['title', 'author', 'summary', 'isbn', 'genre', 'language']
120 |
121 | class BookDelete(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
122 | model = Book
123 | success_url = reverse_lazy('books')
124 | permission_required = 'catalog.can_mark_returned'
125 |
126 | class BooksBorrowed(LoginRequiredMixin, PermissionRequiredMixin, generic.ListView):
127 | permission_required = 'catalog.can_mark_returned'
128 | model = BookInstance
129 | template_name ='catalog/list_borrowed.html'
130 | paginate_by = 10
131 |
132 | def get_book_queryset(query=None):
133 | queryset = []
134 | queries = query.split(' ') # python install 2019 = ['python', 'install', '2019']
135 | for q in queries:
136 | posts = Book.objects.filter(Q(title__contains=q)| Q(summary__icontains=q) ).distinct()
137 |
138 | for post in posts:
139 | queryset.append(post)
140 |
141 | return list(set(queryset))
142 |
143 | def search_screen_view(request):
144 | context = {}
145 |
146 | query = ""
147 | if request.GET:
148 | query = request.GET.get('q', '')
149 | context['query'] = str(query)
150 |
151 | books = sorted(get_book_queryset(query), key=attrgetter('title'), reverse=True)
152 |
153 | context['books'] = books
154 |
155 | return render(request, 'catalog/search.html', context)
--------------------------------------------------------------------------------
/locallibrary/catalog/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | import uuid
3 | from django.urls import reverse
4 | from django.utils import timezone
5 | from django.contrib.auth.models import User, Permission
6 | import datetime
7 | from catalog.models import BookInstance, Book, Genre, Language, Author
8 | from catalog.forms import RenewBookForm
9 |
10 | class AuthorListViewTest(TestCase):
11 | @classmethod
12 | def setUpTestData(cls):
13 | # Create 13 authors for pagination tests
14 | number_of_authors = 13
15 |
16 | for author_id in range(number_of_authors):
17 | Author.objects.create(
18 | first_name=f'Christian {author_id}',
19 | last_name=f'Surname {author_id}',
20 | )
21 |
22 | def test_view_url_exists_at_desired_location(self):
23 | response = self.client.get('/catalog/authors/')
24 | self.assertEqual(response.status_code, 200)
25 |
26 | def test_view_url_accessible_by_name(self):
27 | response = self.client.get(reverse('authors'))
28 | self.assertEqual(response.status_code, 200)
29 |
30 | def test_view_uses_correct_template(self):
31 | response = self.client.get(reverse('authors'))
32 | self.assertEqual(response.status_code, 200)
33 | self.assertTemplateUsed(response, 'catalog/author_list.html')
34 |
35 | def test_pagination_is_ten(self):
36 | response = self.client.get(reverse('authors'))
37 | self.assertEqual(response.status_code, 200)
38 | self.assertTrue('is_paginated' in response.context)
39 | self.assertTrue(response.context['is_paginated'] == True)
40 | self.assertTrue(len(response.context['author_list']) == 10)
41 |
42 | def test_lists_all_authors(self):
43 | # Get second page and confirm it has (exactly) remaining 3 items
44 | response = self.client.get(reverse('authors')+'?page=2')
45 | self.assertEqual(response.status_code, 200)
46 | self.assertTrue('is_paginated' in response.context)
47 | self.assertTrue(response.context['is_paginated'] == True)
48 | self.assertTrue(len(response.context['author_list']) == 3)
49 |
50 | class LoanedBookInstancesByUserListViewTest(TestCase):
51 | def setUp(self):
52 | # Create two users
53 | test_user1 = User.objects.create_user(username='testuser1', password='1X