├── core ├── __init__.py ├── views.py ├── apps.py ├── urls.py ├── static │ └── css │ │ └── style.css └── templates │ ├── index.html │ ├── includes │ ├── nav.html │ └── pagination.html │ └── base.html ├── backend ├── __init__.py ├── api.py ├── urls.py ├── asgi.py ├── wsgi.py └── settings.py ├── bookstore ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── templates │ └── bookstore │ │ ├── author_detail.html │ │ ├── publisher_detail.html │ │ ├── book_detail.html │ │ ├── book_form.html │ │ ├── author_form.html │ │ ├── publisher_form.html │ │ ├── author_confirm_delete.html │ │ ├── book_confirm_delete.html │ │ ├── publisher_confirm_delete.html │ │ ├── author_list.html │ │ ├── publisher_list.html │ │ └── book_list.html ├── admin.py ├── api_drf │ ├── serializers.py │ └── viewsets.py ├── forms.py ├── models.py ├── views.py ├── urls.py └── api.py ├── requirements.txt ├── manage.py ├── contrib └── env_gen.py ├── README.md ├── .gitignore └── passo-a-passo.md /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bookstore/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def index(request): 5 | template_name = 'index.html' 6 | return render(request, template_name) 7 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'core' 7 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import index 4 | 5 | app_name = 'core' 6 | 7 | urlpatterns = [ 8 | path('', index, name='index'), 9 | ] 10 | -------------------------------------------------------------------------------- /backend/api.py: -------------------------------------------------------------------------------- 1 | from ninja import NinjaAPI 2 | 3 | from bookstore.api import router as bookstore_router 4 | 5 | api = NinjaAPI() 6 | 7 | api.add_router('/bookstore/', bookstore_router) 8 | -------------------------------------------------------------------------------- /core/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 60px; 3 | } 4 | 5 | label.required:after { 6 | content: ' *'; 7 | color: red; 8 | } 9 | 10 | .no { 11 | color: red; 12 | } -------------------------------------------------------------------------------- /bookstore/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BookstoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'bookstore' 7 | 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.2.8 2 | djangorestframework==3.12.4 3 | dr-scaffold==2.1.1 4 | django-extensions==3.1.3 5 | django-seed==0.3.1 6 | python-decouple==3.5 7 | psycopg2-binary==2.9.1 8 | django-ninja==0.16.1 9 | -------------------------------------------------------------------------------- /core/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Django

6 | Github 7 |
8 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/author_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Detalhes do Autor

6 | 7 | 10 | 11 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/publisher_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Detalhes da Editora

6 | 7 | 11 | 12 | {% endblock content %} -------------------------------------------------------------------------------- /backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | from .api import api 5 | 6 | 7 | urlpatterns = [ 8 | path('', include('core.urls')), 9 | path('', include('bookstore.urls', namespace='bookstore')), 10 | path('api/v2/', api.urls), 11 | path('admin/', admin.site.urls), 12 | ] 13 | -------------------------------------------------------------------------------- /backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend 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/3.2/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', 'backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend 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/3.2/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', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /bookstore/templates/bookstore/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Detalhes do Livro

6 | 7 | 16 | 17 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/book_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Adiciona Livro

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/author_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Adiciona Autor

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/publisher_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Adiciona Editora

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/author_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Deletar Autor

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 |

Deseja deletar {{ object }} ?

12 |
13 | 14 | Não 15 |
16 |
17 |
18 |
19 | 20 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/book_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Deletar Livro

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 |

Deseja deletar {{ object }} ?

12 |
13 | 14 | Não 15 |
16 |
17 |
18 |
19 | 20 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/templates/bookstore/publisher_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

Deletar Editora

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 |

Deseja deletar {{ object }} ?

12 |
13 | 14 | Não 15 |
16 |
17 |
18 |
19 | 20 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from bookstore.models import Author, Book, Publisher 4 | 5 | 6 | @admin.register(Author) 7 | class AuthorAdmin(admin.ModelAdmin): 8 | list_display = ('__str__',) 9 | search_fields = ('name',) 10 | 11 | 12 | @admin.register(Publisher) 13 | class PublisherAdmin(admin.ModelAdmin): 14 | list_display = ('__str__', 'score') 15 | search_fields = ('name',) 16 | 17 | 18 | @admin.register(Book) 19 | class BookAdmin(admin.ModelAdmin): 20 | list_display = ('__str__', 'isbn', 'rating', 'price', 'stock') 21 | search_fields = ('name', 'isbn', 'authors__name', 'publisher__name') 22 | -------------------------------------------------------------------------------- /bookstore/api_drf/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from bookstore.models import Author, Book, Publisher 4 | 5 | 6 | class AuthorSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Author 10 | fields = '__all__' 11 | 12 | 13 | class PublisherSerializer(serializers.ModelSerializer): 14 | 15 | class Meta: 16 | model = Publisher 17 | fields = '__all__' 18 | 19 | 20 | class BookSerializer(serializers.ModelSerializer): 21 | authors = AuthorSerializer(many=True) 22 | publisher = PublisherSerializer() 23 | 24 | class Meta: 25 | model = Book 26 | fields = '__all__' 27 | -------------------------------------------------------------------------------- /bookstore/api_drf/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from bookstore.models import Author, Book, Publisher 4 | from bookstore.api_drf.serializers import ( 5 | AuthorSerializer, 6 | BookSerializer, 7 | PublisherSerializer 8 | ) 9 | 10 | 11 | class AuthorViewSet(viewsets.ModelViewSet): 12 | queryset = Author.objects.all() 13 | serializer_class = AuthorSerializer 14 | 15 | 16 | class PublisherViewSet(viewsets.ModelViewSet): 17 | queryset = Publisher.objects.all() 18 | serializer_class = PublisherSerializer 19 | 20 | 21 | class BookViewSet(viewsets.ModelViewSet): 22 | queryset = Book.objects.all() 23 | serializer_class = BookSerializer 24 | -------------------------------------------------------------------------------- /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', 'backend.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 | -------------------------------------------------------------------------------- /bookstore/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Author, Book, Publisher 4 | 5 | 6 | class _BaseModelForm(forms.ModelForm): 7 | required_css_class = 'required' 8 | 9 | def __init__(self, *args, **kwargs): 10 | super(_BaseModelForm, self).__init__(*args, **kwargs) 11 | for field_name, field in self.fields.items(): 12 | field.widget.attrs['class'] = 'form-control' 13 | 14 | 15 | class AuthorForm(_BaseModelForm): 16 | 17 | class Meta: 18 | model = Author 19 | fields = '__all__' 20 | 21 | 22 | class PublisherForm(_BaseModelForm): 23 | 24 | class Meta: 25 | model = Publisher 26 | fields = '__all__' 27 | 28 | 29 | class BookForm(_BaseModelForm): 30 | 31 | class Meta: 32 | model = Book 33 | fields = '__all__' 34 | -------------------------------------------------------------------------------- /bookstore/templates/bookstore/author_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

6 | Lista de Autores 7 | 8 | 9 | Adicionar 10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for object in object_list %} 22 | 23 | 26 | 34 | 35 | {% endfor %} 36 | 37 |
NomeAções
24 | {{ object.name }} 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
38 | 39 | {% include "includes/pagination.html" %} 40 | 41 | {% endblock content %} -------------------------------------------------------------------------------- /contrib/env_gen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python SECRET_KEY generator. 3 | """ 4 | import random 5 | 6 | chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!?@#$%^&*()" 7 | size = 50 8 | secret_key = "".join(random.sample(chars, size)) 9 | 10 | chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!?@#$%_" 11 | size = 20 12 | password = "".join(random.sample(chars, size)) 13 | 14 | CONFIG_STRING = """ 15 | DEBUG=True 16 | SECRET_KEY=%s 17 | ALLOWED_HOSTS=127.0.0.1,.localhost,0.0.0.0 18 | 19 | #DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME 20 | #POSTGRES_DB= 21 | #POSTGRES_USER= 22 | #POSTGRES_PASSWORD=%s 23 | #DB_HOST=localhost 24 | 25 | #DEFAULT_FROM_EMAIL= 26 | #EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 27 | #EMAIL_HOST=localhost 28 | #EMAIL_PORT= 29 | #EMAIL_HOST_USER= 30 | #EMAIL_HOST_PASSWORD= 31 | #EMAIL_USE_TLS=True 32 | """.strip() % (secret_key, password) 33 | 34 | # Writing our configuration file to '.env' 35 | with open('.env', 'w') as configfile: 36 | configfile.write(CONFIG_STRING) 37 | 38 | print('Success!') 39 | print('Type: cat .env') 40 | -------------------------------------------------------------------------------- /core/templates/includes/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bookstore/templates/bookstore/publisher_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

6 | Lista de Editoras 7 | 8 | 9 | Adicionar 10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for object in object_list %} 23 | 24 | 27 | 28 | 36 | 37 | {% endfor %} 38 | 39 |
NomePontosAções
25 | {{ object.name }} 26 | {{ object.score }} 29 | 30 | 31 | 32 | 33 | 34 | 35 |
40 | 41 | {% include "includes/pagination.html" %} 42 | 43 | {% endblock content %} -------------------------------------------------------------------------------- /core/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Django 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% block css %}{% endblock css %} 22 | 23 | 24 | 25 | 26 |
27 | {% include "includes/nav.html" %} 28 | {% block content %}{% endblock content %} 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /bookstore/templates/bookstore/book_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

6 | Lista de Livros 7 | 8 | 9 | Adicionar 10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for object in object_list %} 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 46 | 47 | {% endfor %} 48 | 49 |
NomeISBNPontuaçãoAutoresEditoraPreçoEstoqueAções
30 | {{ object.name }} 31 | {{ object.isbn }}{{ object.rating }}{{ object.authors.all|join:", " }}{{ object.publisher }}{{ object.price }}{{ object.stock }} 39 | 40 | 41 | 42 | 43 | 44 | 45 |
50 | 51 | {% include "includes/pagination.html" %} 52 | 53 | {% endblock content %} -------------------------------------------------------------------------------- /bookstore/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse_lazy 3 | 4 | 5 | class Author(models.Model): 6 | name = models.CharField(max_length=50, unique=True) 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | class Meta: 12 | ordering = ('name',) 13 | verbose_name = 'Autor' 14 | verbose_name_plural = 'Autores' 15 | 16 | def get_absolute_url(self): 17 | return reverse_lazy('bookstore:author_detail', kwargs={'pk': self.pk}) 18 | 19 | 20 | class Publisher(models.Model): 21 | name = models.CharField(max_length=50, unique=True) 22 | score = models.PositiveIntegerField(default=0) 23 | 24 | def __str__(self): 25 | return self.name 26 | 27 | class Meta: 28 | ordering = ('name',) 29 | verbose_name = 'Editora' 30 | verbose_name_plural = 'Editoras' 31 | 32 | def get_absolute_url(self): 33 | return reverse_lazy('bookstore:publisher_detail', kwargs={'pk': self.pk}) 34 | 35 | 36 | class Book(models.Model): 37 | name = models.CharField(max_length=100) 38 | isbn = models.CharField(max_length=13) 39 | rating = models.DecimalField(max_digits=5, decimal_places=2, null=True, default=0.0) 40 | authors = models.ManyToManyField(Author) 41 | publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, null=True) 42 | price = models.DecimalField(max_digits=5, decimal_places=2, null=True, default=0.0) 43 | stock = models.IntegerField(default=0) 44 | created = models.DateTimeField(auto_now_add=True) 45 | 46 | def __str__(self): 47 | return self.name 48 | 49 | class Meta: 50 | ordering = ('name',) 51 | verbose_name = 'Livro' 52 | verbose_name_plural = 'Livros' 53 | 54 | def get_absolute_url(self): 55 | return reverse_lazy('bookstore:book_detail', kwargs={'pk': self.pk}) 56 | -------------------------------------------------------------------------------- /bookstore/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse_lazy 2 | from django.views.generic import ( 3 | CreateView, 4 | DeleteView, 5 | DetailView, 6 | ListView, 7 | UpdateView 8 | ) 9 | 10 | from .forms import AuthorForm, BookForm, PublisherForm 11 | from .models import Author, Book, Publisher 12 | 13 | 14 | class AuthorListView(ListView): 15 | model = Author 16 | paginate_by = 20 17 | 18 | 19 | class AuthorDetailView(DetailView): 20 | model = Author 21 | 22 | 23 | class AuthorCreateView(CreateView): 24 | model = Author 25 | form_class = AuthorForm 26 | 27 | 28 | class AuthorUpdateView(UpdateView): 29 | model = Author 30 | form_class = AuthorForm 31 | 32 | 33 | class AuthorDeleteView(DeleteView): 34 | model = Author 35 | success_url = reverse_lazy('bookstore:author_list') 36 | 37 | 38 | class PublisherListView(ListView): 39 | model = Publisher 40 | paginate_by = 20 41 | 42 | 43 | class PublisherDetailView(DetailView): 44 | model = Publisher 45 | 46 | 47 | class PublisherCreateView(CreateView): 48 | model = Publisher 49 | form_class = PublisherForm 50 | 51 | 52 | class PublisherUpdateView(UpdateView): 53 | model = Publisher 54 | form_class = PublisherForm 55 | 56 | 57 | class PublisherDeleteView(DeleteView): 58 | model = Publisher 59 | success_url = reverse_lazy('bookstore:publisher_list') 60 | 61 | 62 | class BookListView(ListView): 63 | model = Book 64 | paginate_by = 20 65 | 66 | 67 | class BookDetailView(DetailView): 68 | model = Book 69 | 70 | 71 | class BookCreateView(CreateView): 72 | model = Book 73 | form_class = BookForm 74 | 75 | 76 | class BookUpdateView(UpdateView): 77 | model = Book 78 | form_class = BookForm 79 | 80 | 81 | class BookDeleteView(DeleteView): 82 | model = Book 83 | success_url = reverse_lazy('bookstore:book_list') 84 | -------------------------------------------------------------------------------- /bookstore/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from bookstore import views as v 5 | from bookstore.api_drf.viewsets import ( 6 | AuthorViewSet, 7 | BookViewSet, 8 | PublisherViewSet 9 | ) 10 | 11 | app_name = 'bookstore' 12 | 13 | router = routers.DefaultRouter() 14 | 15 | router.register(r'authors', AuthorViewSet) 16 | router.register(r'publishers', PublisherViewSet) 17 | router.register(r'books', BookViewSet) 18 | 19 | authors_urlpatterns = [ 20 | path('', v.AuthorListView.as_view(), name='author_list'), 21 | path('/', v.AuthorDetailView.as_view(), name='author_detail'), 22 | path('create/', v.AuthorCreateView.as_view(), name='author_create'), 23 | path('/update/', v.AuthorUpdateView.as_view(), name='author_update'), 24 | path('/delete/', v.AuthorDeleteView.as_view(), name='author_delete'), 25 | ] 26 | 27 | publishers_urlpatterns = [ 28 | path('', v.PublisherListView.as_view(), name='publisher_list'), 29 | path('/', v.PublisherDetailView.as_view(), name='publisher_detail'), 30 | path('create/', v.PublisherCreateView.as_view(), name='publisher_create'), 31 | path('/update/', v.PublisherUpdateView.as_view(), name='publisher_update'), 32 | path('/delete/', v.PublisherDeleteView.as_view(), name='publisher_delete'), 33 | ] 34 | 35 | books_urlpatterns = [ 36 | path('', v.BookListView.as_view(), name='book_list'), 37 | path('/', v.BookDetailView.as_view(), name='book_detail'), 38 | path('create/', v.BookCreateView.as_view(), name='book_create'), 39 | path('/update/', v.BookUpdateView.as_view(), name='book_update'), 40 | path('/delete/', v.BookDeleteView.as_view(), name='book_delete'), 41 | ] 42 | 43 | urlpatterns = [ 44 | path('api/v1/bookstore/', include(router.urls)), 45 | path('author/', include(authors_urlpatterns)), 46 | path('publisher/', include(publishers_urlpatterns)), 47 | path('book/', include(books_urlpatterns)), 48 | ] 49 | -------------------------------------------------------------------------------- /core/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 | {% if page_obj.has_previous %} 5 |
  • «
  • 6 | {% endif %} 7 | 8 | {% for pg in page_obj.paginator.page_range %} 9 | 10 | {% if pg == 1 or pg == 2 or pg == 3 or pg == page_obj.paginator.num_pages or pg == page_obj.paginator.num_pages|add:'-1' or pg == page_obj.paginator.num_pages|add:'-2' %} 11 | {% if page_obj.number == pg %} 12 |
  • {{ pg }} 14 |
  • 15 | {% else %} 16 |
  • {{ pg }} 18 |
  • 19 | {% endif %} 20 | {% else %} 21 | {% if page_obj.number == pg %} 22 |
  • {{ pg }}
  • 23 | {% elif pg > page_obj.number|add:'-4' and pg < page_obj.number|add:'4' %} 24 | 25 |
  • {{ pg }}
  • 26 | {% elif pg == page_obj.number|add:'-4' or pg == page_obj.number|add:'4' %} 27 |
  • ...
  • 28 | {% endif %} 29 | {% endif %} 30 | {% endfor %} 31 | 32 | {% if page_obj.has_next %} 33 |
  • »
  • 34 | {% endif %} 35 |
36 |
37 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookstore 2 | 3 | Exemplo de biblioteca feito com Django. 4 | 5 | 6 | ## Este projeto foi feito com: 7 | 8 | * [Python 3.9.7](https://www.python.org/) 9 | * [Django 3.2.8](https://www.djangoproject.com/) 10 | * [Django Rest Framework 3.12.4](https://www.django-rest-framework.org/) 11 | * [Bootstrap 4.0](https://getbootstrap.com/) 12 | * [VueJS 2.6.11](https://vuejs.org/) 13 | * [jQuery 3.4.1](https://jquery.com/) 14 | 15 | ## Como rodar o projeto? 16 | 17 | * Clone esse repositório. 18 | * Crie um virtualenv com Python 3. 19 | * Ative o virtualenv. 20 | * Instale as dependências. 21 | * Rode as migrações. 22 | 23 | ``` 24 | git clone https://github.com/rg3915/bookstore.git 25 | cd bookstore 26 | python -m venv .venv 27 | source .venv/bin/activate 28 | pip install -r requirements.txt 29 | python contrib/env_gen.py 30 | python manage.py migrate 31 | python manage.py createsuperuser --username="admin" --email="" 32 | ``` 33 | 34 | # Passo a passo 35 | 36 | https://github.com/rg3915/bookstore/blob/main/passo-a-passo.md 37 | 38 | --- 39 | 40 | ## Vídeos no meu canal 41 | 42 | * [A Essência do Django](https://youtu.be/mlaCLGItR7Q) 43 | * [Introdução a Arquitetura do Django - Pyjamas 2019](https://youtu.be/XjXpwZhOKOs) 44 | * [VueJS + Django Ninja](https://youtu.be/cZ7n3HN9MiU) 45 | * [Python-triangulo: Django: FBV vs CBV](https://www.youtube.com/watch?v=2qZcPb8ZWQA) 46 | * [Regis do Python](https://www.youtube.com/regis-do-python) 47 | 48 | 49 | ## Links 50 | 51 | * https://www.python.org/ 52 | * https://www.djangoproject.com/ 53 | * http://pythonclub.com.br/ 54 | * https://getbootstrap.com/ 55 | * https://docs.djangoproject.com/en/3.2/ref/models/fields/ 56 | * https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css 57 | * https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css 58 | * https://code.jquery.com/jquery-3.4.1.min.js 59 | * https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js 60 | * https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js 61 | * https://getbootstrap.com/docs/4.0/examples/starter-template/ 62 | * https://github.com/JTruax/bootstrap-starter-template/blob/master/template/start.html 63 | * https://ccbv.co.uk/ 64 | 65 | 66 | ## Livros 67 | 68 | * [Pense Python 2e](https://penseallen.github.io/PensePython2e/) 69 | * Aprenda Django 3 com Exemplos 70 | * Two Scoops of Django 71 | 72 | -------------------------------------------------------------------------------- /bookstore/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-25 10:12 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Author', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=50, unique=True)), 20 | ], 21 | options={ 22 | 'verbose_name': 'Autor', 23 | 'verbose_name_plural': 'Autores', 24 | 'ordering': ('name',), 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='Publisher', 29 | fields=[ 30 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('name', models.CharField(max_length=50, unique=True)), 32 | ('score', models.PositiveIntegerField(default=0)), 33 | ], 34 | options={ 35 | 'verbose_name': 'Editora', 36 | 'verbose_name_plural': 'Editoras', 37 | 'ordering': ('name',), 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='Book', 42 | fields=[ 43 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('name', models.CharField(max_length=100)), 45 | ('isbn', models.CharField(max_length=13)), 46 | ('rating', models.DecimalField(decimal_places=2, default=0.0, max_digits=5, null=True)), 47 | ('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=5, null=True)), 48 | ('stock', models.IntegerField(default=0)), 49 | ('created', models.DateTimeField(auto_now_add=True)), 50 | ('authors', models.ManyToManyField(to='bookstore.Author')), 51 | ('publisher', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookstore.publisher')), 52 | ], 53 | options={ 54 | 'verbose_name': 'Livro', 55 | 'verbose_name_plural': 'Livros', 56 | 'ordering': ('name',), 57 | }, 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | 133 | media/ 134 | staticfiles/ 135 | .idea 136 | .ipynb_checkpoints/ 137 | .vscode 138 | -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | from pathlib import Path 13 | 14 | from decouple import Csv, config 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = config('SECRET_KEY') 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = config('DEBUG', default=False, cast=bool) 27 | 28 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) 29 | 30 | INSTALLED_APPS = [ 31 | 'django.contrib.admin', 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.sessions', 35 | 'django.contrib.messages', 36 | 'django.contrib.staticfiles', 37 | # 3rd apps 38 | 'dr_scaffold', 39 | 'rest_framework', 40 | 'django_extensions', 41 | 'django_seed', 42 | # my apps 43 | 'core', 44 | 'bookstore', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'backend.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'backend.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': BASE_DIR / 'db.sqlite3', 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'pt-br' 112 | 113 | TIME_ZONE = 'America/Sao_Paulo' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 127 | 128 | # Default primary key field type 129 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 130 | 131 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 132 | -------------------------------------------------------------------------------- /bookstore/api.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from django.shortcuts import get_object_or_404 4 | from ninja import Router, Schema 5 | from ninja.orm import create_schema 6 | 7 | from .models import Author, Book, Publisher 8 | 9 | router = Router() 10 | 11 | AuthorSchema = create_schema(Author) 12 | PublisherSchema = create_schema(Publisher) 13 | BookSchema = create_schema(Book, depth=1) 14 | 15 | 16 | class AuthorSchemaIn(Schema): 17 | name: str 18 | 19 | 20 | class PublisherSchemaIn(Schema): 21 | name: str 22 | score: int 23 | 24 | 25 | class BookSchemaIn(Schema): 26 | name: str 27 | isbn: str 28 | rating: float 29 | publisher_id: int 30 | price: float 31 | stock: int 32 | authors: List[int] 33 | 34 | 35 | @router.get("/books", response=List[BookSchema]) 36 | def list_books(request): 37 | qs = Book.objects.all() 38 | return qs 39 | 40 | 41 | @router.get("/books/{id}", response=BookSchema) 42 | def get_book(request, id: int): 43 | book = get_object_or_404(Book, id=id) 44 | return book 45 | 46 | 47 | @router.post("/books", response={201: BookSchema}) 48 | def create_book(request, payload: BookSchemaIn): 49 | # Get params 50 | authors = payload.dict().pop('authors') 51 | publisher_id = payload.dict().pop('publisher_id') 52 | 53 | # Instance models 54 | # Get publisher 55 | publisher = get_object_or_404(Publisher, id=publisher_id) 56 | 57 | # Mount dict data 58 | data = {} 59 | for k, v in payload.dict().items(): 60 | data[k] = v 61 | 62 | data['publisher'] = publisher 63 | # Necessário porque é uma lista, mas não pode ser salvo diretamente. 64 | data.pop('authors', None) 65 | 66 | # Save data 67 | book = Book.objects.create(**data) 68 | 69 | # Add authors 70 | for author in authors: 71 | # Get authors 72 | author_obj = get_object_or_404(Author, id=author) 73 | book.authors.add(author_obj) 74 | 75 | return 201, book 76 | 77 | 78 | @router.put("/books/{id}", response=BookSchema) 79 | def update_book(request, id: int, payload: BookSchemaIn): 80 | book = get_object_or_404(Book, id=id) 81 | for attr, value in payload.dict().items(): 82 | if attr != 'authors': 83 | setattr(book, attr, value) 84 | book.save() 85 | 86 | authors = payload.dict().pop('authors') 87 | 88 | # Remove all authors 89 | book.authors.clear() 90 | 91 | # Add authors 92 | for author in authors: 93 | # Get authors 94 | author_obj = get_object_or_404(Author, id=author) 95 | book.authors.add(author_obj) 96 | 97 | return book 98 | 99 | 100 | @router.delete("/books/{id}", response={204: None}) 101 | def delete_book(request, id: int): 102 | book = get_object_or_404(Book, id=id) 103 | book.delete() 104 | return 204, None 105 | 106 | 107 | @router.get("/authors", response=List[AuthorSchema]) 108 | def list_authors(request): 109 | qs = Author.objects.all() 110 | return qs 111 | 112 | 113 | @router.get("/authors/{id}", response=AuthorSchema) 114 | def get_author(request, id: int): 115 | author = get_object_or_404(Author, id=id) 116 | return author 117 | 118 | 119 | @router.post("/authors", response={201: AuthorSchema}) 120 | def create_author(request, payload: AuthorSchemaIn): 121 | author = Author.objects.create(**payload.dict()) 122 | return 201, author 123 | 124 | 125 | @router.put("/authors/{id}", response=AuthorSchema) 126 | def update_author(request, id: int, payload: AuthorSchemaIn): 127 | author = get_object_or_404(Author, id=id) 128 | for attr, value in payload.dict().items(): 129 | setattr(author, attr, value) 130 | author.save() 131 | return author 132 | 133 | 134 | @router.delete("/authors/{id}", response={204: None}) 135 | def delete_author(request, id: int): 136 | author = get_object_or_404(Author, id=id) 137 | author.delete() 138 | return 204, None 139 | 140 | 141 | @router.get("/publishers", response=List[PublisherSchema]) 142 | def list_publishers(request): 143 | qs = Publisher.objects.all() 144 | return qs 145 | 146 | 147 | @router.get("/publishers/{id}", response=PublisherSchema) 148 | def get_publisher(request, id: int): 149 | publisher = get_object_or_404(Publisher, id=id) 150 | return publisher 151 | 152 | 153 | @router.post("/publishers", response={201: PublisherSchema}) 154 | def create_publisher(request, payload: PublisherSchemaIn): 155 | publisher = Publisher.objects.create(**payload.dict()) 156 | return 201, publisher 157 | 158 | 159 | @router.put("/publishers/{id}", response=PublisherSchema) 160 | def update_publisher(request, id: int, payload: PublisherSchemaIn): 161 | publisher = get_object_or_404(Publisher, id=id) 162 | for attr, value in payload.dict().items(): 163 | setattr(publisher, attr, value) 164 | publisher.save() 165 | return publisher 166 | 167 | 168 | @router.delete("/publishers/{id}", response={204: None}) 169 | def delete_publisher(request, id: int): 170 | publisher = get_object_or_404(Publisher, id=id) 171 | publisher.delete() 172 | return 204, None 173 | -------------------------------------------------------------------------------- /passo-a-passo.md: -------------------------------------------------------------------------------- 1 | # Passo a passo 2 | 3 | ## Criando virtualenv 4 | 5 | ``` 6 | python -m venv .venv 7 | source .venv/bin/activate 8 | ``` 9 | 10 | 11 | ## Instalando os pacotes 12 | 13 | ``` 14 | pip install dr_scaffold djangorestframework 15 | 16 | pip freeze | grep 'Django\|djangorestframework\|dr-scaffold' >> requirements.txt 17 | ``` 18 | 19 | 20 | ``` 21 | pip install django-extensions python-decouple django-seed 22 | 23 | pip freeze | grep 'django-extensions\|python-decouple\|django-seed\|faker' >> requirements.txt 24 | ``` 25 | 26 | ## Criando o projeto 27 | 28 | `django-admin startproject backend .` 29 | 30 | > Editar `settings.py` e incluir `dr_scaffold` em `INSTALLED_APPS`. 31 | 32 | ```python 33 | from pathlib import Path 34 | 35 | from decouple import Csv, config 36 | 37 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 38 | BASE_DIR = Path(__file__).resolve().parent.parent 39 | 40 | # Quick-start development settings - unsuitable for production 41 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 42 | 43 | # SECURITY WARNING: keep the secret key used in production secret! 44 | SECRET_KEY = config('SECRET_KEY') 45 | 46 | # SECURITY WARNING: don't run with debug turned on in production! 47 | DEBUG = config('DEBUG', default=False, cast=bool) 48 | 49 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) 50 | 51 | INSTALLED_APPS = [ 52 | 'django.contrib.admin', 53 | 'django.contrib.auth', 54 | 'django.contrib.contenttypes', 55 | 'django.contrib.sessions', 56 | 'django.contrib.messages', 57 | 'django.contrib.staticfiles', 58 | # 3rd apps 59 | 'dr_scaffold', 60 | 'rest_framework', 61 | 'django_extensions', 62 | 'django_seed', 63 | # my apps 64 | ] 65 | 66 | 67 | LANGUAGE_CODE = 'pt-br' 68 | 69 | TIME_ZONE = 'America/Sao_Paulo' 70 | 71 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 72 | ``` 73 | 74 | #### contrib 75 | 76 | https://github.com/rg3915/bookstore/blob/main/contrib/env_gen.py 77 | 78 | ``` 79 | mkdir contrib 80 | touch contrib/env_gen.py 81 | python contrib/env_gen.py 82 | ``` 83 | 84 | 85 | ``` 86 | python manage.py dr_scaffold bookstore Author name:charfield 87 | 88 | python manage.py dr_scaffold bookstore Publisher name:charfield score:positiveintegerfield 89 | 90 | python manage.py dr_scaffold bookstore Book \ 91 | name:charfield \ 92 | isbn:charfield \ 93 | rating:decimalfield \ 94 | authors:manytomany:Author \ 95 | publisher:foreignkey:Publisher \ 96 | price:decimalfield \ 97 | stock:integerfield 98 | ``` 99 | 100 | 101 | > Editar `models.py` 102 | 103 | ```python 104 | from django.db import models 105 | 106 | 107 | class Author(models.Model): 108 | name = models.CharField(max_length=50, unique=True) 109 | 110 | def __str__(self): 111 | return self.name 112 | 113 | class Meta: 114 | ordering = ('name',) 115 | verbose_name = 'Autor' 116 | verbose_name_plural = 'Autores' 117 | 118 | 119 | class Publisher(models.Model): 120 | name = models.CharField(max_length=50, unique=True) 121 | score = models.PositiveIntegerField(default=0) 122 | 123 | def __str__(self): 124 | return self.name 125 | 126 | class Meta: 127 | ordering = ('name',) 128 | verbose_name = 'Editora' 129 | verbose_name_plural = 'Editoras' 130 | 131 | 132 | class Book(models.Model): 133 | name = models.CharField(max_length=100) 134 | isbn = models.CharField(max_length=13) 135 | rating = models.DecimalField(max_digits=5, decimal_places=2, null=True, default=0.0) 136 | authors = models.ManyToManyField(Author) 137 | publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, null=True) 138 | price = models.DecimalField(max_digits=5, decimal_places=2, null=True, default=0.0) 139 | stock = models.IntegerField(default=0) 140 | created = models.DateTimeField(auto_now_add=True) 141 | 142 | def __str__(self): 143 | return self.name 144 | 145 | class Meta: 146 | ordering = ('name',) 147 | verbose_name = 'Livro' 148 | verbose_name_plural = 'Livros' 149 | 150 | ``` 151 | 152 | #### Rode as migrações 153 | 154 | > Adicione `bookstore` em `INSTALLED_APPS`. 155 | 156 | ``` 157 | python manage.py makemigrations 158 | python manage.py migrate 159 | python manage.py createsuperuser 160 | ``` 161 | 162 | 163 | ``` 164 | python manage.py seed bookstore --number=160 165 | ``` 166 | 167 | 168 | ``` 169 | pip install psycopg2-binary 170 | 171 | pip freeze | grep psycopg2-binary >> requirements.txt 172 | ``` 173 | 174 | 175 | ``` 176 | python manage.py runserver 177 | ``` 178 | 179 | 180 | > Editar `admin.py` 181 | 182 | ```python 183 | from django.contrib import admin 184 | 185 | from bookstore.models import Author, Book, Publisher 186 | 187 | 188 | @admin.register(Author) 189 | class AuthorAdmin(admin.ModelAdmin): 190 | list_display = ('__str__',) 191 | search_fields = ('name',) 192 | 193 | 194 | @admin.register(Publisher) 195 | class PublisherAdmin(admin.ModelAdmin): 196 | list_display = ('__str__', 'score') 197 | search_fields = ('name',) 198 | 199 | 200 | @admin.register(Book) 201 | class BookAdmin(admin.ModelAdmin): 202 | list_display = ('__str__', 'isbn', 'rating', 'price', 'stock') 203 | search_fields = ('name', 'isbn', 'authors__name', 'publisher__name') 204 | 205 | ``` 206 | 207 | > Abrir `shell_plus` 208 | 209 | `python manage.py shell_plus` 210 | 211 | 212 | ```python 213 | import string 214 | from random import choice, randint, random 215 | 216 | from faker import Faker 217 | 218 | faker = Faker('pt-br') 219 | 220 | def gen_digits(max_length): 221 | return str(''.join(choice(string.digits) for i in range(max_length))) 222 | 223 | def gen_rating(): 224 | return random() + choice((3, 4)) 225 | 226 | def gen_price(): 227 | return random() + randint(10, 100) 228 | 229 | def gen_name(): 230 | return faker.name() 231 | 232 | books = Book.objects.all() 233 | for book in books: 234 | book.isbn = '978' + gen_digits(10) 235 | book.rating = gen_rating() 236 | book.price = gen_price() 237 | 238 | Book.objects.bulk_update(books, ['isbn', 'rating', 'price']) 239 | 240 | authors = Author.objects.all() 241 | for _ in authors: 242 | Author.objects.update_or_create(name=gen_name()) 243 | ``` 244 | 245 | 246 | > Crie uma pasta `api_drf` e mova `serializers.py` e `viewsets.py` para lá. 247 | 248 | 249 | ``` 250 | cd bookstore 251 | mkdir api_drf 252 | mv serializers.py api_drf 253 | mv views.py api_drf/viewsets.py 254 | cd .. 255 | ``` 256 | 257 | 258 | > Editar `backend/urls.py` 259 | 260 | ```python 261 | from django.contrib import admin 262 | from django.urls import include, path 263 | 264 | 265 | urlpatterns = [ 266 | path('', include('bookstore.urls', namespace='bookstore')), 267 | path('admin/', admin.site.urls), 268 | ] 269 | ``` 270 | 271 | > Editar `bookstore/urls.py` 272 | 273 | ```python 274 | from bookstore.api_drf.viewsets import AuthorViewSet, BookViewSet, PublisherViewSet 275 | 276 | app_name = 'bookstore' 277 | 278 | urlpatterns = [ 279 | path('api/v1/bookstore/', include(router.urls)), 280 | ] 281 | ``` 282 | 283 | > Editar `bookstore/api/viewsets.py` 284 | 285 | ```python 286 | from bookstore.api_drf.serializers import ... 287 | 288 | ``` 289 | 290 | > Editar `bookstore/serializers.py` 291 | 292 | ```python 293 | from rest_framework import serializers 294 | 295 | from bookstore.models import Author, Book, Publisher 296 | 297 | 298 | class AuthorSerializer(serializers.ModelSerializer): 299 | 300 | class Meta: 301 | model = Author 302 | fields = '__all__' 303 | 304 | 305 | class PublisherSerializer(serializers.ModelSerializer): 306 | 307 | class Meta: 308 | model = Publisher 309 | fields = '__all__' 310 | 311 | 312 | class BookSerializer(serializers.ModelSerializer): 313 | authors = AuthorSerializer(many=True) 314 | publisher = PublisherSerializer() 315 | 316 | class Meta: 317 | model = Book 318 | fields = '__all__' 319 | 320 | ``` 321 | 322 | ## Django Ninja 323 | 324 | ``` 325 | pip install django-ninja 326 | 327 | pip freeze | grep django-ninja >> requirements.txt 328 | ``` 329 | 330 | ``` 331 | touch backend/api.py 332 | touch bookstore/api.py 333 | ``` 334 | 335 | > Editar `backend/api.py` 336 | 337 | ```python 338 | from ninja import NinjaAPI 339 | 340 | from bookstore.api import router as bookstore_router 341 | 342 | api = NinjaAPI() 343 | 344 | api.add_router('/bookstore/', bookstore_router) 345 | 346 | ``` 347 | 348 | > Editar `bookstore/api.py` primeiro `books` 349 | 350 | ```python 351 | from typing import List 352 | 353 | from django.shortcuts import get_object_or_404 354 | from ninja import Router, Schema 355 | from ninja.orm import create_schema 356 | 357 | from .models import Author, Book, Publisher 358 | 359 | router = Router() 360 | 361 | BookSchema = create_schema(Book, depth=1) 362 | 363 | 364 | class BookSchemaIn(Schema): 365 | name: str 366 | isbn: str 367 | rating: float 368 | publisher_id: int 369 | price: float 370 | stock: int 371 | authors: List[int] 372 | 373 | 374 | @router.get("/books", response=List[BookSchema]) 375 | def list_books(request): 376 | qs = Book.objects.all() 377 | return qs 378 | 379 | 380 | @router.get("/books/{id}", response=BookSchema) 381 | def get_book(request, id: int): 382 | book = get_object_or_404(Book, id=id) 383 | return book 384 | 385 | 386 | @router.post("/books", response={201: BookSchema}) 387 | def create_book(request, payload: BookSchemaIn): 388 | # Get params 389 | authors = payload.dict().pop('authors') 390 | publisher_id = payload.dict().pop('publisher_id') 391 | 392 | # Instance models 393 | # Get publisher 394 | publisher = get_object_or_404(Publisher, id=publisher_id) 395 | 396 | # Mount dict data 397 | data = {} 398 | for k, v in payload.dict().items(): 399 | data[k] = v 400 | 401 | data['publisher'] = publisher 402 | # Necessário porque é uma lista, mas não pode ser salvo diretamente. 403 | data.pop('authors', None) 404 | 405 | # Save data 406 | book = Book.objects.create(**data) 407 | 408 | # Add authors 409 | for author in authors: 410 | # Get authors 411 | author_obj = get_object_or_404(Author, id=author) 412 | book.authors.add(author_obj) 413 | 414 | return 201, book 415 | 416 | 417 | @router.put("/books/{id}", response=BookSchema) 418 | def update_book(request, id: int, payload: BookSchemaIn): 419 | book = get_object_or_404(Book, id=id) 420 | for attr, value in payload.dict().items(): 421 | if attr != 'authors': 422 | setattr(book, attr, value) 423 | book.save() 424 | 425 | authors = payload.dict().pop('authors') 426 | 427 | # Remove all authors 428 | book.authors.clear() 429 | 430 | # Add authors 431 | for author in authors: 432 | # Get authors 433 | author_obj = get_object_or_404(Author, id=author) 434 | book.authors.add(author_obj) 435 | 436 | return book 437 | 438 | 439 | @router.delete("/books/{id}", response={204: None}) 440 | def delete_book(request, id: int): 441 | book = get_object_or_404(Book, id=id) 442 | book.delete() 443 | return 204, None 444 | 445 | ``` 446 | 447 | > depois `authors, publishers` 448 | 449 | ```python 450 | AuthorSchema = create_schema(Author) 451 | PublisherSchema = create_schema(Publisher) 452 | 453 | 454 | class AuthorSchemaIn(Schema): 455 | name: str 456 | 457 | 458 | class PublisherSchemaIn(Schema): 459 | name: str 460 | score: int 461 | 462 | @router.get("/authors", response=List[AuthorSchema]) 463 | def list_authors(request): 464 | qs = Author.objects.all() 465 | return qs 466 | 467 | 468 | @router.get("/authors/{id}", response=AuthorSchema) 469 | def get_author(request, id: int): 470 | author = get_object_or_404(Author, id=id) 471 | return author 472 | 473 | 474 | @router.post("/authors", response={201: AuthorSchema}) 475 | def create_author(request, payload: AuthorSchemaIn): 476 | author = Author.objects.create(**payload.dict()) 477 | return 201, author 478 | 479 | 480 | @router.put("/authors/{id}", response=AuthorSchema) 481 | def update_author(request, id: int, payload: AuthorSchemaIn): 482 | author = get_object_or_404(Author, id=id) 483 | for attr, value in payload.dict().items(): 484 | setattr(author, attr, value) 485 | author.save() 486 | return author 487 | 488 | 489 | @router.delete("/authors/{id}", response={204: None}) 490 | def delete_author(request, id: int): 491 | author = get_object_or_404(Author, id=id) 492 | author.delete() 493 | return 204, None 494 | 495 | 496 | @router.get("/publishers", response=List[PublisherSchema]) 497 | def list_publishers(request): 498 | qs = Publisher.objects.all() 499 | return qs 500 | 501 | 502 | @router.get("/publishers/{id}", response=PublisherSchema) 503 | def get_publisher(request, id: int): 504 | publisher = get_object_or_404(Publisher, id=id) 505 | return publisher 506 | 507 | 508 | @router.post("/publishers", response={201: PublisherSchema}) 509 | def create_publisher(request, payload: PublisherSchemaIn): 510 | publisher = Publisher.objects.create(**payload.dict()) 511 | return 201, publisher 512 | 513 | 514 | @router.put("/publishers/{id}", response=PublisherSchema) 515 | def update_publisher(request, id: int, payload: PublisherSchemaIn): 516 | publisher = get_object_or_404(Publisher, id=id) 517 | for attr, value in payload.dict().items(): 518 | setattr(publisher, attr, value) 519 | publisher.save() 520 | return publisher 521 | 522 | 523 | @router.delete("/publishers/{id}", response={204: None}) 524 | def delete_publisher(request, id: int): 525 | publisher = get_object_or_404(Publisher, id=id) 526 | publisher.delete() 527 | return 204, None 528 | 529 | ``` 530 | 531 | > Editar `backend/urls.py` 532 | 533 | ```python 534 | from .api import api 535 | 536 | ... 537 | path('api/v2/', api.urls), 538 | ... 539 | 540 | ``` 541 | 542 | 543 | ## Templates 544 | 545 | ``` 546 | python manage.py startapp core 547 | rm -f core/{admin,models,tests}.py 548 | rm -rf core/migrations 549 | touch core/urls.py 550 | ``` 551 | 552 | 553 | > Editar `settings.py` 554 | 555 | ```python 556 | INSTALLED_APPS = [ 557 | ... 558 | 'core', 559 | ... 560 | ``` 561 | > Editar `backend/urls.py` 562 | 563 | ```python 564 | path('', include('core.urls')), 565 | ``` 566 | 567 | > Editar `core/urls.py` 568 | 569 | ```python 570 | from django.urls import path 571 | 572 | from .views import index 573 | 574 | app_name = 'core' 575 | 576 | urlpatterns = [ 577 | path('', index, name='index'), 578 | ] 579 | ``` 580 | 581 | > Editar `core/views.py` 582 | 583 | ```python 584 | from django.shortcuts import render 585 | 586 | 587 | def index(request): 588 | template_name = 'index.html' 589 | return render(request, template_name) 590 | 591 | ``` 592 | 593 | ``` 594 | mkdir -p core/static/css 595 | touch core/static/css/style.css 596 | 597 | mkdir -p core/templates/includes 598 | touch core/templates/{base,index}.html 599 | touch core/templates/includes/{nav,pagination}.html 600 | # copiar o conteúdo 601 | 602 | mkdir -p bookstore/templates/bookstore 603 | touch bookstore/templates/bookstore/{book,author,publisher}_{list,detail,form,confirm_delete}.html 604 | 605 | touch bookstore/{forms,views}.py 606 | ``` 607 | 608 | 609 | > Editar `bookstore/urls.py` 610 | 611 | ```python 612 | from django.urls import include, path 613 | from rest_framework import routers 614 | 615 | from bookstore import views as v 616 | from bookstore.api_drf.viewsets import ( 617 | AuthorViewSet, 618 | BookViewSet, 619 | PublisherViewSet 620 | ) 621 | 622 | app_name = 'bookstore' 623 | 624 | router = routers.DefaultRouter() 625 | 626 | router.register(r'authors', AuthorViewSet) 627 | router.register(r'publishers', PublisherViewSet) 628 | router.register(r'books', BookViewSet) 629 | 630 | authors_urlpatterns = [ 631 | path('', v.AuthorListView.as_view(), name='author_list'), 632 | path('/', v.AuthorDetailView.as_view(), name='author_detail'), 633 | path('create/', v.AuthorCreateView.as_view(), name='author_create'), 634 | path('/update/', v.AuthorUpdateView.as_view(), name='author_update'), 635 | path('/delete/', v.AuthorDeleteView.as_view(), name='author_delete'), 636 | ] 637 | 638 | publishers_urlpatterns = [ 639 | path('', v.PublisherListView.as_view(), name='publisher_list'), 640 | path('/', v.PublisherDetailView.as_view(), name='publisher_detail'), 641 | path('create/', v.PublisherCreateView.as_view(), name='publisher_create'), 642 | path('/update/', v.PublisherUpdateView.as_view(), name='publisher_update'), 643 | path('/delete/', v.PublisherDeleteView.as_view(), name='publisher_delete'), 644 | ] 645 | 646 | books_urlpatterns = [ 647 | path('', v.BookListView.as_view(), name='book_list'), 648 | path('/', v.BookDetailView.as_view(), name='book_detail'), 649 | path('create/', v.BookCreateView.as_view(), name='book_create'), 650 | path('/update/', v.BookUpdateView.as_view(), name='book_update'), 651 | path('/delete/', v.BookDeleteView.as_view(), name='book_delete'), 652 | ] 653 | 654 | urlpatterns = [ 655 | path('api/v1/bookstore/', include(router.urls)), 656 | path('author/', include(authors_urlpatterns)), 657 | path('publisher/', include(publishers_urlpatterns)), 658 | path('book/', include(books_urlpatterns)), 659 | ] 660 | 661 | ``` 662 | 663 | > Editar `bookstore/views.py` 664 | 665 | ```python 666 | from django.urls import reverse_lazy 667 | from django.views.generic import ( 668 | CreateView, 669 | DeleteView, 670 | DetailView, 671 | ListView, 672 | UpdateView 673 | ) 674 | 675 | from .forms import AuthorForm, BookForm, PublisherForm 676 | from .models import Author, Book, Publisher 677 | 678 | 679 | class AuthorListView(ListView): 680 | model = Author 681 | paginate_by = 20 682 | 683 | 684 | class AuthorDetailView(DetailView): 685 | model = Author 686 | 687 | 688 | class AuthorCreateView(CreateView): 689 | model = Author 690 | form_class = AuthorForm 691 | 692 | 693 | class AuthorUpdateView(UpdateView): 694 | model = Author 695 | form_class = AuthorForm 696 | 697 | 698 | class AuthorDeleteView(DeleteView): 699 | model = Author 700 | success_url = reverse_lazy('bookstore:author_list') 701 | 702 | 703 | class PublisherListView(ListView): 704 | model = Publisher 705 | paginate_by = 20 706 | 707 | 708 | class PublisherDetailView(DetailView): 709 | model = Publisher 710 | 711 | 712 | class PublisherCreateView(CreateView): 713 | model = Publisher 714 | form_class = PublisherForm 715 | 716 | 717 | class PublisherUpdateView(UpdateView): 718 | model = Publisher 719 | form_class = PublisherForm 720 | 721 | 722 | class PublisherDeleteView(DeleteView): 723 | model = Publisher 724 | success_url = reverse_lazy('bookstore:publisher_list') 725 | 726 | 727 | class BookListView(ListView): 728 | model = Book 729 | paginate_by = 20 730 | 731 | 732 | class BookDetailView(DetailView): 733 | model = Book 734 | 735 | 736 | class BookCreateView(CreateView): 737 | model = Book 738 | form_class = BookForm 739 | 740 | 741 | class BookUpdateView(UpdateView): 742 | model = Book 743 | form_class = BookForm 744 | 745 | 746 | class BookDeleteView(DeleteView): 747 | model = Book 748 | success_url = reverse_lazy('bookstore:book_list') 749 | 750 | ``` 751 | 752 | 753 | > Editar `bookstore/forms.py` 754 | 755 | ```python 756 | from django import forms 757 | 758 | from .models import Author, Book, Publisher 759 | 760 | 761 | class _BaseModelForm(forms.ModelForm): 762 | required_css_class = 'required' 763 | 764 | def __init__(self, *args, **kwargs): 765 | super(_BaseModelForm, self).__init__(*args, **kwargs) 766 | for field_name, field in self.fields.items(): 767 | field.widget.attrs['class'] = 'form-control' 768 | 769 | 770 | class AuthorForm(_BaseModelForm): 771 | 772 | class Meta: 773 | model = Author 774 | fields = '__all__' 775 | 776 | 777 | class PublisherForm(_BaseModelForm): 778 | 779 | class Meta: 780 | model = Publisher 781 | fields = '__all__' 782 | 783 | 784 | class BookForm(_BaseModelForm): 785 | 786 | class Meta: 787 | model = Book 788 | fields = '__all__' 789 | ``` 790 | 791 | > Editar `bookstore/models.py` 792 | 793 | ```python 794 | class Author(models.Model): 795 | 796 | def get_absolute_url(self): 797 | return reverse_lazy('bookstore:author_detail', kwargs={'pk': self.pk}) 798 | 799 | 800 | class Publisher(models.Model): 801 | 802 | def get_absolute_url(self): 803 | return reverse_lazy('bookstore:publisher_detail', kwargs={'pk': self.pk}) 804 | 805 | 806 | class Book(models.Model): 807 | 808 | def get_absolute_url(self): 809 | return reverse_lazy('bookstore:book_detail', kwargs={'pk': self.pk}) 810 | 811 | ``` 812 | 813 | > Editar os templates 814 | 815 | --------------------------------------------------------------------------------