├── backend ├── __init__.py ├── crm │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── urls.py │ ├── admin.py │ ├── models.py │ └── api │ │ ├── serializers.py │ │ └── viewsets.py ├── core │ ├── __init__.py │ ├── static │ │ ├── js │ │ │ └── main.js │ │ └── css │ │ │ └── style.css │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── create_data.py │ ├── migrations │ │ └── __init__.py │ ├── templatetags │ │ ├── __init__.py │ │ └── url_replace.py │ ├── models.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── urls.py │ ├── templates │ │ ├── index.html │ │ ├── base.html │ │ └── includes │ │ │ ├── nav.html │ │ │ └── pagination.html │ ├── admin.py │ └── handler.py ├── order │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── urls.py │ ├── admin.py │ ├── templates │ │ └── order │ │ │ ├── order_form.html │ │ │ └── order_list.html │ ├── views.py │ ├── forms.py │ └── models.py ├── todo │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_todo_description.py │ │ ├── 0004_alter_todo_description_alter_todo_task.py │ │ ├── 0001_initial.py │ │ └── 0002_todo_created_by_todo_status.py │ ├── tests.py │ ├── apps.py │ ├── api │ │ ├── serializers.py │ │ └── viewsets.py │ ├── admin.py │ ├── templates │ │ └── todo │ │ │ ├── todo_form.html │ │ │ ├── todo_detail.html │ │ │ ├── todo_confirm_delete.html │ │ │ └── todo_list.html │ ├── views.py │ ├── urls.py │ ├── models.py │ └── forms.py ├── video │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── forms.py │ ├── admin.py │ ├── urls.py │ ├── models.py │ ├── views.py │ └── tests.py ├── example │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── viewsets.py │ ├── models.py │ └── urls.py ├── hotel │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── urls.py │ ├── api │ │ ├── viewsets.py │ │ └── serializers.py │ ├── admin.py │ └── models.py ├── movie │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_movie_title.py │ │ ├── 0006_movie_censure.py │ │ ├── 0003_movie_category.py │ │ ├── 0004_rename_create_date_category_created_and_more.py │ │ ├── 0005_alter_category_title_alter_movie_sinopse_and_more.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── admin.py │ ├── urls.py │ ├── models.py │ └── api │ │ ├── viewsets.py │ │ └── serializers.py ├── school │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_grade.py │ │ ├── 0003_class.py │ │ ├── 0004_alter_class_classroom_alter_class_teacher_and_more.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── urls.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── viewsets.py │ └── models.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── _config.yml ├── .flake8 ├── img ├── db01.png ├── db02.png ├── pgadmin.png ├── youtube.png ├── diagrama01.png ├── diagrama02.png ├── portainer.png ├── docker-compose.png └── grupos_permissoes.png ├── Makefile ├── requirements.txt ├── manage.py ├── client.py ├── passo-a-passo ├── 10_reescrevendo_admin_user.md ├── 13_drf_fix_permissao.md ├── 12_drf_editando_mensagens_erro.md ├── 24_docker_popos.md ├── 08_drf_salvando_dados_extra.md ├── 17_novo_comando.md ├── 14_grupos_permissoes.md ├── 19_readonly_validation.md ├── 21_chain_list.md ├── 23_modelchoice.md ├── 15_drf_entendendo_validacoes.md ├── 05_drf_serializers_mais_rapido.md ├── 07_drf_apiview_get_extra_actions.md ├── 20_marvel_api.md ├── 03_drf_entendendo_rotas.md ├── 22_postgresql_docker.md ├── 09_drf_entendendo_autenticacao.md ├── 11_drf_entendendo_permissoes.md ├── 01_django_full_template_como_criar_um_projeto_django_completo_api_rest_render_template.md ├── 02_criando_api_com_django_sem_drf_parte2.md └── 06_drf_entendendo_viewsets.md ├── contrib └── env_gen.py ├── docker-compose.yml ├── service.py ├── README.md ├── .gitignore └── index.md /backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/crm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/todo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/video/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/static/js/main.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /backend/core/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/crm/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/example/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/hotel/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/movie/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/order/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/school/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/todo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/video/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/core/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max_line_length = 120 3 | ignore = E501 4 | -------------------------------------------------------------------------------- /img/db01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/db01.png -------------------------------------------------------------------------------- /img/db02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/db02.png -------------------------------------------------------------------------------- /backend/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/crm/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/todo/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /img/pgadmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/pgadmin.png -------------------------------------------------------------------------------- /img/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/youtube.png -------------------------------------------------------------------------------- /backend/crm/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /backend/order/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /img/diagrama01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/diagrama01.png -------------------------------------------------------------------------------- /img/diagrama02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/diagrama02.png -------------------------------------------------------------------------------- /img/portainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/portainer.png -------------------------------------------------------------------------------- /img/docker-compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/docker-compose.png -------------------------------------------------------------------------------- /img/grupos_permissoes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-experience/main/img/grupos_permissoes.png -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 = 'backend.core' 7 | -------------------------------------------------------------------------------- /backend/crm/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CrmConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.crm' 7 | -------------------------------------------------------------------------------- /backend/todo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodoConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.todo' 7 | -------------------------------------------------------------------------------- /backend/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/hotel/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HotelConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.hotel' 7 | -------------------------------------------------------------------------------- /backend/movie/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MovieConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.movie' 7 | -------------------------------------------------------------------------------- /backend/order/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrderConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.order' 7 | -------------------------------------------------------------------------------- /backend/school/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SchoolConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.school' 7 | -------------------------------------------------------------------------------- /backend/video/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VideoConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.video' 7 | -------------------------------------------------------------------------------- /backend/example/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExampleConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.example' 7 | -------------------------------------------------------------------------------- /backend/example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from backend.example.models import Example 4 | 5 | 6 | @admin.register(Example) 7 | class ExampleAdmin(admin.ModelAdmin): 8 | exclude = () 9 | -------------------------------------------------------------------------------- /backend/video/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Video 4 | 5 | 6 | class VideoForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = Video 10 | fields = ('title', 'release_year') 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | indenter: 2 | find backend -name "*.html" | xargs djhtml -t 2 -i 3 | 4 | autopep8: 5 | find backend -name "*.py" | xargs autopep8 --max-line-length 120 --in-place 6 | 7 | isort: 8 | isort -m 3 * 9 | 10 | lint: autopep8 isort indenter 11 | -------------------------------------------------------------------------------- /backend/video/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Video 4 | 5 | 6 | @admin.register(Video) 7 | class VideoAdmin(admin.ModelAdmin): 8 | list_display = ('__str__', 'release_year') 9 | search_fields = ('title',) 10 | -------------------------------------------------------------------------------- /backend/example/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.example.models import Example 4 | 5 | 6 | class ExampleSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Example 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /backend/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 | .ok { 11 | color: green; 12 | } 13 | 14 | .no { 15 | color: red; 16 | } 17 | 18 | .errorlist { 19 | color: red; 20 | } -------------------------------------------------------------------------------- /backend/order/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.order import views as v 4 | 5 | app_name = 'order' 6 | 7 | 8 | urlpatterns = [ 9 | path('order/', v.order_list, name='order_list'), 10 | path('order/create/', v.order_create, name='order_create'), 11 | ] 12 | -------------------------------------------------------------------------------- /backend/todo/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.todo.models import Todo 4 | 5 | 6 | class TodoSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Todo 10 | fields = '__all__' 11 | depth = 1 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8==1.6.0 2 | django-extensions==3.1.* 3 | django-filter==21.1 4 | django-seed==0.3.* 5 | Django==4.0.* 6 | djangorestframework==3.12.* 7 | djhtml==1.4.11 8 | djoser==2.1.0 9 | dr-scaffold==2.1.* 10 | drf-yasg==1.20.* 11 | ipdb 12 | psycopg2-binary==2.9.* 13 | python-decouple==3.5 14 | pytz 15 | -------------------------------------------------------------------------------- /backend/todo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from backend.todo.models import Todo 4 | 5 | 6 | @admin.register(Todo) 7 | class TodoAdmin(admin.ModelAdmin): 8 | list_display = ('__str__', 'is_done') 9 | search_fields = ('task',) 10 | list_filter = ('is_done',) 11 | date_hierarchy = 'created' 12 | -------------------------------------------------------------------------------- /backend/core/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |

Django Experience

7 | github.com/rg3915/django-experience 8 |
9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /backend/video/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from backend.video import views as v 4 | 5 | app_name = 'video' 6 | 7 | v1_urlpatterns = [ 8 | path('videos/', v.videos, name='videos'), 9 | path('videos//', v.video, name='video'), 10 | ] 11 | 12 | urlpatterns = [ 13 | path('api/v1/', include(v1_urlpatterns)), 14 | ] 15 | -------------------------------------------------------------------------------- /backend/hotel/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from backend.hotel.api.viewsets import HotelViewSet 5 | 6 | app_name = 'hotel' 7 | 8 | router = routers.DefaultRouter() 9 | 10 | router.register(r'hotels', HotelViewSet, basename='hotel') 11 | 12 | urlpatterns = [ 13 | path('api/v1/', include(router.urls)), 14 | ] 15 | -------------------------------------------------------------------------------- /backend/core/templatetags/url_replace.py: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/a/62587351/802542 2 | from django import template 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag(takes_context=True) 8 | def url_replace(context, **kwargs): 9 | query = context['request'].GET.copy() 10 | query.pop('page', None) 11 | query.update(kwargs) 12 | return query.urlencode() 13 | -------------------------------------------------------------------------------- /backend/hotel/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.permissions import AllowAny 3 | 4 | from backend.hotel.api.serializers import HotelSerializer 5 | from backend.hotel.models import Hotel 6 | 7 | 8 | class HotelViewSet(viewsets.ModelViewSet): 9 | queryset = Hotel.objects.all() 10 | serializer_class = HotelSerializer 11 | permission_classes = (AllowAny,) 12 | -------------------------------------------------------------------------------- /backend/hotel/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from backend.hotel.models import Hotel 4 | 5 | 6 | @admin.register(Hotel) 7 | class HotelAdmin(admin.ModelAdmin): 8 | list_display = ( 9 | '__str__', 10 | 'start_date', 11 | 'end_date', 12 | 'created' 13 | ) 14 | search_fields = ('name',) 15 | date_hierarchy = 'created' 16 | ordering = ('-created',) 17 | -------------------------------------------------------------------------------- /backend/example/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Example(models.Model): 5 | title = models.CharField(max_length=255, null=True, blank=True) 6 | created = models.DateTimeField(auto_now_add=True) 7 | updated = models.DateTimeField(auto_now=True) 8 | 9 | def __str__(self): 10 | return f"{self.title}" 11 | 12 | class Meta: 13 | verbose_name_plural = "Examples" 14 | -------------------------------------------------------------------------------- /backend/order/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Department, Employee, Order 4 | 5 | 6 | @admin.register(Employee) 7 | class EmployeeAdmin(admin.ModelAdmin): 8 | list_display = ('__str__', 'department') 9 | 10 | 11 | @admin.register(Order) 12 | class OrderAdmin(admin.ModelAdmin): 13 | list_display = ('__str__', 'employee') 14 | 15 | 16 | admin.site.register(Department) 17 | -------------------------------------------------------------------------------- /backend/todo/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from backend.todo.api.serializers import TodoSerializer 4 | from backend.todo.models import Todo 5 | 6 | 7 | class TodoViewSet(viewsets.ModelViewSet): 8 | queryset = Todo.objects.all() 9 | serializer_class = TodoSerializer 10 | 11 | def perform_create(self, serializer): 12 | serializer.save(created_by=self.request.user, status='a') 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/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', '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/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/crm/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from backend.crm.api.viewsets import ComissionViewSet, CustomerViewSet 5 | 6 | app_name = 'crm' 7 | 8 | router = routers.DefaultRouter() 9 | 10 | router.register(r'comissions', ComissionViewSet, basename='comission') 11 | router.register(r'customers', CustomerViewSet, basename='customer') 12 | 13 | urlpatterns = [ 14 | path('api/v1/', include(router.urls)), 15 | ] 16 | -------------------------------------------------------------------------------- /backend/hotel/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Hotel(models.Model): 5 | name = models.CharField(max_length=32) 6 | start_date = models.DateField(null=True, blank=True) 7 | end_date = models.DateField(null=True, blank=True) 8 | created = models.DateTimeField(auto_now_add=True) 9 | 10 | def __str__(self): 11 | return f'{self.name}' 12 | 13 | class Meta: 14 | verbose_name = 'Hotel' 15 | verbose_name_plural = 'Hotéis' 16 | -------------------------------------------------------------------------------- /backend/hotel/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.hotel.models import Hotel 4 | 5 | 6 | class HotelSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Hotel 10 | fields = '__all__' 11 | 12 | def validate(self, data): 13 | if data['start_date'] > data['end_date']: 14 | raise serializers.ValidationError('A data inicial deve ser anterior ou igual a data final!') 15 | return data 16 | -------------------------------------------------------------------------------- /backend/movie/migrations/0002_alter_movie_title.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-12 23:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('movie', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='movie', 15 | name='title', 16 | field=models.CharField(blank=True, max_length=30, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/movie/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from backend.movie.models import Category, Movie 4 | 5 | 6 | @admin.register(Category) 7 | class CategoryAdmin(admin.ModelAdmin): 8 | list_display = ('__str__',) 9 | search_fields = ('title',) 10 | 11 | 12 | @admin.register(Movie) 13 | class MovieAdmin(admin.ModelAdmin): 14 | list_display = ('__str__', 'censure', 'rating', 'like') 15 | search_fields = ('title', 'sinopse', 'rating', 'like') 16 | list_filter = ('like', 'category') 17 | -------------------------------------------------------------------------------- /backend/todo/migrations/0003_todo_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-20 01:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todo', '0002_todo_created_by_todo_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='todo', 15 | name='description', 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/todo/templates/todo/todo_form.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

Tarefa

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

Adicionar Pedido

6 |
7 |
8 |
9 | {% csrf_token %} 10 | {{ form.as_p }} 11 |
12 | 13 |
14 |
15 |
16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /backend/movie/migrations/0006_movie_censure.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | # Generated by Django 4.0 on 2022-03-02 05:51 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('movie', '0005_alter_category_title_alter_movie_sinopse_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='movie', 15 | name='censure', 16 | field=models.PositiveIntegerField(default=14), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/movie/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from backend.movie.api.viewsets import ( 5 | CategoryViewSet, 6 | MovieExampleView, 7 | MovieViewSet 8 | ) 9 | 10 | app_name = 'movie' 11 | 12 | router = routers.DefaultRouter() 13 | 14 | router.register(r'movies', MovieViewSet, basename='movie') 15 | router.register(r'categories', CategoryViewSet, basename='category') 16 | 17 | urlpatterns = [ 18 | path('api/v1/', include(router.urls)), 19 | path('api/v1/movie-examples/', MovieExampleView.as_view()), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | # from django.urls import include 3 | from rest_framework import routers 4 | 5 | # from backend.example.api.viewsets import ExampleViewSet 6 | from backend.example.api.viewsets import ExampleView 7 | 8 | app_name = 'example' 9 | 10 | router = routers.DefaultRouter() 11 | 12 | # router.register(r'examples', ExampleViewSet, basename='example') 13 | # router.register(r'examples', ExampleView, basename='example') 14 | 15 | urlpatterns = [ 16 | # path('api/v1/', include(router.urls)), 17 | path('api/v1/examples/', ExampleView.as_view()), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/todo/templates/todo/todo_detail.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

Detalhes

6 | 7 | 19 | {% endblock content %} 20 | -------------------------------------------------------------------------------- /backend/todo/templates/todo/todo_confirm_delete.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

Deletar

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

Deseja deletar {{ object }} ?

11 |
12 | 13 | Não 14 |
15 |
16 |
17 |
18 | {% endblock content %} -------------------------------------------------------------------------------- /backend/video/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Video(models.Model): 5 | title = models.CharField('título', max_length=50, unique=True) 6 | release_year = models.PositiveIntegerField('lançamento', null=True, blank=True) 7 | 8 | class Meta: 9 | ordering = ('id',) 10 | verbose_name = 'filme' 11 | verbose_name_plural = 'filmes' 12 | 13 | def __str__(self): 14 | return f'{self.title}' 15 | 16 | def to_dict(self): 17 | return { 18 | 'id': self.id, 19 | 'title': self.title, 20 | 'release_year': self.release_year, 21 | } 22 | -------------------------------------------------------------------------------- /backend/movie/migrations/0003_movie_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-12 23:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('movie', '0002_alter_movie_title'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='movie', 16 | name='category', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 18 | related_name='movies', to='movie.category', verbose_name='categoria'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class CustomUserAdmin(UserAdmin): 7 | list_display = ( 8 | '__str__', 9 | 'email', 10 | 'first_name', 11 | 'last_name', 12 | 'get_groups', 13 | 'is_staff', 14 | 'is_superuser', 15 | ) 16 | 17 | @admin.display(description='Grupos') 18 | def get_groups(self, obj): 19 | groups = obj.groups.all() 20 | if groups: 21 | return ', '.join([group.name for group in groups]) 22 | 23 | 24 | admin.site.unregister(User) 25 | admin.site.register(User, CustomUserAdmin) 26 | -------------------------------------------------------------------------------- /backend/school/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from backend.school.api.viewsets import ( 5 | ClassroomViewSet, 6 | ClassViewSet, 7 | GradeViewSet 8 | ) 9 | from backend.school.api.viewsets import StudentViewSet as SimpleStudentViewSet 10 | 11 | app_name = 'school' 12 | 13 | router = routers.DefaultRouter() 14 | 15 | router.register(r'students', SimpleStudentViewSet, basename='student') 16 | router.register(r'classrooms', ClassroomViewSet, basename='classroom') 17 | router.register(r'classes', ClassViewSet, basename='classes') 18 | router.register(r'grades', GradeViewSet, basename='grade') 19 | 20 | urlpatterns = [ 21 | path('api/v1/', include(router.urls)), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/school/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from backend.school.models import Class, Classroom, Grade, Student 4 | 5 | 6 | @admin.register(Student) 7 | class StudentAdmin(admin.ModelAdmin): 8 | list_display = ('__str__', 'registration') 9 | search_fields = ('registration', 'first_name', 'last_name') 10 | 11 | 12 | @admin.register(Classroom) 13 | class ClassroomAdmin(admin.ModelAdmin): 14 | list_display = ('__str__',) 15 | search_fields = ('title',) 16 | 17 | 18 | @admin.register(Grade) 19 | class GradeAdmin(admin.ModelAdmin): 20 | list_display = ('student', 'note') 21 | search_fields = ('note',) 22 | 23 | 24 | @admin.register(Class) 25 | class ClassAdmin(admin.ModelAdmin): 26 | exclude = () 27 | -------------------------------------------------------------------------------- /backend/todo/migrations/0004_alter_todo_description_alter_todo_task.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-04-20 01:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todo', '0003_todo_description'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='todo', 15 | name='description', 16 | field=models.TextField(blank=True, null=True, verbose_name='descrição'), 17 | ), 18 | migrations.AlterField( 19 | model_name='todo', 20 | name='task', 21 | field=models.CharField(max_length=50, verbose_name='tarefa'), 22 | ), 23 | ] 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 | -------------------------------------------------------------------------------- /backend/todo/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 TodoForm 11 | from .models import Todo 12 | 13 | 14 | class TodoListView(ListView): 15 | model = Todo 16 | paginate_by = 10 17 | 18 | 19 | class TodoDetailView(DetailView): 20 | model = Todo 21 | 22 | 23 | class TodoCreateView(CreateView): 24 | model = Todo 25 | form_class = TodoForm 26 | 27 | 28 | class TodoUpdateView(UpdateView): 29 | model = Todo 30 | form_class = TodoForm 31 | 32 | 33 | class TodoDeleteView(DeleteView): 34 | model = Todo 35 | success_url = reverse_lazy('todo:todo_list') 36 | -------------------------------------------------------------------------------- /backend/crm/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Comission, Customer 4 | 5 | 6 | @admin.register(Customer) 7 | class CustomerAdmin(admin.ModelAdmin): 8 | list_display = ('id', '__str__', 'rg', 'cpf', 'cep', 'seller', 'active') 9 | list_display_links = ('__str__',) 10 | search_fields = ( 11 | 'user__first_name', 12 | 'user__last_name', 13 | 'user__email', 14 | 'seller__first_name', 15 | 'seller__last_name', 16 | 'seller__email', 17 | 'rg', 18 | 'cpf', 19 | 'cep', 20 | 'address', 21 | ) 22 | list_filter = ('active',) 23 | 24 | 25 | @admin.register(Comission) 26 | class ComissionAdmin(admin.ModelAdmin): 27 | list_display = ('__str__', 'percentage') 28 | -------------------------------------------------------------------------------- /backend/core/handler.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.views import exception_handler 3 | 4 | 5 | def custom_exception_handler(exc, context): 6 | response = exception_handler(exc, context) 7 | method = context['request'].method 8 | 9 | if response: 10 | if response.status_code == status.HTTP_403_FORBIDDEN: 11 | if method == 'POST': 12 | response.data = {'message': 'Você não tem permissão para Adicionar.'} 13 | elif method == 'PUT' or method == 'PATCH': 14 | response.data = {'message': 'Você não tem permissão para Editar.'} 15 | elif method == 'DELETE': 16 | response.data = {'message': 'Você não tem permissão para Deletar.'} 17 | 18 | return response 19 | -------------------------------------------------------------------------------- /backend/example/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.response import Response 3 | from rest_framework.views import APIView 4 | 5 | from backend.example.api.serializers import ExampleSerializer 6 | from backend.example.models import Example 7 | 8 | 9 | class ExampleViewSet(viewsets.ModelViewSet): 10 | queryset = Example.objects.all() 11 | serializer_class = ExampleSerializer 12 | 13 | 14 | class ExampleView(APIView): 15 | 16 | def get(self, request, format=None): 17 | content = { 18 | 'user': str(request.user), 19 | 'auth': str(request.auth) 20 | } 21 | return Response(content) 22 | 23 | # Não serve 24 | # @classmethod 25 | # def get_extra_actions(cls): 26 | # return [] 27 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | # client.py 2 | import timeit 3 | 4 | import requests 5 | 6 | base_url = 'http://localhost:8000/api/v1' 7 | 8 | url_video = f'{base_url}/videos/' 9 | url_movie = f'{base_url}/movies/?format=json' 10 | url_movie_readonly = f'{base_url}/movies/movies_readonly/?format=json' 11 | url_movie_regular_readonly = f'{base_url}/movies/movies_regular_readonly/?format=json' 12 | 13 | 14 | def get_result(url): 15 | start_time = timeit.default_timer() 16 | r = requests.get(url) 17 | print('status_code:', r.status_code) 18 | end_time = timeit.default_timer() 19 | print('time:', round(end_time - start_time, 3)) 20 | print() 21 | 22 | 23 | if __name__ == '__main__': 24 | get_result(url_video) 25 | get_result(url_movie) 26 | get_result(url_movie_readonly) 27 | get_result(url_movie_regular_readonly) 28 | -------------------------------------------------------------------------------- /backend/todo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from backend.todo import views as v 5 | from backend.todo.api.viewsets import TodoViewSet 6 | 7 | app_name = 'todo' 8 | 9 | router = routers.DefaultRouter() 10 | 11 | router.register(r'todos', TodoViewSet) 12 | 13 | todo_urlpatterns = [ 14 | path('', v.TodoListView.as_view(), name='todo_list'), 15 | path('/', v.TodoDetailView.as_view(), name='todo_detail'), 16 | path('create/', v.TodoCreateView.as_view(), name='todo_create'), 17 | path('/update/', v.TodoUpdateView.as_view(), name='todo_update'), 18 | path('/delete/', v.TodoDeleteView.as_view(), name='todo_delete'), 19 | ] 20 | 21 | urlpatterns = [ 22 | path('todo/', include(todo_urlpatterns)), 23 | path('api/v1/', include(router.urls)), 24 | ] 25 | -------------------------------------------------------------------------------- /passo-a-passo/10_reescrevendo_admin_user.md: -------------------------------------------------------------------------------- 1 | # Django Experience #10 - Dica: Reescrevendo o Admin do User 2 | 3 | ```python 4 | # core/admin.py 5 | from django.contrib import admin 6 | from django.contrib.auth.admin import UserAdmin 7 | from django.contrib.auth.models import User 8 | 9 | 10 | class CustomUserAdmin(UserAdmin): 11 | list_display = ( 12 | '__str__', 13 | 'email', 14 | 'first_name', 15 | 'last_name', 16 | 'get_groups', 17 | 'is_staff', 18 | 'is_superuser' 19 | ) 20 | 21 | @admin.display(description='Grupos') 22 | def get_groups(self, obj): 23 | groups = obj.groups.all() 24 | if groups: 25 | return ', '.join([group.name for group in groups]) 26 | 27 | 28 | admin.site.unregister(User) 29 | admin.site.register(User, CustomUserAdmin) 30 | ``` 31 | -------------------------------------------------------------------------------- /backend/example/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-02-27 23:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Example', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(blank=True, max_length=255, null=True)), 19 | ('created', models.DateTimeField(auto_now_add=True)), 20 | ('updated', models.DateTimeField(auto_now=True)), 21 | ], 22 | options={ 23 | 'verbose_name_plural': 'Examples', 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /backend/movie/migrations/0004_rename_create_date_category_created_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-12 23:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('movie', '0003_movie_category'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='category', 15 | old_name='create_date', 16 | new_name='created', 17 | ), 18 | migrations.RenameField( 19 | model_name='movie', 20 | old_name='create_date', 21 | new_name='created', 22 | ), 23 | migrations.AlterField( 24 | model_name='category', 25 | name='title', 26 | field=models.CharField(blank=True, max_length=30, null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/order/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import redirect, render 3 | 4 | from .forms import OrderForm 5 | from .models import Order 6 | 7 | 8 | def order_list(request): 9 | template_name = 'order/order_list.html' 10 | object_list = Order.objects.all() 11 | context = {'object_list': object_list} 12 | return render(request, template_name, context) 13 | 14 | 15 | @login_required 16 | def order_create(request): 17 | template_name = 'order/order_form.html' 18 | # Passa o usuário logado no formulário. 19 | form = OrderForm(request.user, request.POST or None) 20 | 21 | if request.method == 'POST': 22 | if form.is_valid(): 23 | form.save() 24 | return redirect('order:order_list') 25 | 26 | context = {'form': form} 27 | return render(request, template_name, context) 28 | -------------------------------------------------------------------------------- /backend/todo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-11 03:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Todo', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, 18 | primary_key=True, serialize=False, verbose_name='ID')), 19 | ('task', models.CharField(max_length=50)), 20 | ('is_done', models.BooleanField(default=False)), 21 | ('created', models.DateTimeField(auto_now_add=True)), 22 | ], 23 | options={ 24 | 'verbose_name': 'Tarefa', 25 | 'verbose_name_plural': 'Tarefas', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/todo/migrations/0002_todo_created_by_todo_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-02-28 22:20 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('auth', '0012_alter_user_first_name_max_length'), 11 | ('todo', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='todo', 17 | name='created_by', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.user'), 19 | ), 20 | migrations.AddField( 21 | model_name='todo', 22 | name='status', 23 | field=models.CharField(choices=[('p', 'Pendente'), ('a', 'Aprovado'), ('c', 'Cancelado')], default='p', max_length=1), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/hotel/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-19 12:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Hotel', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=32)), 19 | ('start_date', models.DateField(blank=True, null=True)), 20 | ('end_date', models.DateField(blank=True, null=True)), 21 | ('created', models.DateTimeField(auto_now_add=True)), 22 | ], 23 | options={ 24 | 'verbose_name': 'Hotel', 25 | 'verbose_name_plural': 'Hotéis', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/movie/migrations/0005_alter_category_title_alter_movie_sinopse_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-02-06 19:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('movie', '0004_rename_create_date_category_created_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='category', 15 | name='title', 16 | field=models.CharField(blank=True, default='', max_length=30), 17 | ), 18 | migrations.AlterField( 19 | model_name='movie', 20 | name='sinopse', 21 | field=models.CharField(blank=True, default='', max_length=255), 22 | ), 23 | migrations.AlterField( 24 | model_name='movie', 25 | name='title', 26 | field=models.CharField(blank=True, default='', max_length=30), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/video/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-12 20:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Video', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, 18 | primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=50, 20 | unique=True, verbose_name='título')), 21 | ('release_year', models.PositiveIntegerField( 22 | blank=True, null=True, verbose_name='lançamento')), 23 | ], 24 | options={ 25 | 'verbose_name': 'filme', 26 | 'verbose_name_plural': 'filmes', 27 | 'ordering': ('id',), 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /passo-a-passo/13_drf_fix_permissao.md: -------------------------------------------------------------------------------- 1 | # Django Experience #13 - DRF: Fix Permissão 2 | 3 | 4 | Doc: https://www.django-rest-framework.org/api-guide/permissions/ 5 | 6 | 7 | ```python 8 | #movie/viewsets.py 9 | class CensurePermission(BasePermission): 10 | age_user = 14 11 | group_name = 'Infantil' 12 | message = 'Este filme não é permitido para este perfil.' 13 | 14 | def has_object_permission(self, request, view, obj): 15 | groups = request.user.groups.values_list('name', flat=True) 16 | 17 | censure = obj.censure 18 | 19 | if self.group_name in groups and censure >= self.age_user: 20 | response = { 21 | 'message': self.message, 22 | 'status_code': status.HTTP_403_FORBIDDEN 23 | } 24 | raise DRFValidationError(response) 25 | else: 26 | return True 27 | 28 | 29 | class MovieViewSet(viewsets.ModelViewSet): 30 | ... 31 | permission_classes = (DjangoModelPermissions, CensurePermission) 32 | ``` 33 | -------------------------------------------------------------------------------- /backend/order/templates/order/order_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Lista de Pedidos

9 |
10 |
11 | Adicionar 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for object in object_list %} 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
IDPedidoFuncionário
{{ object.id }}{{ object.title }}{{ object.employee }}
35 |
36 | 37 | {% include "includes/pagination.html" %} 38 | 39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /backend/todo/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from django.urls import reverse_lazy 4 | 5 | STATUS = ( 6 | ('p', 'Pendente'), 7 | ('a', 'Aprovado'), 8 | ('c', 'Cancelado'), 9 | ) 10 | 11 | 12 | class Todo(models.Model): 13 | task = models.CharField('tarefa', max_length=50) 14 | description = models.TextField('descrição', null=True, blank=True) 15 | is_done = models.BooleanField(default=False) 16 | created = models.DateTimeField(auto_now_add=True) 17 | created_by = models.ForeignKey( 18 | User, 19 | on_delete=models.SET_NULL, 20 | null=True, 21 | blank=True 22 | ) 23 | status = models.CharField(max_length=1, choices=STATUS, default='p') 24 | 25 | def __str__(self): 26 | return f"{self.task}" 27 | 28 | class Meta: 29 | verbose_name = "Tarefa" 30 | verbose_name_plural = "Tarefas" 31 | 32 | def get_absolute_url(self): 33 | return reverse_lazy('todo:todo_detail', kwargs={'pk': self.pk}) 34 | -------------------------------------------------------------------------------- /passo-a-passo/12_drf_editando_mensagens_erro.md: -------------------------------------------------------------------------------- 1 | # Django Experience #12 - Dica: Editando as mensagens de erro 2 | 3 | 4 | ```python 5 | # core/handler.py 6 | from rest_framework.views import exception_handler 7 | from rest_framework import status 8 | 9 | 10 | def custom_exception_handler(exc, context): 11 | response = exception_handler(exc, context) 12 | method = context['request'].method 13 | 14 | if response.status_code == status.HTTP_403_FORBIDDEN: 15 | if method == 'POST': 16 | response.data = {'message': 'Você não tem permissão para Adicionar.'} 17 | elif method == 'PUT' or method == 'PATCH': 18 | response.data = {'message': 'Você não tem permissão para Editar.'} 19 | elif method == 'DELETE': 20 | response.data = {'message': 'Você não tem permissão para Deletar.'} 21 | 22 | return response 23 | ``` 24 | 25 | ```python 26 | # settings.py 27 | REST_FRAMEWORK = { 28 | ... 29 | 'EXCEPTION_HANDLER': 'backend.core.handler.custom_exception_handler', 30 | ... 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /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=db 21 | POSTGRES_USER=postgres 22 | POSTGRES_PASSWORD=postgres 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 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 | -------------------------------------------------------------------------------- /backend/school/migrations/0002_grade.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-13 02:21 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('school', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Grade', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, 18 | primary_key=True, serialize=False, verbose_name='ID')), 19 | ('note', models.DecimalField(decimal_places=2, 20 | default=0.0, max_digits=5, null=True)), 21 | ('created', models.DateTimeField(auto_now_add=True)), 22 | ('student', models.ForeignKey( 23 | null=True, on_delete=django.db.models.deletion.CASCADE, to='school.student')), 24 | ], 25 | options={ 26 | 'verbose_name': 'Nota', 27 | 'verbose_name_plural': 'Notas', 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/order/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Employee, Order 4 | 5 | 6 | class OrderForm(forms.ModelForm): 7 | required_css_class = 'required' 8 | 9 | employee = forms.ModelChoiceField( 10 | label='Funcionário', 11 | queryset=None, 12 | ) 13 | 14 | class Meta: 15 | model = Order 16 | fields = '__all__' 17 | 18 | def __init__(self, user, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | 21 | # Retorna o funcionário logado. 22 | employee = user.user_employees.first() 23 | 24 | # Retorna o departamento do funcionário logado. 25 | department = employee.department 26 | 27 | # Retorna somente os funcionários do meu departamento. 28 | employees = Employee.objects.filter(department=department) 29 | 30 | # Altera o filtro de ModelChoiceField. 31 | self.fields['employee'].queryset = employees 32 | 33 | # Adiciona classe form-control nos campos. 34 | for field_name, field in self.fields.items(): 35 | field.widget.attrs['class'] = 'form-control' 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | database: 5 | container_name: db 6 | image: postgres:14-alpine 7 | restart: always 8 | user: postgres # importante definir o usuário 9 | volumes: 10 | - pgdata:/var/lib/postgresql/data 11 | environment: 12 | - LC_ALL=C.UTF-8 13 | - POSTGRES_PASSWORD=postgres # senha padrão 14 | - POSTGRES_USER=postgres # usuário padrão 15 | - POSTGRES_DB=db # necessário porque foi configurado assim no settings 16 | ports: 17 | - 5433:5432 # repare na porta externa 5433 18 | networks: 19 | - postgres 20 | 21 | pgadmin: 22 | container_name: pgadmin 23 | image: dpage/pgadmin4 24 | restart: unless-stopped 25 | volumes: 26 | - pgadmin:/var/lib/pgadmin 27 | environment: 28 | PGADMIN_DEFAULT_EMAIL: admin@admin.com 29 | PGADMIN_DEFAULT_PASSWORD: admin 30 | PGADMIN_CONFIG_SERVER_MODE: 'False' 31 | ports: 32 | - 5050:80 33 | networks: 34 | - postgres 35 | 36 | volumes: 37 | pgdata: # mesmo nome do volume externo definido na linha 10 38 | pgadmin: 39 | 40 | networks: 41 | postgres: -------------------------------------------------------------------------------- /backend/school/migrations/0003_class.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-13 02:32 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('auth', '0012_alter_user_first_name_max_length'), 11 | ('school', '0002_grade'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Class', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, 19 | primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created', models.DateTimeField(auto_now_add=True)), 21 | ('classroom', models.ForeignKey( 22 | on_delete=django.db.models.deletion.CASCADE, to='school.classroom')), 23 | ('teacher', models.ForeignKey( 24 | on_delete=django.db.models.deletion.CASCADE, to='auth.user')), 25 | ], 26 | options={ 27 | 'verbose_name': 'Aula', 28 | 'verbose_name_plural': 'Aulas', 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /backend/school/migrations/0004_alter_class_classroom_alter_class_teacher_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-02-06 19:56 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('auth', '0012_alter_user_first_name_max_length'), 11 | ('school', '0003_class'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='class', 17 | name='classroom', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='classes', to='school.classroom'), 19 | ), 20 | migrations.AlterField( 21 | model_name='class', 22 | name='teacher', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teacher_classes', to='auth.user'), 24 | ), 25 | migrations.AlterField( 26 | model_name='grade', 27 | name='student', 28 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='grades', to='school.student'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/todo/templates/todo/todo_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

6 | Lista 7 | Adicionar 8 |

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for object in object_list %} 20 | 21 | 24 | 25 | 32 | 40 | 41 | {% endfor %} 42 | 43 |
TarefaDescriçãoFeito?Ações
22 | {{ object.task }} 23 | {{ object.description }} 26 | {% if object.is_done %} 27 | 28 | {% else %} 29 | 30 | {% endif %} 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
44 | 45 | {% include "includes/pagination.html" %} 46 | {% endblock content %} 47 | -------------------------------------------------------------------------------- /backend/movie/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | title = models.CharField(max_length=30, default='', blank=True) 6 | created = models.DateTimeField(auto_now_add=True) 7 | 8 | def __str__(self): 9 | return f"{self.title}" 10 | 11 | class Meta: 12 | verbose_name_plural = "Categories" 13 | 14 | 15 | class Movie(models.Model): 16 | title = models.CharField(max_length=30, default='', blank=True) 17 | sinopse = models.CharField(max_length=255, default='', blank=True) 18 | rating = models.PositiveIntegerField() 19 | like = models.BooleanField() 20 | censure = models.PositiveIntegerField() 21 | created = models.DateTimeField(auto_now_add=True) 22 | category = models.ForeignKey( 23 | 'Category', 24 | on_delete=models.SET_NULL, 25 | verbose_name='categoria', 26 | related_name='movies', 27 | null=True, 28 | blank=True 29 | ) 30 | 31 | def __str__(self): 32 | return f"{self.title}" 33 | 34 | class Meta: 35 | verbose_name_plural = "Movies" 36 | 37 | def to_dict(self): 38 | return { 39 | 'id': self.id, 40 | 'title': self.title, 41 | 'sinopse': self.sinopse, 42 | 'rating': self.rating, 43 | 'like': self.like, 44 | 'created': self.created, 45 | # 'category': self.category.id, 46 | } 47 | -------------------------------------------------------------------------------- /backend/school/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-12 23:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Student', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, 18 | primary_key=True, serialize=False, verbose_name='ID')), 19 | ('registration', models.CharField(max_length=7)), 20 | ('first_name', models.CharField(max_length=30)), 21 | ('last_name', models.CharField(max_length=30)), 22 | ], 23 | options={ 24 | 'verbose_name': 'Aluno', 25 | 'verbose_name_plural': 'Alunos', 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='Classroom', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, 32 | primary_key=True, serialize=False, verbose_name='ID')), 33 | ('title', models.CharField(max_length=30)), 34 | ('students', models.ManyToManyField(to='school.Student')), 35 | ], 36 | options={ 37 | 'verbose_name': 'Sala de aula', 38 | 'verbose_name_plural': 'Salas de aula', 39 | }, 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /backend/crm/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Group, User 2 | from django.db import models 3 | 4 | 5 | class Customer(models.Model): 6 | user = models.ForeignKey( 7 | User, 8 | on_delete=models.CASCADE, 9 | related_name='customers' 10 | ) 11 | seller = models.ForeignKey( 12 | User, 13 | on_delete=models.SET_NULL, 14 | related_name='seller_customers', 15 | null=True, 16 | blank=True 17 | ) 18 | rg = models.CharField(max_length=10, null=True, blank=True) 19 | cpf = models.CharField(max_length=11, null=True, blank=True) 20 | cep = models.CharField(max_length=8, null=True, blank=True) 21 | address = models.CharField(max_length=100, null=True, blank=True) 22 | active = models.BooleanField(default=True) 23 | 24 | class Meta: 25 | ordering = ('user__first_name',) 26 | verbose_name = 'cliente' 27 | verbose_name_plural = 'clientes' 28 | 29 | def __str__(self): 30 | return f'{self.user.get_full_name()}' 31 | 32 | 33 | class Comission(models.Model): 34 | group = models.ForeignKey( 35 | Group, 36 | on_delete=models.CASCADE, 37 | related_name='comissions' 38 | ) 39 | percentage = models.DecimalField(max_digits=5, decimal_places=2) 40 | 41 | class Meta: 42 | ordering = ('group__name',) 43 | verbose_name = 'comissão' 44 | verbose_name_plural = 'comissões' 45 | 46 | def __str__(self): 47 | return f'{self.group}' 48 | -------------------------------------------------------------------------------- /backend/movie/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-12 22:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Category', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, 18 | primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(blank=True, max_length=255, null=True)), 20 | ('create_date', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'verbose_name_plural': 'Categories', 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name='Movie', 28 | fields=[ 29 | ('id', models.BigAutoField(auto_created=True, 30 | primary_key=True, serialize=False, verbose_name='ID')), 31 | ('title', models.CharField(blank=True, max_length=255, null=True)), 32 | ('sinopse', models.CharField(blank=True, max_length=255, null=True)), 33 | ('rating', models.PositiveIntegerField()), 34 | ('like', models.BooleanField()), 35 | ('create_date', models.DateTimeField(auto_now_add=True)), 36 | ], 37 | options={ 38 | 'verbose_name_plural': 'Movies', 39 | }, 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /backend/core/templates/includes/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /backend/todo/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | 4 | from .models import Todo 5 | 6 | 7 | class TodoForm(forms.ModelForm): 8 | required_css_class = 'required' 9 | 10 | class Meta: 11 | model = Todo 12 | fields = '__all__' 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(TodoForm, self).__init__(*args, **kwargs) 16 | 17 | for field_name, field in self.fields.items(): 18 | field.widget.attrs['class'] = 'form-control' 19 | 20 | # Remove a class de is_done. 21 | self.fields['is_done'].widget.attrs['class'] = None 22 | 23 | # Torna description somente leitura. 24 | self.fields['description'].widget.attrs['readonly'] = True 25 | 26 | # def clean(self): 27 | # self.cleaned_data = super().clean() 28 | # self.description = self.cleaned_data.get('description') 29 | # self.label = self.fields['description'].label 30 | 31 | # if self.description or (self.instance.pk and self.description != self.instance.description): 32 | # raise ValidationError(f'O campo {self.label} não pode ser editado.') 33 | 34 | # return self.cleaned_data 35 | 36 | def clean_description(self): 37 | self.description = self.cleaned_data.get('description') 38 | self.label = self.fields['description'].label 39 | 40 | if self.description or (self.instance.pk and self.description != self.instance.description): 41 | raise ValidationError(f'O campo {self.label} não pode ser editado.') 42 | 43 | return self.cleaned_data['description'] 44 | -------------------------------------------------------------------------------- /backend/school/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.school.models import Class, Classroom, Grade, Student 4 | 5 | 6 | class StudentSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | model = Student 10 | fields = '__all__' 11 | 12 | 13 | class StudentUpdateSerializer(serializers.ModelSerializer): 14 | 15 | class Meta: 16 | model = Student 17 | fields = ('first_name', 'last_name') 18 | 19 | 20 | class StudentRegistrationSerializer(serializers.BaseSerializer): 21 | 22 | class Meta: 23 | model = Student 24 | 25 | def to_representation(self, instance): 26 | return { 27 | 'registration': instance.registration.zfill(7), 28 | 'full_name': instance.__str__() 29 | } 30 | 31 | 32 | class ClassroomSerializer(serializers.ModelSerializer): 33 | # students = serializers.ListSerializer(child=StudentSerializer()) 34 | # students = StudentSerializer(many=True) 35 | students = serializers.ListSerializer(child=StudentSerializer(), required=False) 36 | 37 | class Meta: 38 | model = Classroom 39 | fields = '__all__' 40 | depth = 1 41 | 42 | 43 | class GradeSerializer(serializers.ModelSerializer): 44 | 45 | class Meta: 46 | model = Grade 47 | fields = '__all__' 48 | 49 | 50 | class ClassSerializer(serializers.ModelSerializer): 51 | 52 | class Meta: 53 | model = Class 54 | fields = '__all__' 55 | 56 | 57 | class ClassAddSerializer(serializers.ModelSerializer): 58 | 59 | class Meta: 60 | model = Class 61 | fields = ('classroom',) 62 | -------------------------------------------------------------------------------- /service.py: -------------------------------------------------------------------------------- 1 | # service.py 2 | import hashlib 3 | 4 | import requests 5 | from decouple import config 6 | from rich import print 7 | from rich.console import Console 8 | from rich.table import Table 9 | 10 | console = Console() 11 | 12 | 13 | def compute_md5_hash(my_string): 14 | ''' 15 | Converte string em md5 hash. 16 | https://stackoverflow.com/a/13259879/802542 17 | ''' 18 | m = hashlib.md5() 19 | m.update(my_string.encode('utf-8')) 20 | return m.hexdigest() 21 | 22 | 23 | def make_authorization(): 24 | ''' 25 | Gera os tokens de autorização. 26 | ''' 27 | publicKey = config('PUBLIC_KEY') 28 | privateKey = config('PRIVATE_KEY') 29 | ts = 1 30 | md5_hash = compute_md5_hash(f'{ts}{privateKey}{publicKey}') 31 | query_params = f'?ts={ts}&apikey={publicKey}&hash={md5_hash}' 32 | return query_params 33 | 34 | 35 | def main(url): 36 | url += make_authorization() 37 | with requests.Session() as session: 38 | response = session.get(url) 39 | print(response) 40 | characters = response.json()['data']['results'] 41 | 42 | table = Table(title='Marvel characters') 43 | headers = ( 44 | 'id', 45 | 'name', 46 | 'description', 47 | ) 48 | 49 | for header in headers: 50 | table.add_column(header) 51 | 52 | for character in characters: 53 | values = str(character['id']), str(character['name']), str(character['description']) # noqa E501 54 | table.add_row(*values) 55 | 56 | console.print(table) 57 | 58 | 59 | if __name__ == '__main__': 60 | endpoint = 'http://gateway.marvel.com/v1/public/characters' 61 | main(endpoint) 62 | -------------------------------------------------------------------------------- /backend/order/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | 5 | class Department(models.Model): 6 | name = models.CharField('nome', max_length=255, unique=True) 7 | 8 | class Meta: 9 | ordering = ('name',) 10 | verbose_name = 'Departamento' 11 | verbose_name_plural = 'Departamentos' 12 | 13 | def __str__(self): 14 | return f'{self.name}' 15 | 16 | 17 | class Employee(models.Model): 18 | user = models.ForeignKey( 19 | User, 20 | on_delete=models.CASCADE, 21 | verbose_name='usuário', 22 | related_name='user_employees', 23 | ) 24 | department = models.ForeignKey( 25 | Department, 26 | on_delete=models.SET_NULL, 27 | verbose_name='departamento', 28 | related_name='department_employees', 29 | null=True, 30 | blank=True 31 | ) 32 | 33 | class Meta: 34 | ordering = ('user__first_name',) 35 | verbose_name = 'Funcionário' 36 | verbose_name_plural = 'Funcionários' 37 | 38 | @property 39 | def full_name(self): 40 | return f'{self.user.first_name} {self.user.last_name or ""}'.strip() 41 | 42 | def __str__(self): 43 | return self.full_name 44 | 45 | 46 | class Order(models.Model): 47 | title = models.CharField('título', max_length=255) 48 | employee = models.ForeignKey( 49 | Employee, 50 | on_delete=models.SET_NULL, 51 | verbose_name='funcionário', 52 | related_name='orders', 53 | null=True, 54 | blank=True 55 | ) 56 | 57 | class Meta: 58 | ordering = ('title',) 59 | verbose_name = 'Pedido' 60 | verbose_name_plural = 'Pedidos' 61 | 62 | def __str__(self): 63 | return self.title 64 | -------------------------------------------------------------------------------- /backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | from drf_yasg import openapi 4 | from drf_yasg.views import get_schema_view 5 | from rest_framework import permissions 6 | 7 | schema_view = get_schema_view( 8 | openapi.Info( 9 | title="Snippets API", 10 | default_version='v1', 11 | description="Test description", 12 | terms_of_service="https://www.google.com/policies/terms/", 13 | contact=openapi.Contact(email="contact@snippets.local"), 14 | license=openapi.License(name="BSD License"), 15 | ), 16 | public=True, 17 | permission_classes=(permissions.AllowAny,), 18 | ) 19 | 20 | urlpatterns = [ 21 | path('accounts/', include('django.contrib.auth.urls')), 22 | path('', include('backend.core.urls', namespace='core')), 23 | path('', include('backend.crm.urls', namespace='crm')), 24 | path('', include('backend.example.urls', namespace='example')), 25 | path('', include('backend.hotel.urls', namespace='hotel')), 26 | path('', include('backend.movie.urls', namespace='movie')), 27 | path('', include('backend.order.urls', namespace='order')), 28 | path('', include('backend.school.urls', namespace='school')), 29 | path('', include('backend.todo.urls', namespace='todo')), 30 | path('', include('backend.video.urls', namespace='video')), 31 | path('admin/', admin.site.urls), 32 | ] 33 | 34 | # djoser 35 | urlpatterns += [ 36 | path('api/v1/', include('djoser.urls')), 37 | path('api/v1/auth/', include('djoser.urls.authtoken')), 38 | ] 39 | 40 | # swagger 41 | urlpatterns += [ 42 | path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), # noqa E501 43 | path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), # noqa E501 44 | ] 45 | -------------------------------------------------------------------------------- /passo-a-passo/24_docker_popos.md: -------------------------------------------------------------------------------- 1 | # Django Experience #24 - Instalando Docker no Pop!_OS 2 | 3 | 4 | 5 | 6 | 7 | 8 | # Installation of the Docker through the repository in Pop!_OS 9 | 10 | ``` 11 | sudo apt update 12 | sudo apt install ca-certificates curl gnupg lsb-release 13 | ``` 14 | 15 | 16 | Download the GPG key of the Docker from its website and add it to the repository of Pop!_OS: 17 | 18 | ``` 19 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 20 | ``` 21 | 22 | 23 | Add the stable repository of the dockers from its website to the repository of Pop!_OS: 24 | 25 | ``` 26 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 27 | ``` 28 | 29 | 30 | Update again 31 | 32 | ``` 33 | sudo apt update 34 | ``` 35 | 36 | 37 | Install the latest version of Dockers on Pop!_OS: 38 | 39 | ``` 40 | sudo apt install docker-ce docker-ce-cli containerd.io -y 41 | ``` 42 | 43 | 44 | After the complete installation of the Docker, we will check its status using the command: 45 | 46 | ``` 47 | sudo systemctl status docker 48 | ``` 49 | 50 | 51 | See the docker version 52 | 53 | ``` 54 | docker --version 55 | ``` 56 | 57 | 58 | Install docker-compose 59 | 60 | ``` 61 | sudo apt install docker-compose -y 62 | ``` 63 | 64 | 65 | See the docker-compose version 66 | 67 | ``` 68 | docker-compose --version 69 | ``` 70 | 71 | Ref: https://linuxhint.com/install-docker-on-pop_os/ 72 | 73 | Add group permissions 74 | 75 | ``` 76 | sudo groupadd docker 77 | sudo gpasswd -a $USER docker 78 | sudo setfacl -m user:$USER:rw /var/run/docker.sock 79 | ``` 80 | 81 | Test a container example 82 | 83 | ``` 84 | docker run hello-world 85 | ``` 86 | -------------------------------------------------------------------------------- /passo-a-passo/08_drf_salvando_dados_extra.md: -------------------------------------------------------------------------------- 1 | # Django Experience #08 - DRF: Salvando dados extra 2 | 3 | Considere a app `todo`. E o model `Todo`. 4 | 5 | Agora temos os campos `created_by` e `status`. 6 | 7 | ```python 8 | from django.contrib.auth.models import User 9 | 10 | STATUS = ( 11 | ('p', 'Pendente'), 12 | ('a', 'Aprovado'), 13 | ('c', 'Cancelado'), 14 | ) 15 | 16 | 17 | class Todo(models.Model): 18 | task = models.CharField(max_length=50) 19 | is_done = models.BooleanField(default=False) 20 | created = models.DateTimeField(auto_now_add=True) 21 | created_by = models.ForeignKey( 22 | User, 23 | on_delete=models.SET_NULL, 24 | null=True, 25 | blank=True 26 | ) 27 | status = models.CharField(max_length=1, choices=STATUS, default='p') 28 | ``` 29 | 30 | É muito simples, basta sobreescrever o método `perform_create` em `viewsets.py`. 31 | 32 | ```python 33 | class TodoViewSet(viewsets.ModelViewSet): 34 | queryset = Todo.objects.all() 35 | serializer_class = TodoSerializer 36 | 37 | def perform_create(self, serializer): 38 | serializer.save(created_by=self.request.user, status='a') 39 | ``` 40 | 41 | 42 | ## Salvando os dados 43 | 44 | Abra o Postman e faça um POST **logado** com `Basic Auth`. 45 | 46 | ``` 47 | { 48 | "task": "Tarefa aprovada" 49 | } 50 | ``` 51 | 52 | O resultado será 53 | 54 | ``` 55 | { 56 | "id": 60, 57 | "task": "Tarefa aprovada", 58 | "is_done": false, 59 | "created": "2021-12-21T23:15:53.259852-03:00", 60 | "status": "a", 61 | "created_by": 1 62 | } 63 | ``` 64 | 65 | Se quiser ver o usuário expandido, basta colocar `depth = 1` em `TodoSerializer`. 66 | 67 | 68 | Leia [https://simpleisbetterthancomplex.com/tutorial/2019/04/07/how-to-save-extra-data-to-a-django-rest-framework-serializer.html 69 | ](https://simpleisbetterthancomplex.com/tutorial/2019/04/07/how-to-save-extra-data-to-a-django-rest-framework-serializer.html 70 | ) 71 | -------------------------------------------------------------------------------- /passo-a-passo/17_novo_comando.md: -------------------------------------------------------------------------------- 1 | # Django Experience #17 - Novo comando 2 | 3 | 4 | 5 | 6 | 7 | Vamos criar um comando para criar novos clientes. 8 | 9 | ``` 10 | python manage.py create_command core -n create_data 11 | ``` 12 | 13 | ```python 14 | # core/management/commands/create_data.py 15 | import string 16 | from random import choice 17 | 18 | from django.contrib.auth.models import User 19 | from django.core.management.base import BaseCommand 20 | from django.utils.text import slugify 21 | from faker import Faker 22 | 23 | from backend.crm.models import Customer 24 | 25 | fake = Faker() 26 | 27 | 28 | def gen_digits(max_length): 29 | return str(''.join(choice(string.digits) for i in range(max_length))) 30 | 31 | 32 | def gen_email(first_name: str, last_name: str): 33 | first_name = slugify(first_name) 34 | last_name = slugify(last_name) 35 | email = f'{first_name}.{last_name}@email.com' 36 | return email 37 | 38 | 39 | def get_person(): 40 | name = fake.first_name() 41 | username = name.lower() 42 | first_name = name 43 | last_name = fake.last_name() 44 | email = gen_email(first_name, last_name) 45 | 46 | user = User.objects.create( 47 | username=username, 48 | first_name=first_name, 49 | last_name=last_name, 50 | email=email 51 | ) 52 | 53 | data = dict( 54 | user=user, 55 | rg=gen_digits(9), 56 | cpf=gen_digits(11), 57 | cep=gen_digits(8), 58 | ) 59 | return data 60 | 61 | 62 | def create_persons(): 63 | aux_list = [] 64 | for _ in range(6): 65 | data = get_person() 66 | obj = Customer(**data) 67 | aux_list.append(obj) 68 | Customer.objects.bulk_create(aux_list) 69 | 70 | 71 | class Command(BaseCommand): 72 | help = 'Create data.' 73 | 74 | def handle(self, *args, **options): 75 | create_persons() 76 | 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /backend/school/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | 5 | class Student(models.Model): 6 | registration = models.CharField(max_length=7) 7 | first_name = models.CharField(max_length=30) 8 | last_name = models.CharField(max_length=30) 9 | 10 | def __str__(self): 11 | return f"{self.first_name} {self.last_name}" 12 | 13 | class Meta: 14 | verbose_name = "Aluno" 15 | verbose_name_plural = "Alunos" 16 | 17 | 18 | class Classroom(models.Model): 19 | title = models.CharField(max_length=30) 20 | students = models.ManyToManyField(Student) 21 | 22 | def __str__(self): 23 | return f"{self.title}" 24 | 25 | class Meta: 26 | verbose_name = "Sala de aula" 27 | verbose_name_plural = "Salas de aula" 28 | 29 | 30 | class Class(models.Model): 31 | classroom = models.ForeignKey( 32 | Classroom, 33 | on_delete=models.CASCADE, 34 | related_name='classes', 35 | ) 36 | teacher = models.ForeignKey( 37 | User, 38 | on_delete=models.CASCADE, 39 | related_name='teacher_classes', 40 | ) 41 | created = models.DateTimeField(auto_now_add=True) 42 | 43 | def __str__(self): 44 | return f"{self.classroom} {self.teacher}" 45 | 46 | class Meta: 47 | verbose_name = "Aula" 48 | verbose_name_plural = "Aulas" 49 | 50 | 51 | class Grade(models.Model): 52 | student = models.ForeignKey( 53 | Student, 54 | on_delete=models.CASCADE, 55 | related_name='grades', 56 | null=True, 57 | blank=True, 58 | ) 59 | note = models.DecimalField( 60 | max_digits=5, 61 | decimal_places=2, 62 | null=True, 63 | default=0.0 64 | ) 65 | created = models.DateTimeField(auto_now_add=True) 66 | 67 | def __str__(self): 68 | return f"{self.student} {self.note}" 69 | 70 | class Meta: 71 | verbose_name = "Nota" 72 | verbose_name_plural = "Notas" 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-experience 2 | 3 | Tutorial Django Experience 2022 4 | 5 | ## Este projeto foi feito com: 6 | 7 | * [Python 3.10.4](https://www.python.org/) 8 | * [Django 4.0.4](https://www.djangoproject.com/) 9 | * [Django Rest Framework 3.12.4](https://www.django-rest-framework.org/) 10 | * [Bootstrap 4.0](https://getbootstrap.com/) 11 | * [htmx 1.6.1](https://htmx.org/) 12 | 13 | ## Como rodar o projeto? 14 | 15 | * Clone esse repositório. 16 | * Crie um virtualenv com Python 3. 17 | * Ative o virtualenv. 18 | * Instale as dependências. 19 | * Rode as migrações. 20 | 21 | ``` 22 | git clone https://github.com/rg3915/django-experience.git 23 | cd django-experience 24 | python -m venv .venv 25 | source .venv/bin/activate 26 | pip install -r requirements.txt 27 | python contrib/env_gen.py 28 | python manage.py migrate 29 | python manage.py createsuperuser --username="admin" --email="" 30 | ``` 31 | 32 | ## Passo a passo 33 | 34 | Leia [https://rg3915.github.io/django-experience/](https://rg3915.github.io/django-experience/) ou 35 | 36 | Veja a pasta de [passo-a-passo](https://github.com/rg3915/django-experience/tree/main/passo-a-passo). 37 | 38 | 39 | ## Features da aplicação 40 | 41 | * Renderização de templates na app `todo`. 42 | * API REST feita com Django puro na app `video`. 43 | * Django REST framework nas apps `example`, `hotel`, `movie` e `school`. 44 | * [Salvando dados extra](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/08_drf_salvando_dados_extra.md) com [perform_create](https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#associating-snippets-with-users) 45 | * **Dica:** [Reescrevendo o Admin do User](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/10_reescrevendo_admin_user.md) 46 | * [Editando mensagens de erro no DRF](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/12_drf_editando_mensagens_erro.md) 47 | * **Dica:** [Adicionando Grupos e Permissões](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/14_grupos_permissoes.md) 48 | -------------------------------------------------------------------------------- /backend/core/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 | 2 | {% load url_replace %} 3 | 4 | 5 |
6 |
7 |
    8 | {% if page_obj.has_previous %} 9 |
  • «
  • 10 | {% endif %} 11 | 12 | {% for pg in page_obj.paginator.page_range %} 13 | 14 | {% 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' %} 15 | {% if page_obj.number == pg %} 16 |
  • {{ pg }}
  • 17 | {% else %} 18 |
  • {{ pg }}
  • 19 | {% endif %} 20 | 21 | {% else %} 22 | 23 | {% if page_obj.number == pg %} 24 |
  • {{ pg }}
  • 25 | {% elif pg > page_obj.number|add:'-4' and pg < page_obj.number|add:'4' %} 26 |
  • {{ pg }}
  • 27 | {% elif pg == page_obj.number|add:'-4' or pg == page_obj.number|add:'4' %} 28 |
  • ...
  • 29 | {% endif %} 30 | {% endif %} 31 | {% endfor %} 32 | 33 | {% if page_obj.has_next %} 34 |
  • »
  • 35 | {% endif %} 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /passo-a-passo/14_grupos_permissoes.md: -------------------------------------------------------------------------------- 1 | # Django Experience #14 - DRF: Grupos e Permissões 2 | 3 | ![](../img/grupos_permissoes.png) 4 | 5 | ## Definindo permissões por linha de comando 6 | 7 | Abra o `shell_plus` 8 | 9 | ``` 10 | python manage.py shell_plus 11 | ``` 12 | 13 | 14 | ```python 15 | # Cria os grupos 16 | groups = ['Criador', 'Editor', 'Gerente', 'Infantil'] 17 | [Group.objects.get_or_create(name=group) for group in groups] 18 | 19 | # Lê as permissões 20 | permissions = Permission.objects.filter(codename__icontains='movie') 21 | for perm in permissions: 22 | print(perm.codename) 23 | 24 | # Função para adicionar os grupos 25 | def add_permissions(group_name, permissions): 26 | group = Group.objects.get(name=group_name) 27 | permissions = Permission.objects.filter(codename__in=permissions) 28 | # Remove todas as permissões. 29 | group.permissions.clear() 30 | # Adiciona novas permissões. 31 | for perm in permissions: 32 | group.permissions.add(perm) 33 | 34 | # Adiciona permissões aos grupos 35 | add_permissions('Criador', ['add_movie']) 36 | add_permissions('Editor', ['add_movie', 'change_movie']) 37 | add_permissions('Gerente', ['add_movie', 'change_movie', 'delete_movie']) 38 | 39 | # Cria os usuários 40 | users = ['regis', 'criador', 'editor', 'gerente', 'pedrinho'] 41 | 42 | for user in users: 43 | obj = User.objects.create_user(user) 44 | obj.set_password('d') 45 | obj.save() 46 | 47 | # Associa os usuários aos grupos 48 | criador = User.objects.get(username='criador') 49 | editor = User.objects.get(username='editor') 50 | gerente = User.objects.get(username='gerente') 51 | pedrinho = User.objects.get(username='pedrinho') 52 | 53 | grupo_criador = Group.objects.get(name='Criador') 54 | criador.groups.clear() 55 | criador.groups.add(grupo_criador) 56 | 57 | grupo_editor = Group.objects.get(name='Editor') 58 | editor.groups.clear() 59 | editor.groups.add(grupo_editor) 60 | 61 | grupo_gerente = Group.objects.get(name='Gerente') 62 | gerente.groups.clear() 63 | gerente.groups.add(grupo_gerente) 64 | 65 | grupo_infantil = Group.objects.get(name='Infantil') 66 | pedrinho.groups.clear() 67 | pedrinho.groups.add(grupo_infantil) 68 | ``` 69 | -------------------------------------------------------------------------------- /backend/video/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.http import JsonResponse 4 | from django.shortcuts import get_object_or_404 5 | from django.views.decorators.csrf import csrf_exempt 6 | 7 | from .forms import VideoForm 8 | from .models import Video 9 | 10 | 11 | @csrf_exempt 12 | def videos(request): 13 | ''' 14 | Lista ou cria videos. 15 | ''' 16 | videos = Video.objects.all() 17 | data = [video.to_dict() for video in videos] 18 | form = VideoForm(request.POST or None) 19 | 20 | if request.method == 'POST': 21 | if request.POST: 22 | # Dados obtidos pelo formulário. 23 | if form.is_valid(): 24 | video = form.save() 25 | 26 | elif request.body: 27 | # Dados obtidos via json. 28 | data = json.loads(request.body) 29 | video = Video.objects.create(**data) 30 | 31 | else: 32 | return JsonResponse({'message': 'Algo deu errado.'}) 33 | 34 | return JsonResponse({'data': video.to_dict()}) 35 | 36 | return JsonResponse({'data': data}) 37 | 38 | 39 | @csrf_exempt 40 | def video(request, pk): 41 | ''' 42 | Mostra os detalhes, edita ou deleta um video. 43 | ''' 44 | video = get_object_or_404(Video, pk=pk) 45 | form = VideoForm(request.POST or None, instance=video) 46 | 47 | if request.method == 'GET': 48 | data = video.to_dict() 49 | return JsonResponse({'data': data}) 50 | 51 | if request.method == 'POST': 52 | if request.POST: 53 | # Dados obtidos pelo formulário. 54 | if form.is_valid(): 55 | video = form.save() 56 | 57 | elif request.body: 58 | # Dados obtidos via json. 59 | data = json.loads(request.body) 60 | 61 | for attr, value in data.items(): 62 | setattr(video, attr, value) 63 | video.save() 64 | 65 | else: 66 | return JsonResponse({'message': 'Algo deu errado.'}) 67 | 68 | return JsonResponse({'data': video.to_dict()}) 69 | 70 | if request.method == 'DELETE': 71 | video.delete() 72 | return JsonResponse({'data': 'Item deletado com sucesso.'}) 73 | -------------------------------------------------------------------------------- /passo-a-passo/19_readonly_validation.md: -------------------------------------------------------------------------------- 1 | # Django Experience #19 - Dica: O problema do readonly e validação no Django 2 | 3 | 4 | 5 | 6 | 7 | Doc: [https://docs.djangoproject.com/en/4.0/ref/forms/validation/#cleaning-a-specific-field-attribute](https://docs.djangoproject.com/en/4.0/ref/forms/validation/#cleaning-a-specific-field-attribute) 8 | 9 | Considere a app `todo` e o campo `description`: 10 | 11 | ```python 12 | # todo/models.py 13 | class Todo(models.Model): 14 | ... 15 | description = models.TextField('descrição', null=True, blank=True) 16 | ... 17 | ``` 18 | 19 | ```python 20 | # todo/forms.py 21 | from django import forms 22 | from django.core.exceptions import ValidationError 23 | 24 | from .models import Todo 25 | 26 | 27 | class TodoForm(forms.ModelForm): 28 | required_css_class = 'required' 29 | 30 | class Meta: 31 | model = Todo 32 | fields = '__all__' 33 | 34 | def __init__(self, *args, **kwargs): 35 | super(TodoForm, self).__init__(*args, **kwargs) 36 | 37 | for field_name, field in self.fields.items(): 38 | field.widget.attrs['class'] = 'form-control' 39 | 40 | # Remove a class de is_done. 41 | self.fields['is_done'].widget.attrs['class'] = None 42 | 43 | # Torna description somente leitura. 44 | self.fields['description'].widget.attrs['readonly'] = True 45 | 46 | # def clean(self): 47 | # self.cleaned_data = super().clean() 48 | # self.description = self.cleaned_data.get('description') 49 | # self.label = self.fields["description"].label 50 | 51 | # if self.description or self.description != self.instance.description: 52 | # raise ValidationError(f'O campo {self.label} não pode ser editado!') 53 | 54 | # return self.cleaned_data 55 | 56 | def clean_description(self): 57 | self.description = self.cleaned_data.get('description') 58 | self.label = self.fields["description"].label 59 | 60 | if self.description or self.description != self.instance.description: 61 | raise ValidationError(f'O campo {self.label} não pode ser editado!') 62 | 63 | return self.cleaned_data 64 | ``` 65 | -------------------------------------------------------------------------------- /backend/crm/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-30 01:55 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0012_alter_user_first_name_max_length'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Customer', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('rg', models.CharField(blank=True, max_length=10, null=True)), 23 | ('cpf', models.CharField(blank=True, max_length=11, null=True)), 24 | ('cep', models.CharField(blank=True, max_length=8, null=True)), 25 | ('address', models.CharField(blank=True, max_length=100, null=True)), 26 | ('active', models.BooleanField(default=True)), 27 | ('seller', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seller_customers', to=settings.AUTH_USER_MODEL)), 28 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | 'verbose_name': 'cliente', 32 | 'verbose_name_plural': 'clientes', 33 | 'ordering': ('user__first_name',), 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='Comission', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('percentage', models.DecimalField(decimal_places=2, max_digits=5)), 41 | ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comissions', to='auth.group')), 42 | ], 43 | options={ 44 | 'verbose_name': 'comissão', 45 | 'verbose_name_plural': 'comissões', 46 | 'ordering': ('group__name',), 47 | }, 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /backend/order/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-06-11 10:07 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Department', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=255, unique=True, verbose_name='nome')), 22 | ], 23 | options={ 24 | 'verbose_name': 'Departamento', 25 | 'verbose_name_plural': 'Departamentos', 26 | 'ordering': ('name',), 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='Employee', 31 | fields=[ 32 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='department_employees', to='order.department', verbose_name='departamento')), 34 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_employees', to=settings.AUTH_USER_MODEL, verbose_name='usuário')), 35 | ], 36 | options={ 37 | 'verbose_name': 'Funcionário', 38 | 'verbose_name_plural': 'Funcionários', 39 | 'ordering': ('user__first_name',), 40 | }, 41 | ), 42 | migrations.CreateModel( 43 | name='Order', 44 | fields=[ 45 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('title', models.CharField(max_length=255, verbose_name='título')), 47 | ('employee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='order.employee', verbose_name='funcionário')), 48 | ], 49 | options={ 50 | 'verbose_name': 'Pedido', 51 | 'verbose_name_plural': 'Pedidos', 52 | 'ordering': ('title',), 53 | }, 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /.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 | *.cast 139 | 140 | -------------------------------------------------------------------------------- /passo-a-passo/21_chain_list.md: -------------------------------------------------------------------------------- 1 | # Django Experience #21 - Chain List Queryset - Encadeando consultas 2 | 3 | 4 | 5 | 6 | 7 | ## Problema 8 | 9 | Eu preciso juntar todos os **filmes** e **videos** numa única lista. 10 | 11 | Mas os filmes estão no models `Movie` e os videos no models `Video`. 12 | 13 | ## Possível solução 14 | 15 | Antes vamos inserir alguns dados. 16 | 17 | ### Inserindo alguns dados 18 | 19 | Primeiro vamos deletar alguns dados. 20 | 21 | ``` 22 | python manage.py shell_plus 23 | ``` 24 | 25 | ```python 26 | Movie.objects.all().delete() 27 | Video.objects.all().delete() 28 | 29 | titles = ['Matrix', 'Star Wars IV', 'Avengers'] 30 | movies = [Movie(title=title, rating=5, like=True, censure=14) for title in titles] 31 | Movie.objects.bulk_create(movies) 32 | 33 | titles = [ 34 | ('A Essência do Django', 2021), 35 | ('Mini curso Entendendo Django REST framework', 2022), 36 | ('Django Ninja API REST', 2022), 37 | ('Matrix', 1999) 38 | ] 39 | videos = [Video(title=title[0], release_year=title[1]) for title in titles] 40 | Video.objects.bulk_create(videos) 41 | ``` 42 | 43 | ### Union 44 | 45 | Primeiro vamos tentar com [union](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#union). 46 | 47 | [https://docs.djangoproject.com/en/4.0/ref/models/querysets/#union](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#union) 48 | 49 | 50 | ```python 51 | movies = Movie.objects.values_list('title') 52 | videos = Video.objects.values_list('title') 53 | 54 | movies.count() 55 | 56 | videos.count() 57 | 58 | qs = movies.union(videos).order_by('title') 59 | qs 60 | ``` 61 | 62 | **Obs:** Union **não** funciona no SQLite. 63 | 64 | 65 | ### itertools.chain 66 | 67 | Então vamos tentar com [chain](https://docs.python.org/3/library/itertools.html#itertools.chain) do *itertools*. 68 | 69 | [https://docs.python.org/3/library/itertools.html#itertools.chain](https://docs.python.org/3/library/itertools.html#itertools.chain) 70 | 71 | 72 | ```python 73 | from itertools import chain 74 | 75 | list(chain(movies, videos)) 76 | ``` 77 | 78 | Também podemos fazer 79 | 80 | ```python 81 | movies = Movie.objects.all() 82 | videos = Video.objects.all() 83 | 84 | items = list(chain(movies, videos)) 85 | 86 | for item in items: 87 | try: 88 | print(item.title, item.rating) 89 | except AttributeError: 90 | print(item.title, item.release_year) 91 | ``` 92 | 93 | Mas não é uma boa solução. 94 | 95 | Então façamos 96 | 97 | ```python 98 | movies = Movie.objects.values('title', 'rating') 99 | videos = Video.objects.values('title', 'release_year') 100 | 101 | list(chain(movies, videos)) 102 | ``` 103 | -------------------------------------------------------------------------------- /backend/video/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import TestCase 4 | 5 | from .models import Video 6 | 7 | 8 | class VideoTest(TestCase): 9 | 10 | def setUp(self): 11 | self.payload = { 12 | "title": "Matrix", 13 | "release_year": 1999 14 | } 15 | 16 | def test_video_create(self): 17 | response = self.client.post( 18 | '/api/v1/videos/', 19 | data=self.payload, 20 | content_type='application/json' 21 | ) 22 | resultado = json.loads(response.content) 23 | esperado = { 24 | "data": { 25 | "id": 1, 26 | **self.payload 27 | } 28 | } 29 | self.assertEqual(esperado, resultado) 30 | 31 | def test_video_list(self): 32 | Video.objects.create(**self.payload) 33 | 34 | response = self.client.get( 35 | '/api/v1/videos/', 36 | content_type='application/json' 37 | ) 38 | resultado = json.loads(response.content) 39 | esperado = { 40 | "data": [ 41 | { 42 | "id": 1, 43 | **self.payload 44 | } 45 | ] 46 | } 47 | self.assertEqual(esperado, resultado) 48 | 49 | def test_video_detail(self): 50 | Video.objects.create(**self.payload) 51 | 52 | response = self.client.get( 53 | '/api/v1/videos/1/', 54 | content_type='application/json' 55 | ) 56 | resultado = json.loads(response.content) 57 | esperado = { 58 | "data": { 59 | "id": 1, 60 | **self.payload 61 | } 62 | } 63 | self.assertEqual(esperado, resultado) 64 | 65 | def test_video_update(self): 66 | Video.objects.create(**self.payload) 67 | 68 | data = { 69 | "title": "Matrix 2" 70 | } 71 | 72 | response = self.client.post( 73 | '/api/v1/videos/1/', 74 | data=data, 75 | content_type='application/json' 76 | ) 77 | resultado = json.loads(response.content) 78 | esperado = { 79 | "data": { 80 | "id": 1, 81 | "title": "Matrix 2", 82 | "release_year": 1999 83 | } 84 | } 85 | self.assertEqual(esperado, resultado) 86 | 87 | def test_video_delete(self): 88 | Video.objects.create(**self.payload) 89 | 90 | response = self.client.delete( 91 | '/api/v1/videos/1/', 92 | content_type='application/json' 93 | ) 94 | resultado = json.loads(response.content) 95 | esperado = {"data": "Item deletado com sucesso."} 96 | 97 | self.assertEqual(esperado, resultado) 98 | -------------------------------------------------------------------------------- /passo-a-passo/23_modelchoice.md: -------------------------------------------------------------------------------- 1 | # Django Experience #23 - ModelChoiceField 2 | 3 | 4 | 5 | 6 | 7 | ## Criando dados para os Pedidos 8 | 9 | ``` 10 | python manage.py seed order --number=15 11 | ``` 12 | 13 | 14 | ``` 15 | python manage.py shell_plus 16 | ``` 17 | 18 | 19 | ```python 20 | from random import choice 21 | 22 | 23 | Department.objects.all().delete() 24 | Employee.objects.all().delete() 25 | Order.objects.all().delete() 26 | 27 | departments = ('Vendas', 'Financeiro', 'RH') 28 | objs = [Department(name=name) for name in departments] 29 | Department.objects.bulk_create(objs) 30 | 31 | 32 | users = User.objects.exclude(username='admin') 33 | 34 | for user in users: 35 | user.username = user.first_name.lower() 36 | user.email = f'{user.first_name.lower()}@example.net' 37 | user.is_staff = True 38 | user.save() 39 | 40 | departments = Department.objects.all() 41 | 42 | for user in users: 43 | Employee.objects.create(department=choice(departments), user=user) 44 | ``` 45 | 46 | ## O formulário 47 | 48 | ```python 49 | from django import forms 50 | 51 | from .models import Order 52 | 53 | 54 | class OrderForm(forms.ModelForm): 55 | required_css_class = 'required' 56 | 57 | employee = forms.ModelChoiceField( 58 | label='Funcionário', 59 | queryset=None, 60 | ) 61 | 62 | class Meta: 63 | model = Order 64 | fields = '__all__' 65 | 66 | def __init__(self, user, *args, **kwargs): 67 | super().__init__(*args, **kwargs) 68 | 69 | # Retorna o funcionário logado. 70 | employee = user.user_employees.first() 71 | 72 | # Retorna o departamento do funcionário logado. 73 | department = employee.department 74 | 75 | # Retorna somente os funcionários do meu departamento. 76 | employees = Employee.objects.filter(department=department) 77 | 78 | # Altera o filtro de ModelChoiceField. 79 | self.fields['employee'].queryset = employees 80 | 81 | # Adiciona classe form-control nos campos. 82 | for field_name, field in self.fields.items(): 83 | field.widget.attrs['class'] = 'form-control' 84 | 85 | ``` 86 | 87 | ## A views 88 | 89 | ```python 90 | from django.contrib.auth.decorators import login_required 91 | from django.shortcuts import redirect, render 92 | 93 | from .forms import OrderForm 94 | from .models import Order 95 | 96 | 97 | def order_list(request): 98 | template_name = 'order/order_list.html' 99 | object_list = Order.objects.all() 100 | context = {'object_list': object_list} 101 | return render(request, template_name, context) 102 | 103 | 104 | @login_required 105 | def order_create(request): 106 | template_name = 'order/order_form.html' 107 | # Passa o usuário logado no formulário. 108 | form = OrderForm(request.user, request.POST or None) 109 | 110 | if request.method == 'POST': 111 | if form.is_valid(): 112 | form.save() 113 | return redirect('order:order_list') 114 | 115 | context = {'form': form} 116 | return render(request, template_name, context) 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # Django Experience 2 | 3 | Tutorial Django Experience 2022 4 | 5 | ## Como rodar o projeto? 6 | 7 | * Clone esse repositório. 8 | * Crie um virtualenv com Python 3. 9 | * Ative o virtualenv. 10 | * Instale as dependências. 11 | * Rode as migrações. 12 | 13 | ``` 14 | git clone https://github.com/rg3915/django-experience.git 15 | cd django-experience 16 | python -m venv .venv 17 | source .venv/bin/activate 18 | pip install -r requirements.txt 19 | python contrib/env_gen.py 20 | python manage.py migrate 21 | python manage.py createsuperuser --username="admin" --email="" 22 | ``` 23 | 24 | ## Features da aplicação 25 | 26 | * Renderização de templates na app `todo`. 27 | * API REST feita com Django puro na app `video`. 28 | * Django REST framework nas apps `example`, `hotel`, `movie` e `school`. 29 | * [Salvando dados extra](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/08_drf_salvando_dados_extra.md) com [perform_create](https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#associating-snippets-with-users) 30 | * **Dica:** [Reescrevendo o Admin do User](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/10_reescrevendo_admin_user.md) 31 | * [Editando mensagens de erro no DRF](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/12_drf_editando_mensagens_erro.md) 32 | * **Dica:** [Adicionando Grupos e Permissões](https://github.com/rg3915/django-experience/blob/main/passo-a-passo/14_grupos_permissoes.md) 33 | 34 | 35 | ## Passo a passo 36 | 37 | * [#01 - Como criar um projeto Django completo + API REST + Render Template](passo-a-passo/01_django_full_template_como_criar_um_projeto_django_completo_api_rest_render_template.md) 38 | * [#02 - Criando API com Django SEM DRF - parte 2](passo-a-passo/02_criando_api_com_django_sem_drf_parte2.md) 39 | * [#03 - DRF: Entendendo Rotas](passo-a-passo/03_drf_entendendo_rotas.md) 40 | * [#04 - DRF: Entendendo Serializers](passo-a-passo/04_drf_entendendo_serializers.md) 41 | * [#05 - DRF: Serializers mais rápido](passo-a-passo/05_drf_serializers_mais_rapido.md) 42 | * [#06 - DRF: Entendendo Viewsets](passo-a-passo/06_drf_entendendo_viewsets.md) 43 | * [#07 - DRF: APIView e o problema do get_extra_actions](passo-a-passo/07_drf_apiview_get_extra_actions.md) 44 | * [#08 - DRF: Salvando dados extra](passo-a-passo/08_drf_salvando_dados_extra.md) 45 | * [#09 - DRF: Entendendo Autenticação](passo-a-passo/09_drf_entendendo_autenticacao.md) 46 | * [#10 - Dica: Reescrevendo o Admin do User](passo-a-passo/10_reescrevendo_admin_user.md) 47 | * [#11 - DRF: Entendendo Permissões](passo-a-passo/11_drf_entendendo_permissoes.md) 48 | * [#12 - Dica: Editando as mensagens de erro](passo-a-passo/12_drf_editando_mensagens_erro.md) 49 | * [#13 - DRF: Fix Permissão](passo-a-passo/13_drf_fix_permissao.md) 50 | * [#14 - DRF: Grupos e Permissões](passo-a-passo/14_grupos_permissoes.md) 51 | * [#15 - DRF: Entendendo Validações](passo-a-passo/15_drf_entendendo_validacoes.md) 52 | * [Entendendo o Django REST framework](passo-a-passo/16_entendendo_drf.md) 53 | * [#17 - Novo comando](passo-a-passo/17_novo_comando.md) 54 | * [#18 - Como rodar o projeto Django Experience no Windows 10 com PowerShell](https://youtu.be/clDiMuITKCs) 55 | * [#19 - Dica: O problema do readonly e validação no Django](passo-a-passo/19_readonly_validation.md) 56 | -------------------------------------------------------------------------------- /passo-a-passo/15_drf_entendendo_validacoes.md: -------------------------------------------------------------------------------- 1 | # Django Experience #15 - DRF: Entendendo Validações 2 | 3 | 4 | Doc: https://www.django-rest-framework.org/api-guide/validators/ 5 | 6 | Podemos fazer a validação por cada campo, ou pelo modelo em geral. 7 | 8 | ```python 9 | # movie/api/serializers.py 10 | class MovieSerializer(serializers.ModelSerializer): 11 | ... 12 | 13 | class Meta: 14 | model = Movie 15 | fields = ( 16 | 'id', 17 | 'title', 18 | 'sinopse', 19 | 'rating', 20 | 'censure', 21 | 'like', 22 | 'created', 23 | 'category' 24 | ) 25 | 26 | def validate_title(self, value): 27 | if 'lorem' in value.lower(): 28 | raise serializers.ValidationError('Lorem não pode.') 29 | return value 30 | 31 | def validate(self, data): 32 | if 'lorem' in data['title'].lower(): 33 | raise serializers.ValidationError('Lorem não pode.') 34 | return data 35 | ``` 36 | 37 | Um outro exemplo interessante é a comparação entra datas. 38 | 39 | Considere a app `Hotel`. 40 | 41 | ```python 42 | # hotel/models.py 43 | from django.db import models 44 | 45 | 46 | class Hotel(models.Model): 47 | name = models.CharField(max_length=32) 48 | start_date = models.DateField(null=True, blank=True) # ou checkin 49 | end_date = models.DateField(null=True, blank=True) # ou checkout 50 | created = models.DateTimeField(auto_now_add=True) 51 | 52 | def __str__(self): 53 | return f'{self.name}' 54 | 55 | class Meta: 56 | verbose_name = 'Hotel' 57 | verbose_name_plural = 'Hotéis' 58 | ``` 59 | 60 | ```python 61 | # hotel/api/viewsets.py 62 | from rest_framework import viewsets 63 | from rest_framework.permissions import AllowAny 64 | 65 | from backend.hotel.api.serializers import HotelSerializer 66 | from backend.hotel.models import Hotel 67 | 68 | 69 | class HotelViewSet(viewsets.ModelViewSet): 70 | queryset = Hotel.objects.all() 71 | serializer_class = HotelSerializer 72 | permission_classes = (AllowAny,) 73 | ``` 74 | 75 | ```python 76 | # hotel/api/serializers.py 77 | from rest_framework import serializers 78 | 79 | from backend.hotel.models import Hotel 80 | 81 | 82 | class HotelSerializer(serializers.ModelSerializer): 83 | 84 | class Meta: 85 | model = Hotel 86 | fields = '__all__' 87 | 88 | def validate(self, data): 89 | if data['start_date'] > data['end_date']: 90 | raise serializers.ValidationError('A data inicial deve ser anterior ou igual a data inicial!') 91 | return data 92 | ``` 93 | 94 | 95 | 96 | https://www.django-rest-framework.org/api-guide/fields/#integerfield 97 | 98 | Temos também as validações específicas de cada campo. 99 | 100 | ```python 101 | # movie/api/serializers.py 102 | class MovieSerializer(serializers.ModelSerializer): 103 | censure = serializers.IntegerField(min_value=0) 104 | ``` 105 | 106 | ## Custom Validators (Validações Customizadas) 107 | 108 | Também podemos criar nossas próprias customizações. 109 | 110 | ```python 111 | # movie/api/serializers.py 112 | def positive_only_validator(value): 113 | if value == 0: 114 | raise serializers.ValidationError('Zero não é um valor permitido.') 115 | 116 | 117 | class MovieSerializer(serializers.ModelSerializer): 118 | censure = serializers.IntegerField(min_value=0, validators=[positive_only_validator]) 119 | ``` 120 | -------------------------------------------------------------------------------- /backend/crm/api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import serializers 3 | 4 | from backend.crm.models import Comission, Customer 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | 9 | class Meta: 10 | model = User 11 | fields = ( 12 | 'id', 13 | 'username', 14 | 'first_name', 15 | 'last_name', 16 | 'email', 17 | # 'password', 18 | # 'last_login', 19 | # 'is_superuser', 20 | # 'is_staff', 21 | # 'is_active', 22 | # 'date_joined', 23 | # 'groups', 24 | # 'user_permissions', 25 | ) 26 | ref_name = 'Custom User Serializer' 27 | 28 | 29 | class CustomerSerializer(serializers.ModelSerializer): 30 | user = UserSerializer() 31 | 32 | class Meta: 33 | model = Customer 34 | fields = ('id', 'rg', 'cpf', 'cep', 'address', 'active', 'user', 'seller') 35 | depth = 1 # expande todas as FK 36 | 37 | def to_representation(self, instance): 38 | ''' 39 | Representação personalizada para RG, CPF e CEP. 40 | ''' 41 | data = super(CustomerSerializer, self).to_representation(instance) 42 | 43 | data['rg'] = f"{instance.rg[:2]}.{instance.rg[2:5]}.{instance.rg[5:8]}-{instance.rg[8:]}" 44 | data['cpf'] = f"{instance.cpf[:3]}.{instance.cpf[3:6]}.{instance.cpf[6:9]}-{instance.cpf[9:]}" 45 | if instance.cep: 46 | data['cep'] = f"{instance.cep[:5]}-{instance.cep[5:]}" 47 | 48 | return data 49 | 50 | 51 | class CustomerCreateSerializer(serializers.ModelSerializer): 52 | 53 | class Meta: 54 | model = Customer 55 | fields = ('user', 'rg', 'cpf', 'cep', 'address') 56 | 57 | 58 | def only_numbers_validator(value): 59 | if not value.isnumeric(): 60 | raise serializers.ValidationError('Digitar somente números.') 61 | 62 | 63 | class CustomerUpdateSerializer(serializers.ModelSerializer): 64 | user = UserSerializer() 65 | cpf = serializers.CharField(validators=[only_numbers_validator]) 66 | cep = serializers.CharField(validators=[only_numbers_validator]) 67 | 68 | class Meta: 69 | model = Customer 70 | fields = ('user', 'seller', 'rg', 'cpf', 'cep', 'address') 71 | 72 | def update(self, instance, validated_data): 73 | # Edita user 74 | if 'user' in validated_data: 75 | user = validated_data.pop('user') 76 | # instance.user.username = user.get('username') 77 | # instance.user.first_name = user.get('first_name') 78 | # instance.user.last_name = user.get('last_name') 79 | # instance.user.email = user.get('email') 80 | 81 | for attr, value in user.items(): 82 | setattr(instance.user, attr, value) 83 | 84 | instance.user.save() 85 | 86 | # Edita seller 87 | if 'seller' in validated_data: 88 | seller = validated_data.pop('seller') 89 | 90 | for attr, value in seller.items(): 91 | setattr(instance.seller, attr, value) 92 | 93 | instance.seller.save() 94 | 95 | # Edita demais campos 96 | for attr, value in validated_data.items(): 97 | setattr(instance, attr, value) 98 | 99 | instance.save() 100 | 101 | return instance 102 | 103 | 104 | class ComissionSerializer(serializers.ModelSerializer): 105 | 106 | class Meta: 107 | model = Comission 108 | fields = ('group', 'percentage') 109 | -------------------------------------------------------------------------------- /passo-a-passo/05_drf_serializers_mais_rapido.md: -------------------------------------------------------------------------------- 1 | # Django Experience #05 - DRF: Serializers mais rápido 2 | 3 | Baseado em [https://hakibenita.com/django-rest-framework-slow](https://hakibenita.com/django-rest-framework-slow) 4 | 5 | Editar `settings.py` 6 | 7 | ```python 8 | # settings.py 9 | 10 | REST_FRAMEWORK = { 11 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 12 | 'PAGE_SIZE': 5000 13 | } 14 | ``` 15 | 16 | Editar `movie/models.py` 17 | 18 | ```python 19 | # movie/models.py 20 | class Movie(models.Model): 21 | ... 22 | 23 | def to_dict(self): 24 | return { 25 | 'id': self.id, 26 | 'title': self.title, 27 | 'sinopse': self.sinopse, 28 | 'rating': self.rating, 29 | 'like': self.like, 30 | 'created': self.created, 31 | # 'category': self.category.id, 32 | } 33 | 34 | ``` 35 | 36 | Editar `movie/api/serializers.py` 37 | 38 | ```python 39 | # movie/api/serializers.py 40 | class MovieReadOnlySerializer(serializers.ModelSerializer): 41 | # category = CategorySerializer(required=False) 42 | 43 | class Meta: 44 | model = Movie 45 | fields = ('id', 'title', 'sinopse', 'rating', 'like', 'created') 46 | read_only_fields = fields 47 | 48 | ``` 49 | 50 | Editar `movie/api/viewsets.py` 51 | 52 | ```python 53 | # movie/api/viewsets.py 54 | class MovieViewSet(viewsets.ModelViewSet): 55 | ... 56 | 57 | @action(detail=False, methods=['get']) 58 | def movies_readonly(self, request, pk=None): 59 | movies = Movie.objects.all() 60 | 61 | page = self.paginate_queryset(movies) 62 | if page is not None: 63 | serializer = MovieReadOnlySerializer(page, many=True) 64 | return self.get_paginated_response(serializer.data) 65 | 66 | serializer = MovieReadOnlySerializer(movies, many=True) 67 | return Response(serializer.data) 68 | 69 | @action(detail=False, methods=['get']) 70 | def movies_regular_readonly(self, request, pk=None): 71 | movies = Movie.objects.all() 72 | 73 | page = self.paginate_queryset(movies) 74 | if page is not None: 75 | serializer = [movie.to_dict() for movie in page] 76 | return self.get_paginated_response(serializer) 77 | 78 | serializer = [movie.to_dict() for movie in movies] 79 | return Response(serializer) 80 | 81 | ``` 82 | 83 | Editar `client.py` 84 | 85 | ```python 86 | # client.py 87 | import timeit 88 | 89 | import requests 90 | 91 | base_url = 'http://localhost:8000/api/v1' 92 | 93 | url_video = f'{base_url}/videos/' 94 | url_movie = f'{base_url}/movies/?format=json' 95 | url_movie_readonly = f'{base_url}/movies/movies_readonly/?format=json' 96 | url_movie_regular_readonly = f'{base_url}/movies/movies_regular_readonly/?format=json' 97 | 98 | 99 | def get_result(url): 100 | start_time = timeit.default_timer() 101 | r = requests.get(url) 102 | print('status_code:', r.status_code) 103 | end_time = timeit.default_timer() 104 | print('time:', round(end_time - start_time, 3)) 105 | print() 106 | 107 | 108 | if __name__ == '__main__': 109 | get_result(url_video) 110 | get_result(url_movie) 111 | get_result(url_movie_readonly) 112 | get_result(url_movie_regular_readonly) 113 | ``` 114 | 115 | Rodando... 116 | 117 | ``` 118 | pip install requests 119 | python client.py 120 | ``` 121 | 122 | Resultado: 123 | 124 | ``` 125 | status_code: 200 126 | time: 0.114 127 | 128 | status_code: 200 129 | time: 4.969 130 | 131 | status_code: 200 132 | time: 0.504 133 | 134 | status_code: 200 135 | time: 0.151 136 | ``` 137 | -------------------------------------------------------------------------------- /passo-a-passo/07_drf_apiview_get_extra_actions.md: -------------------------------------------------------------------------------- 1 | # Django Experience #07 - DRF: APIView e o problema do get_extra_actions 2 | 3 | 4 | ## Criando uma app para o exemplo 5 | 6 | Antes vamos considerar a app `example`, um model `Example` e um campo `title`. 7 | 8 | ``` 9 | python manage.py dr_scaffold example Example title:charfield 10 | mv example backend 11 | mkdir backend/example/api 12 | mv backend/example/serializers.py backend/example/api 13 | mv backend/example/views.py backend/example/api/viewsets.py 14 | ``` 15 | 16 | Edite `example/apps.py` 17 | 18 | ```python 19 | name = 'backend.example' 20 | ``` 21 | 22 | Edite `settings.py` 23 | 24 | ```python 25 | INSTALLED_APPS = [ 26 | # my apps 27 | 'backend.core', 28 | 'backend.example', 29 | ... 30 | ] 31 | ``` 32 | 33 | Edite `urls.py` principal 34 | 35 | ```python 36 | urlpatterns = [ 37 | path('', include('backend.core.urls', namespace='core')), 38 | path('', include('backend.example.urls', namespace='example')), 39 | ... 40 | ] 41 | ``` 42 | 43 | Edite `example/admin.py` 44 | 45 | ```python 46 | from backend.example.models import Example 47 | ``` 48 | 49 | Edite `example/urls.py` 50 | 51 | ```python 52 | from django.urls import include, path 53 | from rest_framework import routers 54 | 55 | from backend.example.api.viewsets import ExampleViewSet 56 | 57 | app_name = 'example' 58 | 59 | router = routers.DefaultRouter() 60 | 61 | router.register(r'examples', ExampleViewSet, basename='example') 62 | 63 | urlpatterns = [ 64 | path('api/v1/', include(router.urls)), 65 | ] 66 | ``` 67 | 68 | Edite `example/api/serializers.py` 69 | 70 | ```python 71 | from backend.example.models import Example 72 | ``` 73 | 74 | Edite `example/api/viewsets.py` 75 | 76 | ```python 77 | from backend.example.models import Example 78 | from backend.example.api.serializers import ExampleSerializer 79 | ``` 80 | 81 | Finalmente rode 82 | 83 | ``` 84 | python manage.py makemigrations 85 | python manage.py migrate 86 | ``` 87 | 88 | 89 | ## O problema do get_extra_actions 90 | 91 | Existem muitos tutoriais na internet, inclusive na documentação, tentando exemplificar de uma forma mais simples, o uso da [APIView](https://www.django-rest-framework.org/api-guide/views/#class-based-views). 92 | Como no exemplo a seguir: 93 | 94 | ```python 95 | class ExampleView(APIView): 96 | 97 | def get(self, request, format=None): 98 | content = { 99 | 'user': str(request.user), # `django.contrib.auth.User` instance. 100 | 'auth': str(request.auth), # None 101 | } 102 | return Response(content) 103 | ``` 104 | 105 | 106 | Uma coisa que não está escrito lá, é que ao usá-lo, ele gera o seguinte erro: 107 | 108 | ``` 109 | AttributeError: 'function' object has no attribute 'get_extra_actions' 110 | ``` 111 | 112 | E mesmo que você tente implementar esse método, por exemplo: 113 | 114 | ```python 115 | class ExampleView(APIView): 116 | ... 117 | 118 | @classmethod 119 | def get_extra_actions(cls): 120 | return [] 121 | ``` 122 | 123 | De nada adianta. Na verdade o problema está no **routers**, que não reconhece esse método, mesmo que você o implemente. Ou seja, se você usar 124 | 125 | ```python 126 | from backend.example.api.viewsets import ExampleView 127 | 128 | router = routers.DefaultRouter() 129 | 130 | router.register(r'examples', ExampleView, basename='example') 131 | ``` 132 | 133 | ... ele não vai funcionar. 134 | 135 | Qual é a solução? 136 | 137 | ```python 138 | urlpatterns = [ 139 | # path('api/v1/', include(router.urls)), 140 | path('api/v1/examples/', ExampleView.as_view()), 141 | ] 142 | ``` 143 | 144 | E não precisa implementar o `get_extra_actions`. 145 | 146 | Resolvido o problema. :) 147 | 148 | -------------------------------------------------------------------------------- /backend/crm/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status, viewsets 2 | from rest_framework.exceptions import ValidationError as DRFValidationError 3 | from rest_framework.filters import SearchFilter 4 | from rest_framework.permissions import BasePermission 5 | 6 | from backend.crm.api.serializers import ( 7 | ComissionSerializer, 8 | CustomerCreateSerializer, 9 | CustomerSerializer, 10 | CustomerUpdateSerializer 11 | ) 12 | from backend.crm.models import Comission, Customer 13 | 14 | 15 | class CustomerViewSet(viewsets.ModelViewSet): 16 | # queryset = Customer.objects.all() 17 | # serializer_class = CustomerSerializer 18 | filter_backends = (SearchFilter,) 19 | search_fields = ( 20 | 'user__first_name', 21 | 'user__last_name', 22 | 'user__email', 23 | 'seller__first_name', 24 | 'seller__last_name', 25 | 'seller__email', 26 | 'rg', 27 | 'cpf', 28 | 'cep', 29 | 'address', 30 | ) 31 | 32 | def get_serializer_class(self): 33 | if self.action == 'create': 34 | return CustomerCreateSerializer 35 | 36 | if self.action == 'update' or self.action == 'partial_update': 37 | return CustomerUpdateSerializer 38 | 39 | return CustomerSerializer 40 | 41 | def is_seller(self, seller): 42 | if seller: 43 | groups_list = seller.groups.values_list('name', flat=True) 44 | return True if 'Vendedor' in groups_list else False 45 | return False 46 | 47 | def get_queryset(self): 48 | ''' 49 | Vendedor só pode ver os seus clientes. 50 | ''' 51 | seller = self.request.user 52 | queryset = Customer.objects.all() 53 | 54 | active = self.request.query_params.get('active') 55 | 56 | if active is not None: 57 | queryset = queryset.filter(active=active) 58 | 59 | if self.is_seller(seller): 60 | queryset = queryset.filter(seller=seller) 61 | return queryset 62 | 63 | return queryset 64 | 65 | def perform_create(self, serializer): 66 | ''' 67 | Ao criar um objeto, se for Vendedor, então define seller com o usuário logado. 68 | ''' 69 | seller = self.request.user 70 | 71 | if self.is_seller(seller): 72 | serializer.save(seller=seller) 73 | else: 74 | serializer.save() 75 | 76 | def perform_update(self, serializer): 77 | ''' 78 | O Vendedor só pode editar os clientes dele. 79 | ''' 80 | instance = self.get_object() 81 | 82 | if self.is_seller(instance.seller) and self.request.user == instance.seller: 83 | serializer.save() 84 | else: 85 | raise DRFValidationError('Você não tem permissão para editar este registro.') 86 | 87 | 88 | class NotSellerPermission(BasePermission): 89 | message = 'Você não tem permissão para visualizar este registro.' 90 | 91 | def is_seller(self, seller): 92 | if seller: 93 | groups_list = seller.groups.values_list('name', flat=True) 94 | return True if 'Vendedor' in groups_list else False 95 | return False 96 | 97 | def has_permission(self, request, view): 98 | seller = request.user 99 | 100 | if self.is_seller(seller): 101 | response = { 102 | 'message': self.message, 103 | 'status_code': status.HTTP_403_FORBIDDEN 104 | } 105 | raise DRFValidationError(response) 106 | else: 107 | return True 108 | 109 | 110 | class ComissionViewSet(viewsets.ModelViewSet): 111 | queryset = Comission.objects.all() 112 | serializer_class = ComissionSerializer 113 | permission_classes = (NotSellerPermission,) 114 | -------------------------------------------------------------------------------- /passo-a-passo/20_marvel_api.md: -------------------------------------------------------------------------------- 1 | # Django Experience #20 - Consumido Marvel Comics API 2 | 3 | 4 | 5 | 6 | 7 | A api da Marvel está em https://developer.marvel.com/docs 8 | 9 | Leia também https://developer.marvel.com/documentation/authorization 10 | 11 | 12 | ## Autorização 13 | 14 | Gere um apikey no site da Marvel. 15 | 16 | ``` 17 | publicKey: 1234 18 | privateKey: abcd 19 | ts: 1 20 | ``` 21 | 22 | 23 | A partir da documentaçãoi em https://developer.marvel.com/documentation/authorization 24 | 25 | Em Authentication for Server-Side Applications temos que vamos precisar de um 26 | 27 | **ts** é timestamp e um **hash** 28 | 29 | `md5(ts+privateKey+publicKey)` 30 | 31 | Você precisará gerar um outro token em md5. 32 | 33 | A partir do site https://www.md5hashgenerator.com/ faça a concatenação de 34 | 35 | `md5(ts+privateKey+publicKey)` 36 | 37 | Ex: `1abcd1234` 38 | 39 | Resultado: `ffd275c5130566a2916217b101f26150` 40 | 41 | ``` 42 | ts=1 43 | apikey=1234 44 | hash=ffd275c5130566a2916217b101f26150 45 | ``` 46 | 47 | Exemplo de endpoint: 48 | 49 | http://gateway.marvel.com/v1/public/characters?ts=1&apikey=1234&hash=ffd275c5130566a2916217b101f26150 50 | 51 | 52 | Veja os demais endpoints em https://developer.marvel.com/docs 53 | 54 | 55 | ## Hash md5 56 | 57 | 58 | ```python 59 | import hashlib 60 | 61 | def compute_md5_hash(my_string): 62 | ''' 63 | Converte string em md5 hash. 64 | https://stackoverflow.com/a/13259879/802542 65 | ''' 66 | m = hashlib.md5() 67 | m.update(my_string.encode('utf-8')) 68 | return m.hexdigest() 69 | 70 | 71 | publicKey = 1234 72 | privateKey = 'abcd' 73 | ts = 1 74 | 75 | assert compute_md5_hash('1abcd1234') == 'ffd275c5130566a2916217b101f26150' 76 | ``` 77 | 78 | ## Requests 79 | 80 | Vamos usar o [Requests](https://docs.python-requests.org/en/latest/) para consumir a API. 81 | 82 | 83 | ## O serviço 84 | 85 | ```python 86 | # service.py 87 | # service.py 88 | import hashlib 89 | 90 | import requests 91 | from decouple import config 92 | from rich import print 93 | from rich.console import Console 94 | from rich.table import Table 95 | 96 | console = Console() 97 | 98 | 99 | def compute_md5_hash(my_string): 100 | ''' 101 | Converte string em md5 hash. 102 | https://stackoverflow.com/a/13259879/802542 103 | ''' 104 | m = hashlib.md5() 105 | m.update(my_string.encode('utf-8')) 106 | return m.hexdigest() 107 | 108 | 109 | def make_authorization(): 110 | ''' 111 | Gera os tokens de autorização. 112 | ''' 113 | publicKey = config('PUBLIC_KEY') 114 | privateKey = config('PRIVATE_KEY') 115 | ts = 1 116 | md5_hash = compute_md5_hash(f'{ts}{privateKey}{publicKey}') 117 | query_params = f'?ts={ts}&apikey={publicKey}&hash={md5_hash}' 118 | return query_params 119 | 120 | 121 | def main(url): 122 | url += make_authorization() 123 | with requests.Session() as session: 124 | response = session.get(url) 125 | print(response) 126 | characters = response.json()['data']['results'] 127 | 128 | table = Table(title='Marvel characters') 129 | headers = ( 130 | 'id', 131 | 'name', 132 | 'description', 133 | ) 134 | 135 | for header in headers: 136 | table.add_column(header) 137 | 138 | for character in characters: 139 | values = str(character['id']), str(character['name']), str(character['description']) # noqa E501 140 | table.add_row(*values) 141 | 142 | console.print(table) 143 | 144 | 145 | if __name__ == '__main__': 146 | endpoint = 'http://gateway.marvel.com/v1/public/characters' 147 | main(endpoint) 148 | ``` 149 | 150 | ``` 151 | python service.py 152 | ``` 153 | 154 | -------------------------------------------------------------------------------- /passo-a-passo/03_drf_entendendo_rotas.md: -------------------------------------------------------------------------------- 1 | # Django Experience #03 - DRF: Entendendo Rotas 2 | 3 | Doc: [Routers](https://www.django-rest-framework.org/api-guide/routers/) 4 | 5 | Vamos criar uma app chamada `movie` usando o [dr_scaffold](). 6 | 7 | ``` 8 | python manage.py dr_scaffold movie Movie \ 9 | title:charfield \ 10 | sinopse:charfield \ 11 | rating:positiveintegerfield \ 12 | like:booleanfield 13 | 14 | python manage.py dr_scaffold movie Category title:charfield 15 | ``` 16 | 17 | 18 | Não se esqueça de adicionar `movie` em `INSTALLED_APPS`. 19 | 20 | 21 | ## SimpleRouter 22 | 23 | Basicamente você precisa informar `prefix` e `viewset` na rota. 24 | 25 | ```python 26 | # movie/urls.py 27 | from django.urls import include, path 28 | from rest_framework import routers 29 | 30 | from movie.views import CategoryViewSet, MovieViewSet 31 | 32 | router = routers.SimpleRouter() 33 | 34 | # router.register(prefix, viewset) 35 | router.register(r'movies', MovieViewSet) 36 | router.register(r'categories', CategoryViewSet) 37 | 38 | urlpatterns = [ 39 | path("", include(router.urls)), 40 | ] 41 | ``` 42 | 43 | 44 | O `basename` é requerido quando você altera o `queryset` original, exemplo: 45 | 46 | ```python 47 | # movie/views.py 48 | class MovieViewSet(viewsets.ModelViewSet): 49 | # queryset = Movie.objects.all() 50 | serializer_class = MovieSerializer 51 | 52 | def get_queryset(self): 53 | return Movie.objects.filter(title__icontains='lorem') 54 | ``` 55 | 56 | Erro: 57 | 58 | ``` 59 | AssertionError: `basename` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute. 60 | ``` 61 | 62 | Então defina 63 | 64 | ```python 65 | # movie/urls.py 66 | router.register(r'movies', MovieViewSet, basename="movie") 67 | router.register(r'categories', CategoryViewSet, basename="category") 68 | ``` 69 | 70 | Por fim teremos as rotas: 71 | 72 | ``` 73 | URL Nome 74 | /movie/categories/ category-list 75 | /movie/categories// category-detail 76 | /movie/movies/ movie-list 77 | /movie/movies// movie-detail 78 | ``` 79 | 80 | O `basename` é usado para especificar a parte inicial do nome da view. 81 | 82 | 83 | ### Rota extra 84 | 85 | Em edite `views.py` 86 | 87 | ```python 88 | # movie/views.py 89 | class MovieViewSet(viewsets.ModelViewSet): 90 | # queryset = Movie.objects.all() 91 | serializer_class = MovieSerializer 92 | 93 | def get_queryset(self): 94 | return Movie.objects.filter(title__icontains='lorem') 95 | 96 | @action(detail=False, methods=['get']) 97 | def get_good_movies(self, request, pk=None): 98 | ''' 99 | Retorna somente filmes bons, com rating maior ou igual a 4. 100 | ''' 101 | movies = Movie.objects.filter(rating__gte=4) 102 | 103 | page = self.paginate_queryset(movies) 104 | if page is not None: 105 | serializer = self.get_serializer(page, many=True) 106 | return self.get_paginated_response(serializer.data) 107 | 108 | serializer = self.get_serializer(movies, many=True) 109 | return Response(serializer.data) 110 | ``` 111 | 112 | Acabamos de ganhar uma sub-rota 113 | 114 | ``` 115 | /movie/movies/get_good_movies/ movie-get-good-movies 116 | ``` 117 | 118 | Ler [https://www.django-rest-framework.org/api-guide/routers/#simplerouter](https://www.django-rest-framework.org/api-guide/routers/#simplerouter) 119 | 120 | 121 | 122 | ## DefaultRouter 123 | 124 | Este é semelhante ao `SimpleRouter`. A única diferença é que ele te oferece um formato de saída na rota, exemplo em `json`. 125 | 126 | ```python 127 | # movie/urls.py 128 | router = routers.DefaultRouter() 129 | ``` 130 | 131 | 132 | Exemplo: 133 | 134 | ``` 135 | http://localhost:8000/movie/movies/?format=json 136 | ``` 137 | 138 | -------------------------------------------------------------------------------- /backend/core/management/commands/create_data.py: -------------------------------------------------------------------------------- 1 | import string 2 | from random import choice 3 | 4 | from django.contrib.auth.models import Group, Permission, User 5 | from django.core.management.base import BaseCommand 6 | from django.utils.text import slugify 7 | from faker import Faker 8 | 9 | from backend.crm.models import Customer 10 | 11 | fake = Faker() 12 | 13 | 14 | def gen_digits(max_length): 15 | return str(''.join(choice(string.digits) for i in range(max_length))) 16 | 17 | 18 | def gen_email(first_name: str, last_name: str): 19 | first_name = slugify(first_name) 20 | last_name = slugify(last_name) 21 | email = f'{first_name}.{last_name}@email.com' 22 | return email 23 | 24 | 25 | def get_person(): 26 | name = fake.first_name() 27 | username = name.lower() 28 | first_name = name 29 | last_name = fake.last_name() 30 | email = gen_email(first_name, last_name) 31 | 32 | user = User.objects.create( 33 | username=username, 34 | first_name=first_name, 35 | last_name=last_name, 36 | email=email 37 | ) 38 | 39 | data = dict( 40 | user=user, 41 | rg=gen_digits(9), 42 | cpf=gen_digits(11), 43 | cep=gen_digits(8), 44 | ) 45 | return data 46 | 47 | 48 | def create_persons(): 49 | aux_list = [] 50 | for _ in range(6): 51 | data = get_person() 52 | obj = Customer(**data) 53 | aux_list.append(obj) 54 | Customer.objects.bulk_create(aux_list) 55 | 56 | 57 | def add_permissions(group_name, permissions): 58 | ''' 59 | Adiciona os grupos. 60 | ''' 61 | group = Group.objects.get(name=group_name) 62 | permissions = Permission.objects.filter(codename__in=permissions) 63 | # Remove todas as permissões. 64 | group.permissions.clear() 65 | # Adiciona novas permissões. 66 | for perm in permissions: 67 | group.permissions.add(perm) 68 | 69 | 70 | def create_users_groups_and_permissions(): 71 | ''' 72 | Criar alguns usuários, com grupos e permissões. 73 | ''' 74 | 75 | # Cria os grupos 76 | groups = ['Criador', 'Editor', 'Gerente', 'Infantil'] 77 | [Group.objects.get_or_create(name=group) for group in groups] 78 | 79 | # Adiciona permissões aos grupos 80 | add_permissions('Criador', ['add_movie']) 81 | add_permissions('Editor', ['add_movie', 'change_movie']) 82 | add_permissions('Gerente', ['add_movie', 'change_movie', 'delete_movie']) 83 | 84 | # Cria os usuários 85 | users = ['regis', 'criador', 'editor', 'gerente', 'pedrinho'] 86 | 87 | for user in users: 88 | obj = User.objects.create_user( 89 | username=user, 90 | first_name=user.title(), 91 | email=f'{user}@email.com', 92 | is_staff=True, 93 | ) 94 | obj.set_password('d') 95 | obj.save() 96 | 97 | # Associa os usuários aos grupos 98 | criador = User.objects.get(username='criador') 99 | editor = User.objects.get(username='editor') 100 | gerente = User.objects.get(username='gerente') 101 | pedrinho = User.objects.get(username='pedrinho') 102 | 103 | grupo_criador = Group.objects.get(name='Criador') 104 | criador.groups.clear() 105 | criador.groups.add(grupo_criador) 106 | 107 | grupo_editor = Group.objects.get(name='Editor') 108 | editor.groups.clear() 109 | editor.groups.add(grupo_editor) 110 | 111 | grupo_gerente = Group.objects.get(name='Gerente') 112 | gerente.groups.clear() 113 | gerente.groups.add(grupo_gerente) 114 | 115 | grupo_infantil = Group.objects.get(name='Infantil') 116 | pedrinho.groups.clear() 117 | pedrinho.groups.add(grupo_infantil) 118 | 119 | 120 | class Command(BaseCommand): 121 | help = "Create data." 122 | 123 | def handle(self, *args, **options): 124 | # Deleta os usuários 125 | User.objects.exclude(username='admin').delete() 126 | 127 | create_persons() 128 | create_users_groups_and_permissions() 129 | -------------------------------------------------------------------------------- /passo-a-passo/22_postgresql_docker.md: -------------------------------------------------------------------------------- 1 | # Django Experience #22 - PostgreSQL + Docker + Portainer + pgAdmin + Django local 2 | 3 | 4 | 5 | 6 | 7 | Agora nós vamos usar o PostgreSQL rodando dentro do Docker. 8 | 9 | ![img/docker-compose.png](../img/docker-compose.png) 10 | 11 | 12 | Instale o [docker](https://docs.docker.com/get-docker/) e o [docker-compose](https://docs.docker.com/compose/install/) na sua máquina. 13 | 14 | ``` 15 | docker --version 16 | docker-compose --version 17 | ``` 18 | 19 | Vamos usar o [Portainer](https://www.portainer.io/) para monitorar nossos containers. 20 | 21 | ``` 22 | # Portainer 23 | docker run -d \ 24 | --name myportainer \ 25 | -p 9000:9000 \ 26 | --restart always \ 27 | -v /var/run/docker.sock:/var/run/docker.sock \ 28 | -v /opt/portainer:/data \ 29 | portainer/portainer 30 | ``` 31 | 32 | ![img/portainer.png](../img/portainer.png) 33 | 34 | ### Escrevendo o `docker-compose.yml` 35 | 36 | ```yml 37 | version: "3.8" 38 | 39 | services: 40 | database: 41 | container_name: db 42 | image: postgres:13.4-alpine 43 | restart: always 44 | user: postgres # importante definir o usuário 45 | volumes: 46 | - pgdata:/var/lib/postgresql/data 47 | environment: 48 | - LC_ALL=C.UTF-8 49 | - POSTGRES_PASSWORD=postgres # senha padrão 50 | - POSTGRES_USER=postgres # usuário padrão 51 | - POSTGRES_DB=db # necessário porque foi configurado assim no settings 52 | ports: 53 | - 5433:5432 # repare na porta externa 5433 54 | networks: 55 | - postgres 56 | 57 | pgadmin: 58 | container_name: pgadmin 59 | image: dpage/pgadmin4 60 | restart: unless-stopped 61 | volumes: 62 | - pgadmin:/var/lib/pgadmin 63 | environment: 64 | PGADMIN_DEFAULT_EMAIL: admin@admin.com 65 | PGADMIN_DEFAULT_PASSWORD: admin 66 | PGADMIN_CONFIG_SERVER_MODE: 'False' 67 | ports: 68 | - 5050:80 69 | networks: 70 | - postgres 71 | 72 | volumes: 73 | pgdata: # mesmo nome do volume externo definido na linha 10 74 | pgadmin: 75 | 76 | networks: 77 | postgres: 78 | ``` 79 | 80 | ### Editando `settings.py` 81 | 82 | ```python 83 | from decouple import Csv, config 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.postgresql', 88 | 'NAME': config('POSTGRES_DB', 'db'), # postgres 89 | 'USER': config('POSTGRES_USER', 'postgres'), 90 | 'PASSWORD': config('POSTGRES_PASSWORD', 'postgres'), 91 | # 'db' caso exista um serviço com esse nome. 92 | 'HOST': config('DB_HOST', '127.0.0.1'), 93 | 'PORT': '5433', 94 | } 95 | } 96 | ``` 97 | 98 | ### Rodando os containers 99 | 100 | ``` 101 | docker-compose up -d 102 | ``` 103 | 104 | ### Corrigindo um **erro** de instalação 105 | 106 | ``` 107 | django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 module: No module named 'psycopg2' 108 | 109 | pip install psycopg2-binary 110 | pip freeze | grep psycopg2-binary >> requirements.txt 111 | ``` 112 | 113 | ### Rodando as migrações 114 | 115 | ``` 116 | python manage.py migrate 117 | ``` 118 | 119 | ### Criando um super usuário 120 | 121 | ``` 122 | python manage.py createsuperuser --username="admin" --email="" 123 | python manage.py createsuperuser --username="regis" --email="regis@email.com" 124 | ``` 125 | 126 | ### Entrando no container do banco pra conferir os dados 127 | 128 | ``` 129 | docker container exec -it db psql 130 | # ou 131 | docker container exec -it db psql -h localhost -U postgres db 132 | ``` 133 | 134 | ``` 135 | \c db 136 | \dt 137 | 138 | SELECT username, email FROM auth_user; 139 | 140 | # CREATE DATABASE db; 141 | # CREATE DATABASE db OWNER postgres; 142 | ``` 143 | 144 | ### Conferindo os logs 145 | 146 | ``` 147 | docker container logs -f db 148 | ``` 149 | 150 | Você também pode ver tudo pelo Portainer. 151 | 152 | 153 | ### Rodando o Django localmente 154 | 155 | ``` 156 | python manage.py runserver 157 | ``` 158 | 159 | ## pgAdmin 160 | 161 | Entre no pgAdmin. 162 | 163 | ![img/db01.png](../img/db01.png) 164 | 165 | ![img/db02.png](../img/db02.png) 166 | 167 | ![img/pgadmin.png](../img/pgadmin.png) 168 | 169 | -------------------------------------------------------------------------------- /passo-a-passo/09_drf_entendendo_autenticacao.md: -------------------------------------------------------------------------------- 1 | # Django Experience #09 - DRF: Entendendo Autenticação 2 | 3 | Doc: https://www.django-rest-framework.org/api-guide/authentication/ 4 | 5 | ## BasicAuthentication 6 | 7 | É quando você faz a autenticação informando usuário e senha no cabeçalho da requisição via POST. Por exemplo, via Postman, usando `Basic Auth`. 8 | 9 | Recomendável usar somente em testes locais. 10 | 11 | 12 | ## SessionAuthentication 13 | 14 | É quando você faz o login pela própria interface do Django. Na tela de login do Admin, por exemplo. 15 | 16 | O que nós chamamos de sessão é a própria autenticação default do Django. 17 | 18 | 19 | ### Configurando o login via Django (Sessão) 20 | 21 | Edite `settings.py` 22 | 23 | ```python 24 | LOGIN_URL = '/admin/login/' 25 | ``` 26 | 27 | 28 | Edite `urls.py` 29 | 30 | ```python 31 | urlpatterns = [ 32 | path('accounts/', include('django.contrib.auth.urls')), 33 | ... 34 | ] 35 | ``` 36 | 37 | https://docs.djangoproject.com/en/4.0/topics/auth/default/#module-django.contrib.auth.views 38 | 39 | 40 | 41 | ```python 42 | # settings.py 43 | REST_FRAMEWORK = { 44 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 45 | 'rest_framework.authentication.BasicAuthentication', 46 | 'rest_framework.authentication.SessionAuthentication', 47 | ], 48 | } 49 | ``` 50 | 51 | Considere a app `movie`. 52 | 53 | ```python 54 | # movie/api/viewsets.py 55 | from rest_framework.authentication import ( 56 | BasicAuthentication, 57 | SessionAuthentication 58 | ) 59 | from rest_framework.permissions import IsAuthenticated 60 | from rest_framework.views import APIView 61 | 62 | 63 | class MovieExampleView(APIView): 64 | 65 | def get(self, request, format=None): 66 | content = { 67 | 'user': str(request.user), # `django.contrib.auth.User` instance. 68 | 'auth': str(request.auth), # None 69 | } 70 | return Response(content) 71 | ``` 72 | 73 | ```python 74 | # movie/urls.py 75 | from backend.movie.api.viewsets import MovieExampleView 76 | 77 | urlpatterns = [ 78 | path('api/v1/', include(router.urls)), 79 | path('api/v1/movie-examples/', MovieExampleView.as_view()), 80 | ] 81 | ``` 82 | 83 | Abra o Postman e faça uma requisição em `http://localhost:8000/api/v1/movie-examples/` 84 | 85 | ```python 86 | # movie/api/viewsets.py 87 | class CategoryViewSet(viewsets.ModelViewSet): 88 | queryset = Category.objects.all() 89 | serializer_class = CategorySerializer 90 | authentication_classes = (SessionAuthentication, BasicAuthentication) 91 | permission_classes = (IsAuthenticated,) 92 | 93 | ``` 94 | 95 | Abra o Postman e faça uma requisição em `http://localhost:8000/api/v1/categories/` 96 | 97 | 98 | ## TokenAuthentication 99 | 100 | É quando você informa um token de autorização para cada usuário logado. 101 | 102 | 103 | ```python 104 | # settings.py 105 | INSTALLED_APPS = [ 106 | ... 107 | 'rest_framework.authtoken', # <-- rode python manage.py migrate 108 | ... 109 | ] 110 | 111 | REST_FRAMEWORK = { 112 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 113 | ... 114 | 'rest_framework.authentication.TokenAuthentication', 115 | ] 116 | } 117 | ``` 118 | 119 | Rode `python manage.py migrate` 120 | 121 | ```python 122 | # movie/api/viewsets.py 123 | from rest_framework.authentication import TokenAuthentication 124 | 125 | class CategoryViewSet(viewsets.ModelViewSet): 126 | queryset = Category.objects.all() 127 | serializer_class = CategorySerializer 128 | authentication_classes = (TokenAuthentication,) 129 | permission_classes = (IsAuthenticated,) 130 | ``` 131 | 132 | 133 | ### Criando token 134 | 135 | ```python 136 | from rest_framework.authtoken.models import Token 137 | 138 | user = User.objects.get(username='admin') 139 | token = Token.objects.create(user=user) 140 | print(token.key) 141 | # 74238954b49eb221559131d677a1ea84b76c735a # (este é o meu exemplo) 142 | ``` 143 | 144 | ### Autenticando via Token 145 | 146 | #### Postman 147 | 148 | Abra o Postman, clique em **Authorization** e escolha **No Auth**. 149 | 150 | Depois clique em **Headers** e em **KEY** digite `Authorization` e em **VALUE** digite o seu `Token 74238954b49eb221559131d677a1ea84b76c735a` (este é o meu exemplo). 151 | 152 | #### curl 153 | 154 | ``` 155 | curl -X GET http://localhost:8000/api/v1/examples/ -H 'Authorization: Token 74238954b49eb221559131d677a1ea84b76c735a' 156 | curl -X GET http://localhost:8000/api/v1/categories/ -H 'Authorization: Token 74238954b49eb221559131d677a1ea84b76c735a' 157 | ``` 158 | 159 | ## Djoser e JWT Authentication 160 | 161 | Leia [Simple JWT](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/index.html) 162 | 163 | Veja os videos 164 | 165 | [Dica 47 - DRF: djoser](https://youtu.be/HUtG2Eg47Gw) 166 | 167 | [Dica 48 - DRF: Reset de Senha com djoser](https://youtu.be/BilRdaQXX8U) 168 | 169 | [Dica 49 - DRF: Autenticação via JWT com djoser](https://youtu.be/dOomllYxj9E) 170 | 171 | -------------------------------------------------------------------------------- /backend/movie/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status, viewsets 2 | from rest_framework.authentication import ( 3 | BasicAuthentication, 4 | SessionAuthentication, 5 | TokenAuthentication 6 | ) 7 | from rest_framework.decorators import action 8 | from rest_framework.exceptions import ValidationError as DRFValidationError 9 | from rest_framework.permissions import ( 10 | BasePermission, 11 | DjangoModelPermissions, 12 | IsAuthenticated, 13 | IsAuthenticatedOrReadOnly 14 | ) 15 | from rest_framework.response import Response 16 | from rest_framework.views import APIView 17 | 18 | from backend.movie.api.serializers import ( 19 | CategorySerializer, 20 | MovieReadOnlySerializer, 21 | MovieSerializer 22 | ) 23 | from backend.movie.models import Category, Movie 24 | 25 | 26 | class CategoryViewSet(viewsets.ModelViewSet): 27 | queryset = Category.objects.all() 28 | serializer_class = CategorySerializer 29 | # authentication_classes = ( 30 | # BasicAuthentication, 31 | # SessionAuthentication, 32 | # TokenAuthentication 33 | # ) 34 | # permission_classes = (IsAuthenticated,) 35 | 36 | 37 | class CensurePermission(BasePermission): 38 | age_user = 14 39 | group_name = 'Infantil' 40 | message = 'Este filme não é permitido para este perfil.' 41 | 42 | # def has_permission(self, request, view): 43 | # # Retorna uma lista de todos os grupos do usuário logado. 44 | # groups = request.user.groups.values_list('name', flat=True) 45 | 46 | # try: 47 | # # Pega a instância do objeto. 48 | # obj = view.get_object() 49 | # except AssertionError: 50 | # return True 51 | 52 | # censure = obj.censure 53 | 54 | # if self.group_name in groups and censure >= self.age_user: 55 | # response = { 56 | # 'message': self.message, 57 | # 'status_code': status.HTTP_403_FORBIDDEN 58 | # } 59 | # raise DRFValidationError(response) 60 | # else: 61 | # return True 62 | 63 | def has_object_permission(self, request, view, obj): 64 | # Retorna uma lista de todos os grupos do usuário logado. 65 | groups = request.user.groups.values_list('name', flat=True) 66 | 67 | censure = obj.censure 68 | 69 | if self.group_name in groups and censure >= self.age_user: 70 | response = { 71 | 'message': self.message, 72 | 'status_code': status.HTTP_403_FORBIDDEN 73 | } 74 | raise DRFValidationError(response) 75 | else: 76 | return True 77 | 78 | 79 | class NotDeletePermission(BasePermission): 80 | message = 'Nenhum registro pode ser deletado.' 81 | 82 | def has_permission(self, request, view): 83 | if request.method == 'DELETE': 84 | response = { 85 | 'message': self.message, 86 | 'status_code': status.HTTP_403_FORBIDDEN 87 | } 88 | raise DRFValidationError(response) 89 | else: 90 | return True 91 | 92 | 93 | class MovieViewSet(viewsets.ModelViewSet): 94 | # queryset = Movie.objects.all() 95 | serializer_class = MovieSerializer 96 | # permission_classes = (IsAuthenticatedOrReadOnly,) 97 | permission_classes = (DjangoModelPermissions, CensurePermission, NotDeletePermission) 98 | 99 | def get_queryset(self): 100 | return Movie.objects.all() 101 | 102 | @action(detail=False, methods=['get']) 103 | def get_good_movies(self, request, pk=None): 104 | ''' 105 | Retorna somente filmes bons, com rating maior ou igual a 4. 106 | ''' 107 | movies = Movie.objects.filter(rating__gte=4) 108 | 109 | page = self.paginate_queryset(movies) 110 | if page is not None: 111 | serializer = self.get_serializer(page, many=True) 112 | return self.get_paginated_response(serializer.data) 113 | 114 | serializer = self.get_serializer(movies, many=True) 115 | return Response(serializer.data) 116 | 117 | @action(detail=False, methods=['get']) 118 | def movies_readonly(self, request, pk=None): 119 | movies = Movie.objects.all() 120 | 121 | page = self.paginate_queryset(movies) 122 | if page is not None: 123 | serializer = MovieReadOnlySerializer(page, many=True) 124 | return self.get_paginated_response(serializer.data) 125 | 126 | serializer = MovieReadOnlySerializer(movies, many=True) 127 | return Response(serializer.data) 128 | 129 | @action(detail=False, methods=['get']) 130 | def movies_regular_readonly(self, request, pk=None): 131 | movies = Movie.objects.all() 132 | 133 | page = self.paginate_queryset(movies) 134 | if page is not None: 135 | serializer = [movie.to_dict() for movie in page] 136 | return self.get_paginated_response(serializer) 137 | 138 | serializer = [movie.to_dict() for movie in movies] 139 | return Response(serializer) 140 | 141 | 142 | class MovieExampleView(APIView): 143 | 144 | def get(self, request, format=None): 145 | content = { 146 | 'user': str(request.user), # `django.contrib.auth.User` instance. 147 | 'auth': str(request.auth), # None 148 | } 149 | return Response(content) 150 | -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | from decouple import config 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = config('SECRET_KEY') 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | # 3rd apps 43 | 'drf_yasg', 44 | 'rest_framework', 45 | 'rest_framework.authtoken', # <-- rode python manage.py migrate 46 | 'djoser', 47 | 'dr_scaffold', 48 | 'django_extensions', 49 | 'django_seed', 50 | # my apps 51 | 'backend.core', 52 | 'backend.crm', 53 | 'backend.example', 54 | 'backend.hotel', 55 | 'backend.movie', 56 | 'backend.order', 57 | 'backend.school', 58 | 'backend.todo', 59 | 'backend.video', 60 | ] 61 | 62 | REST_FRAMEWORK = { 63 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 64 | 'rest_framework.authentication.BasicAuthentication', 65 | 'rest_framework.authentication.SessionAuthentication', 66 | 'rest_framework.authentication.TokenAuthentication', 67 | ], 68 | 'DEFAULT_PERMISSION_CLASSES': [ 69 | 'rest_framework.permissions.IsAuthenticated', 70 | ], 71 | 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 72 | 'EXCEPTION_HANDLER': 'backend.core.handler.custom_exception_handler', 73 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 74 | 'PAGE_SIZE': 10, 75 | } 76 | 77 | MIDDLEWARE = [ 78 | 'django.middleware.security.SecurityMiddleware', 79 | 'django.contrib.sessions.middleware.SessionMiddleware', 80 | 'django.middleware.common.CommonMiddleware', 81 | 'django.middleware.csrf.CsrfViewMiddleware', 82 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 83 | 'django.contrib.messages.middleware.MessageMiddleware', 84 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 85 | ] 86 | 87 | ROOT_URLCONF = 'backend.urls' 88 | 89 | TEMPLATES = [ 90 | { 91 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 92 | 'DIRS': [], 93 | 'APP_DIRS': True, 94 | 'OPTIONS': { 95 | 'context_processors': [ 96 | 'django.template.context_processors.debug', 97 | 'django.template.context_processors.request', 98 | 'django.contrib.auth.context_processors.auth', 99 | 'django.contrib.messages.context_processors.messages', 100 | ], 101 | }, 102 | }, 103 | ] 104 | 105 | WSGI_APPLICATION = 'backend.wsgi.application' 106 | 107 | 108 | # Database 109 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 110 | 111 | # DATABASES = { 112 | # 'default': { 113 | # 'ENGINE': 'django.db.backends.sqlite3', 114 | # 'NAME': BASE_DIR / 'db.sqlite3', 115 | # } 116 | # } 117 | 118 | DATABASES = { 119 | 'default': { 120 | 'ENGINE': 'django.db.backends.postgresql', 121 | 'NAME': config('POSTGRES_DB', 'db'), # postgres 122 | 'USER': config('POSTGRES_USER', 'postgres'), 123 | 'PASSWORD': config('POSTGRES_PASSWORD', 'postgres'), 124 | # 'db' caso exista um serviço com esse nome. 125 | 'HOST': config('DB_HOST', '127.0.0.1'), 126 | 'PORT': 5433, 127 | } 128 | } 129 | 130 | # Password validation 131 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 132 | 133 | AUTH_PASSWORD_VALIDATORS = [ 134 | { 135 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 136 | }, 137 | { 138 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 139 | }, 140 | { 141 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 142 | }, 143 | { 144 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 145 | }, 146 | ] 147 | 148 | 149 | # Internationalization 150 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 151 | 152 | LANGUAGE_CODE = 'pt-br' 153 | 154 | TIME_ZONE = 'America/Sao_Paulo' 155 | 156 | USE_I18N = True 157 | 158 | USE_TZ = True 159 | 160 | 161 | # Static files (CSS, JavaScript, Images) 162 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 163 | 164 | STATIC_URL = 'static/' 165 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 166 | 167 | # Default primary key field type 168 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 169 | 170 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 171 | 172 | LOGIN_URL = '/admin/login/' 173 | -------------------------------------------------------------------------------- /passo-a-passo/11_drf_entendendo_permissoes.md: -------------------------------------------------------------------------------- 1 | # Django Experience #11 - DRF: Entendendo Permissões 2 | 3 | 4 | Doc: https://www.django-rest-framework.org/api-guide/permissions/ 5 | 6 | 7 | ## IsAuthenticated 8 | 9 | Se você define em `settings.py` 10 | 11 | ```python 12 | REST_FRAMEWORK = { 13 | 'DEFAULT_PERMISSION_CLASSES': [ 14 | 'rest_framework.permissions.IsAuthenticated', 15 | ], 16 | ``` 17 | 18 | você não precisa declarar em todas as Viewsets, por isso fica global. 19 | 20 | Se você não definir o padrão é: 21 | 22 | ```python 23 | REST_FRAMEWORK = { 24 | 'DEFAULT_PERMISSION_CLASSES': [ 25 | 'rest_framework.permissions.AllowAny', 26 | ], 27 | ``` 28 | 29 | Onde o acesso é liberado para tudo, mas você não precisa definir isso, pois é o default do Django. 30 | 31 | 32 | ## IsAuthenticatedOrReadOnly 33 | 34 | Se você não estiver autenticado, você só poderá **ler**, acessar o `GET`. Caso esteja autenticado, então os outros métodos serão liberados. Exemplo, você pode **ver** os `movies`, mas não pode editar. 35 | 36 | **Exemplo** 37 | 38 | 39 | ```python 40 | # movie/api/viewsets.py 41 | from rest_framework.permissions import IsAuthenticatedOrReadOnly 42 | 43 | class MovieViewSet(viewsets.ModelViewSet): 44 | ... 45 | permission_classes = (IsAuthenticatedOrReadOnly,) 46 | ``` 47 | 48 | Entre no Postman, e faça um **GET** sem autenticação em http://localhost:8000/api/v1/movies/ 49 | 50 | Depois faça um **POST** sem autenticação em http://localhost:8000/api/v1/movies/ 51 | 52 | 53 | ``` 54 | { 55 | "title": "Inception", 56 | "rating": 5, 57 | "like": true 58 | } 59 | ``` 60 | 61 | Depois tente com autenticação. 62 | 63 | 64 | ## Permissões a nível de objeto (Object level permissions) 65 | 66 | É chamado, por exemplo, quando é executada uma instância do model. 67 | 68 | As permissões rodam quando `.get_object()` é chamado. 69 | 70 | 71 | 72 | 73 | ## Referência da API (API Reference) 74 | 75 | ### AllowAny 76 | 77 | Acesso liberado para todos. 78 | 79 | 80 | ### IsAuthenticated 81 | 82 | Exige que o usuário esteja autenticado. 83 | 84 | 85 | ### IsAdminUser 86 | 87 | Somente usuários do tipo `user.is_staff` igual a `True` podem acessar. 88 | 89 | 90 | ### IsAuthenticatedOrReadOnly 91 | 92 | Qualquer um pode acessar o método `GET`, os demais métodos exige autenticação. 93 | 94 | 95 | ### DjangoModelPermissions 96 | 97 | São as permissões em nível de model, definidas pelo Django Admin. 98 | 99 | | Método | Permissão | 100 | |--------|-------------| 101 | | GET | view | 102 | | POST | add | 103 | | PUT | change | 104 | | DELETE | delete | 105 | 106 | **Exemplo** 107 | 108 | Vamos criar um **grupo** chamado *Editor*. Significa que o usuário que for *Editor* poderá **editar** o filme, caso contrário só podem **ler**. 109 | 110 | * **Importante:** O usuário **não** pode ser *super usuário*, então deve ser `user.is_superuser = False`. 111 | * Crie o grupo *Editor*. 112 | * Adicione a permissão `movie | Can change movie` a este grupo. 113 | * Associe este grupo ao seu usuário. 114 | 115 | ```python 116 | # movie/api/viewsets.py 117 | class MovieViewSet(viewsets.ModelViewSet): 118 | ... 119 | permission_classes = (DjangoModelPermissions,) 120 | ``` 121 | 122 | Tente fazer o `PUT` com um usuário fora do grupo. 123 | 124 | Depois tente com um usuário do grupo. 125 | 126 | 127 | **Exemplo** 128 | 129 | Vamos criar um **grupo** chamado *Criador*. Ele poderá **criar** ou **editar**. 130 | 131 | * Crie o grupo *Criador*. 132 | * Adicione a permissão `movie | Can add movie` a este grupo. 133 | * Adicione a permissão `movie | Can change movie` a este grupo. 134 | * Associe este grupo ao seu usuário. 135 | 136 | Tente fazer o `POST` com um usuário fora do grupo. 137 | 138 | Depois tente com um usuário do grupo. 139 | 140 | Depois faça o mesmo com `PUT`. 141 | 142 | 143 | ### Custom permissions 144 | 145 | Vamos considerar que se um usuário for do perfil *Infantil* e a censura do filme for 14 anos, então ele não poderá ver os detalhes do filme. 146 | 147 | ```python 148 | #movie/viewsets.py 149 | class CensurePermission(BasePermission): 150 | age_user = 14 151 | group_name = 'Infantil' 152 | message = 'Este filme não é permitido para este perfil.' 153 | 154 | def has_object_permission(self, request, view, obj): 155 | # Retorna uma lista de todos os grupos do usuário logado. 156 | groups = request.user.groups.values_list('name', flat=True) 157 | 158 | censure = obj.censure 159 | 160 | if self.group_name in groups and censure >= self.age_user: 161 | response = { 162 | 'message': self.message, 163 | 'status_code': status.HTTP_403_FORBIDDEN 164 | } 165 | raise DRFValidationError(response) 166 | else: 167 | return True 168 | 169 | 170 | class MovieViewSet(viewsets.ModelViewSet): 171 | ... 172 | permission_classes = (DjangoModelPermissions, CensurePermission) 173 | ``` 174 | 175 | Tente acessar um filme com censura de 14 anos e um de 13, no perfil "Infantil". 176 | 177 | #### Proibido deletar 178 | 179 | ```python 180 | #movie/viewsets.py 181 | class NotDeletePermission(BasePermission): 182 | message = 'Nenhum registro pode ser deletado.' 183 | 184 | def has_permission(self, request, view): 185 | if request.method == 'DELETE': 186 | response = { 187 | 'message': self.message, 188 | 'status_code': status.HTTP_403_FORBIDDEN 189 | } 190 | raise DRFValidationError(response) 191 | else: 192 | return True 193 | 194 | 195 | class MovieViewSet(viewsets.ModelViewSet): 196 | ... 197 | permission_classes = (DjangoModelPermissions, CensurePermission, NotDeletePermission) 198 | ``` 199 | 200 | -------------------------------------------------------------------------------- /backend/movie/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from backend.movie.models import Category, Movie 4 | 5 | # class CategorySerializer(serializers.Serializer): 6 | # id = serializers.IntegerField(read_only=True) 7 | # title = serializers.CharField(max_length=30) 8 | 9 | # def create(self, validated_data): 10 | # """ 11 | # Create and return a new `Category` instance, given the validated data. 12 | # Cria e retorna uma nova instância `Category`, de acordo com os dados validados. 13 | # :param validated_data: 14 | # """ 15 | # return Category.objects.create(**validated_data) 16 | 17 | # def update(self, instance, validated_data): 18 | # """ 19 | # Update and return an existing `Category` instance, given the validated data. 20 | # Atualiza e retorna uma instância `Category` existente, de acordo com os dados validados. 21 | # """ 22 | # instance.title = validated_data.get('title', instance.title) 23 | # instance.save() 24 | # return instance 25 | 26 | 27 | class CategorySerializer(serializers.ModelSerializer): 28 | 29 | class Meta: 30 | model = Category 31 | fields = ('id', 'title',) 32 | 33 | 34 | # class MovieSerializer(serializers.Serializer): 35 | # id = serializers.IntegerField(read_only=True) 36 | # title = serializers.CharField(max_length=30) 37 | # sinopse = serializers.CharField(max_length=255) 38 | # rating = serializers.IntegerField() 39 | # like = serializers.BooleanField(default=False) 40 | # category = CategorySerializer() 41 | 42 | # def create(self, validated_data): 43 | # """ 44 | # Create and return a new `Movie` instance, given the validated data. 45 | # Cria e retorna uma nova instância `Movie`, de acordo com os dados validados. 46 | # :param validated_data: 47 | # """ 48 | # category_data = {} 49 | # if 'category' in validated_data: 50 | # category_data = validated_data.pop('category') 51 | 52 | # if category_data: 53 | # category = Category.objects.create(**category_data) 54 | # movie = Movie.objects.create(category=category, **validated_data) 55 | # else: 56 | # movie = Movie.objects.create(**validated_data) 57 | 58 | # return movie 59 | 60 | # def update(self, instance, validated_data): 61 | # """ 62 | # Update and return an existing `Movie` instance, given the validated data. 63 | # Atualiza e retorna uma instância `Movie` existente, de acordo com os dados validados. 64 | # """ 65 | # if 'category' in validated_data: 66 | # category_data = validated_data.pop('category') 67 | # title = category_data.get('title') 68 | # category, _ = Category.objects.get_or_create(title=title) 69 | # # Atualiza a categoria 70 | # instance.category = category 71 | 72 | # # Atualiza a instância 73 | # instance.title = validated_data.get('title', instance.title) 74 | # instance.sinopse = validated_data.get('sinopse', instance.sinopse) 75 | # instance.rating = validated_data.get('rating', instance.rating) 76 | # instance.like = validated_data.get('like', instance.like) 77 | # instance.save() 78 | 79 | # return instance 80 | 81 | 82 | def positive_only_validator(value): 83 | if value == 0: 84 | raise serializers.ValidationError('Zero não é um valor permitido.') 85 | 86 | 87 | class MovieSerializer(serializers.ModelSerializer): 88 | category = CategorySerializer(required=False) 89 | censure = serializers.IntegerField(min_value=0, validators=[positive_only_validator]) 90 | 91 | class Meta: 92 | model = Movie 93 | fields = ( 94 | 'id', 95 | 'title', 96 | 'sinopse', 97 | 'rating', 98 | 'censure', 99 | 'like', 100 | 'created', 101 | 'category' 102 | ) 103 | # depth = 1 104 | 105 | # def validate_title(self, value): 106 | # if 'lorem' in value.lower(): 107 | # raise serializers.ValidationError('Lorem não pode.') 108 | # return value 109 | 110 | def validate(self, data): 111 | if 'lorem' in data['title'].lower(): 112 | raise serializers.ValidationError('Lorem não pode.') 113 | return data 114 | 115 | def create(self, validated_data): 116 | """ 117 | Create and return a new `Movie` instance, given the validated data. 118 | Cria e retorna uma nova instância `Movie`, de acordo com os dados validados. 119 | :param validated_data: 120 | """ 121 | category_data = {} 122 | if 'category' in validated_data: 123 | category_data = validated_data.pop('category') 124 | 125 | if category_data: 126 | category, _ = Category.objects.get_or_create(**category_data) 127 | movie = Movie.objects.create(category=category, **validated_data) 128 | else: 129 | movie = Movie.objects.create(**validated_data) 130 | 131 | return movie 132 | 133 | def update(self, instance, validated_data): 134 | """ 135 | Update and return an existing `Movie` instance, given the validated data. 136 | Atualiza e retorna uma instância `Movie` existente, de acordo com os dados validados. 137 | """ 138 | if 'category' in validated_data: 139 | category_data = validated_data.pop('category') 140 | title = category_data.get('title') 141 | category, _ = Category.objects.get_or_create(title=title) 142 | # Atualiza a categoria 143 | instance.category = category 144 | 145 | # Atualiza a instância 146 | instance.title = validated_data.get('title', instance.title) 147 | instance.sinopse = validated_data.get('sinopse', instance.sinopse) 148 | instance.rating = validated_data.get('rating', instance.rating) 149 | instance.like = validated_data.get('like', instance.like) 150 | instance.save() 151 | 152 | return instance 153 | 154 | 155 | class MovieReadOnlySerializer(serializers.ModelSerializer): 156 | # category = CategorySerializer(required=False) 157 | 158 | class Meta: 159 | model = Movie 160 | fields = ('id', 'title', 'sinopse', 'rating', 'like', 'created') 161 | read_only_fields = fields 162 | -------------------------------------------------------------------------------- /backend/school/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.shortcuts import get_object_or_404 3 | from rest_framework import status, viewsets 4 | from rest_framework.decorators import action 5 | from rest_framework.exceptions import ValidationError as DRFValidationError 6 | from rest_framework.permissions import AllowAny 7 | from rest_framework.response import Response 8 | 9 | from backend.school.api.serializers import ( 10 | ClassAddSerializer, 11 | ClassroomSerializer, 12 | ClassSerializer, 13 | GradeSerializer, 14 | StudentRegistrationSerializer, 15 | StudentSerializer, 16 | StudentUpdateSerializer 17 | ) 18 | from backend.school.models import Class, Classroom, Grade, Student 19 | 20 | # class StudentViewSet(viewsets.ModelViewSet): 21 | # queryset = Student.objects.all() 22 | # serializer_class = StudentSerializer 23 | 24 | 25 | class StudentViewSet(viewsets.ViewSet): 26 | """ 27 | A simple ViewSet for listing or retrieving students. 28 | Uma ViewSet simples para listar ou recuperar alunos. 29 | """ 30 | 31 | def get_serializer_class(self): 32 | # Muda o serializer dependendo da ação. 33 | if self.action == 'create': 34 | return StudentSerializer 35 | 36 | if self.action == 'update': 37 | return StudentUpdateSerializer 38 | 39 | return StudentSerializer 40 | 41 | def get_serializer(self, *args, **kwargs): 42 | serializer_class = self.get_serializer_class() 43 | return serializer_class(*args, **kwargs) 44 | 45 | def get_queryset(self): 46 | queryset = Student.objects.all() 47 | return queryset 48 | 49 | def get_object(self): 50 | queryset = self.get_queryset() 51 | pk = self.kwargs.get('pk') 52 | obj = get_object_or_404(queryset, pk=pk) 53 | return obj 54 | 55 | def list(self, request): 56 | # queryset = Student.objects.all() 57 | # serializer = StudentSerializer(queryset, many=True) 58 | # return Response(serializer.data) 59 | serializer = self.get_serializer(self.get_queryset(), many=True) 60 | # Sem paginação 61 | return Response(serializer.data) 62 | 63 | def create(self, request, *args, **kwargs): 64 | serializer = self.get_serializer(data=request.data) 65 | serializer.is_valid(raise_exception=True) 66 | serializer.save() 67 | return Response(serializer.data, status=status.HTTP_201_CREATED) 68 | 69 | def retrieve(self, request, pk=None): 70 | queryset = Student.objects.all() 71 | student = get_object_or_404(queryset, pk=pk) 72 | serializer = StudentSerializer(student) 73 | return Response(serializer.data) 74 | 75 | def update(self, request, *args, **kwargs): 76 | partial = kwargs.pop('partial', False) 77 | instance = self.get_object() 78 | serializer = self.get_serializer(instance, data=request.data, partial=partial) 79 | serializer.is_valid(raise_exception=True) 80 | serializer.save() 81 | return Response(serializer.data) 82 | 83 | def partial_update(self, request, *args, **kwargs): 84 | kwargs['partial'] = True 85 | return self.update(request, *args, **kwargs) 86 | 87 | def destroy(self, request, pk=None): 88 | item = self.get_object() 89 | item.delete 90 | return Response(status=status.HTTP_204_NO_CONTENT) 91 | 92 | @action(detail=False, methods=['get']) 93 | def all_students(self, request, pk=None): 94 | queryset = Student.objects.all() 95 | serializer = StudentRegistrationSerializer(queryset, many=True) 96 | return Response(serializer.data) 97 | 98 | 99 | class ClassroomViewSet(viewsets.ModelViewSet): 100 | queryset = Classroom.objects.all() 101 | serializer_class = ClassroomSerializer 102 | permission_classes = (AllowAny,) 103 | 104 | 105 | class GradeViewSet(viewsets.ReadOnlyModelViewSet): 106 | queryset = Grade.objects.all() 107 | serializer_class = GradeSerializer 108 | permission_classes = (AllowAny,) 109 | 110 | 111 | class ClassViewSet(viewsets.ModelViewSet): 112 | queryset = Class.objects.all() 113 | # serializer_class = ClassSerializer 114 | 115 | # def list(self, request, *args, **kwargs): 116 | # user = self.request.user 117 | # teacher = User.objects.get(username=user) 118 | 119 | # if user is not None: 120 | # queryset = Class.objects.filter(teacher=teacher) 121 | # else: 122 | # queryset = Class.objects.none() 123 | 124 | # page = self.paginate_queryset(queryset) 125 | # if page is not None: 126 | # serializer = self.get_serializer(page, many=True) 127 | # return self.get_paginated_response(serializer.data) 128 | 129 | # serializer = self.get_serializer(queryset, many=True) 130 | # return Response(serializer.data) 131 | 132 | def get_queryset(self): 133 | ''' 134 | Retorna somente as aulas da pessoa logada no momento. 135 | ''' 136 | user = self.request.user 137 | teacher = User.objects.get(username=user) 138 | 139 | if user is not None: 140 | queryset = Class.objects.filter(teacher=teacher) 141 | else: 142 | queryset = Class.objects.none() 143 | 144 | return queryset 145 | 146 | def get_serializer_class(self): 147 | if self.action == 'create': 148 | return ClassAddSerializer 149 | 150 | if self.action == 'update': 151 | return ClassSerializer 152 | 153 | return ClassSerializer 154 | 155 | def perform_create(self, serializer): 156 | ''' 157 | Ao criar um objeto, define teacher com o usuário logado. 158 | ''' 159 | user = self.request.user 160 | teacher = User.objects.get(username=user) 161 | 162 | if user is not None: 163 | serializer.save(teacher=teacher) 164 | 165 | def create(self, request, *args, **kwargs): 166 | # https://www.cdrf.co/3.12/rest_framework.viewsets/ModelViewSet.html#create 167 | serializer = self.get_serializer(data=request.data) 168 | serializer.is_valid(raise_exception=True) 169 | self.perform_create(serializer) 170 | headers = self.get_success_headers(serializer.data) 171 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 172 | 173 | def perform_update(self, serializer): 174 | user = self.request.user 175 | data = self.request.data 176 | teacher_id = data.get('teacher') 177 | 178 | try: 179 | teacher = User.objects.get(pk=teacher_id) 180 | except User.DoesNotExist: 181 | raise DRFValidationError('Usuário não encontrado.') 182 | 183 | if user and user.is_authenticated: 184 | if user == teacher: 185 | serializer.save() 186 | else: 187 | raise DRFValidationError('Você não tem permissão para esta operação.') 188 | 189 | def retrieve(self, request, *args, **kwargs): 190 | ''' 191 | Método para ver os detalhes. 192 | ''' 193 | instance = self.get_object() 194 | teacher = instance.teacher 195 | user = self.request.user 196 | 197 | if user and user.is_authenticated: 198 | if user == teacher: 199 | serializer = self.get_serializer(instance) 200 | else: 201 | raise DRFValidationError('Você não tem acesso a esta aula.') 202 | 203 | return Response(serializer.data) 204 | 205 | def perform_destroy(self, instance): 206 | ''' 207 | Método pra deletar os dados. 208 | ''' 209 | # instance.delete() 210 | raise DRFValidationError('Nenhuma aula pode ser deletada.') 211 | -------------------------------------------------------------------------------- /passo-a-passo/01_django_full_template_como_criar_um_projeto_django_completo_api_rest_render_template.md: -------------------------------------------------------------------------------- 1 | # Django Experience #01 - Como criar um projeto Django completo + API REST + Render Template 2 | 3 | ## contrib e gitignore 4 | 5 | ``` 6 | mkdir contrib 7 | touch contrib/env_gen.py 8 | touch .gitignore 9 | ``` 10 | 11 | ## A receita do bolo 12 | 13 | ``` 14 | cat << EOF > requirements.txt 15 | Django==4.0 16 | dr-scaffold==2.1.* 17 | djangorestframework==3.12.* 18 | django-seed==0.3.* 19 | python-decouple==3.5 20 | django-extensions==3.1.* 21 | psycopg2-binary==2.9.* 22 | drf-yasg==1.20.* 23 | pytz 24 | EOF 25 | ``` 26 | 27 | ## Criando virtualenv e instalando tudo 28 | 29 | ``` 30 | python -m venv .venv 31 | source .venv/bin/activate 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | 36 | ## Settings 37 | 38 | ``` 39 | django-admin startproject backend . 40 | echo "SECRET_KEY=my-super-secret-key" > .env 41 | ``` 42 | 43 | Edite `settings.py` 44 | 45 | 46 | ```python 47 | # settings.py 48 | from decouple import config 49 | 50 | SECRET_KEY = config('SECRET_KEY') 51 | 52 | INSTALLED_APPS = [ 53 | ... 54 | # 3rd apps 55 | 'drf_yasg', 56 | 'rest_framework', 57 | 'dr_scaffold', 58 | 'django_extensions', 59 | 'django_seed', 60 | # my apps 61 | ] 62 | 63 | LANGUAGE_CODE = 'pt-br' 64 | 65 | TIME_ZONE = 'America/Sao_Paulo' 66 | 67 | STATIC_URL = 'static/' 68 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 69 | ``` 70 | 71 | ## Criando a app principal 72 | 73 | ``` 74 | cd backend 75 | python ../manage.py startapp core 76 | cd .. 77 | ``` 78 | 79 | > Adicione `backend.core` em `INSTALLED_APPS`. 80 | 81 | Edite `backend/core/apps.py`. 82 | 83 | `name = 'backend.core'` 84 | 85 | 86 | ## Criando os arquivos básicos 87 | 88 | ``` 89 | touch backend/core/urls.py 90 | 91 | mkdir -p backend/core/templates/includes 92 | mkdir -p backend/core/static/{css,img,js} 93 | 94 | touch backend/core/static/css/style.css 95 | touch backend/core/static/js/main.js 96 | touch backend/core/templates/{base,index}.html 97 | touch backend/core/templates/includes/{nav,pagination}.html 98 | 99 | python manage.py create_template_tags core -n url_replace 100 | ``` 101 | 102 | > Mostrar as pastas. 103 | 104 | 105 | ## Criando mais uma app 106 | 107 | ``` 108 | cd backend 109 | python ../manage.py startapp todo 110 | cd .. 111 | ``` 112 | 113 | > Adicione `backend.todo` em `INSTALLED_APPS`. 114 | 115 | Edite `backend/todo/apps.py`. 116 | 117 | `name = 'backend.todo'` 118 | 119 | 120 | ``` 121 | mkdir backend/todo/api 122 | mkdir -p backend/todo/templates/todo 123 | 124 | touch backend/todo/api/serializers.py 125 | touch backend/todo/api/viewsets.py 126 | 127 | touch backend/todo/{forms,views,urls}.py 128 | touch backend/todo/templates/todo/todo_{list,detail,form,confirm_delete}.html 129 | ``` 130 | 131 | Editar `backend/urls.py` 132 | 133 | ```python 134 | from django.contrib import admin 135 | from django.urls import include, path 136 | 137 | urlpatterns = [ 138 | path('', include('backend.core.urls', namespace='core')), 139 | path('', include('backend.todo.urls', namespace='todo')), 140 | path('admin/', admin.site.urls), 141 | ] 142 | 143 | ``` 144 | 145 | 146 | Editar `backend/core/urls.py` 147 | 148 | ```python 149 | from django.urls import path 150 | 151 | from .views import index 152 | 153 | app_name = 'core' 154 | 155 | urlpatterns = [ 156 | path('', index, name='index'), 157 | ] 158 | 159 | ``` 160 | 161 | Editar `backend/todo/urls.py` 162 | 163 | ```python 164 | from django.urls import include, path 165 | from rest_framework import routers 166 | 167 | from backend.todo import views as v 168 | from backend.todo.api.viewsets import TodoViewSet 169 | 170 | app_name = 'todo' 171 | 172 | router = routers.DefaultRouter() 173 | 174 | router.register(r'todos', TodoViewSet) 175 | 176 | todo_urlpatterns = [ 177 | path('', v.TodoListView.as_view(), name='todo_list'), 178 | path('/', v.TodoDetailView.as_view(), name='todo_detail'), 179 | path('create/', v.TodoCreateView.as_view(), name='todo_create'), 180 | path('/update/', v.TodoUpdateView.as_view(), name='todo_update'), 181 | path('/delete/', v.TodoDeleteView.as_view(), name='todo_delete'), 182 | ] 183 | 184 | urlpatterns = [ 185 | path('todo/', include(todo_urlpatterns)), 186 | path('api/v1/', include(router.urls)), 187 | ] 188 | 189 | ``` 190 | 191 | ## Editando todos os arquivos 192 | 193 | ``` 194 | git clone https://github.com/rg3915/django-full-template.git /tmp/django-full-template 195 | cp /tmp/django-full-template/.gitignore . 196 | cp /tmp/django-full-template/contrib/env_gen.py contrib/ 197 | 198 | cp /tmp/django-full-template/backend/urls.py backend/ 199 | cp /tmp/django-full-template/backend/core/views.py backend/core/ 200 | cp /tmp/django-full-template/backend/core/static/css/style.css backend/core/static/css/ 201 | cp -R /tmp/django-full-template/backend/core/templates/ backend/core/ 202 | cp /tmp/django-full-template/backend/core/templatetags/url_replace.py backend/core/templatetags/ 203 | 204 | cp /tmp/django-full-template/backend/todo/{admin,forms,models,views}.py backend/todo/ 205 | cp /tmp/django-full-template/backend/todo/api/{serializers,viewsets}.py backend/todo/api/ 206 | cp -R /tmp/django-full-template/backend/todo/templates/todo/ backend/todo/templates/ 207 | ``` 208 | 209 | Depois que editar tudo, faça 210 | 211 | ## Rodando as migrações 212 | 213 | ``` 214 | python manage.py makemigrations 215 | python manage.py migrate 216 | python manage.py createsuperuser --username="admin" --email="" 217 | ``` 218 | 219 | ## Gerando alguns dados randomicamente 220 | 221 | ``` 222 | pip install psycopg2-binary 223 | python manage.py seed todo --number=50 224 | ``` 225 | 226 | ``` 227 | pip freeze | grep psycopg2-binary >> requirements.txt 228 | ``` 229 | 230 | ## urls 231 | 232 | Rode o comando para ver as urls 233 | 234 | ``` 235 | python manage.py show_urls 236 | ``` 237 | 238 | ``` 239 | / 240 | /api/v1/ 241 | /api/v1/todos/ 242 | /api/v1/todos// 243 | /redoc/ 244 | /swagger/ 245 | /todo/ 246 | /todo// 247 | /todo//delete/ 248 | /todo//update/ 249 | /todo/create/ 250 | ``` 251 | 252 | ## Estrutura das pastas 253 | 254 | ``` 255 | . 256 | ├── backend 257 | │   ├── asgi.py 258 | │   ├── core 259 | │   │   ├── admin.py 260 | │   │   ├── apps.py 261 | │   │   ├── __init__.py 262 | │   │   ├── migrations 263 | │   │   │   ├── __init__.py 264 | │   │   │   └── __pycache__ 265 | │   │   ├── models.py 266 | │   │   ├── __pycache__ 267 | │   │   ├── static 268 | │   │   │   ├── css 269 | │   │   │   │   └── style.css 270 | │   │   │   ├── img 271 | │   │   │   └── js 272 | │   │   │   └── main.js 273 | │   │   ├── templates 274 | │   │   │   ├── base.html 275 | │   │   │   ├── includes 276 | │   │   │   │   ├── nav.html 277 | │   │   │   │   └── pagination.html 278 | │   │   │   └── index.html 279 | │   │   ├── templatetags 280 | │   │   │   ├── __init__.py 281 | │   │   │   ├── __pycache__ 282 | │   │   │   └── url_replace.py 283 | │   │   ├── tests.py 284 | │   │   ├── urls.py 285 | │   │   └── views.py 286 | │   ├── __init__.py 287 | │   ├── __pycache__ 288 | │   ├── settings.py 289 | │   ├── todo 290 | │   │   ├── admin.py 291 | │   │   ├── api 292 | │   │   │   ├── __pycache__ 293 | │   │   │   ├── serializers.py 294 | │   │   │   └── viewsets.py 295 | │   │   ├── apps.py 296 | │   │   ├── forms.py 297 | │   │   ├── __init__.py 298 | │   │   ├── migrations 299 | │   │   │   ├── 0001_initial.py 300 | │   │   │   ├── __init__.py 301 | │   │   │   └── __pycache__ 302 | │   │   ├── models.py 303 | │   │   ├── __pycache__ 304 | │   │   ├── templates 305 | │   │   │   └── todo 306 | │   │   │   ├── todo_confirm_delete.html 307 | │   │   │   ├── todo_detail.html 308 | │   │   │   ├── todo_form.html 309 | │   │   │   └── todo_list.html 310 | │   │   ├── tests.py 311 | │   │   ├── urls.py 312 | │   │   └── views.py 313 | │   ├── urls.py 314 | │   └── wsgi.py 315 | ├── contrib 316 | │   └── env_gen.py 317 | ├── db.sqlite3 318 | ├── manage.py 319 | ├── README.md 320 | └── requirements.txt 321 | ``` 322 | -------------------------------------------------------------------------------- /passo-a-passo/02_criando_api_com_django_sem_drf_parte2.md: -------------------------------------------------------------------------------- 1 | # Django Experience #02 - Criando API com Django SEM DRF - parte 2 2 | 3 | ``` 4 | cd backend 5 | python ../manage.py startapp video 6 | cd .. 7 | ``` 8 | 9 | 10 | Edite `settings.py` 11 | 12 | ```python 13 | INSTALLED_APPS = [ 14 | ... 15 | 'backend.video', 16 | ] 17 | ``` 18 | 19 | Edite `video/apps.py` 20 | 21 | ```python 22 | # video/apps.py 23 | name = 'backend.video' 24 | ``` 25 | 26 | Edite `video/models.py` 27 | 28 | ```python 29 | # video/models.py 30 | from django.db import models 31 | 32 | 33 | class Video(models.Model): 34 | title = models.CharField('título', max_length=50, unique=True) 35 | release_year = models.PositiveIntegerField('lançamento', null=True, blank=True) 36 | 37 | class Meta: 38 | ordering = ('id',) 39 | verbose_name = 'filme' 40 | verbose_name_plural = 'filmes' 41 | 42 | def __str__(self): 43 | return f'{self.title}' 44 | 45 | def to_dict(self): 46 | return { 47 | 'id': self.id, 48 | 'title': self.title, 49 | 'release_year': self.release_year, 50 | } 51 | 52 | ``` 53 | 54 | Edite `video/admin.py` 55 | 56 | ```python 57 | # video/admin.py 58 | from django.contrib import admin 59 | 60 | from .models import Video 61 | 62 | 63 | @admin.register(Video) 64 | class VideoAdmin(admin.ModelAdmin): 65 | list_display = ('__str__', 'release_year') 66 | search_fields = ('title',) 67 | 68 | ``` 69 | 70 | 71 | ``` 72 | python manage.py makemigrations 73 | python manage.py migrate 74 | ``` 75 | 76 | 77 | Edite `video/forms.py` 78 | 79 | ```python 80 | # video/forms.py 81 | from django import forms 82 | 83 | from .models import Video 84 | 85 | 86 | class VideoForm(forms.ModelForm): 87 | 88 | class Meta: 89 | model = Video 90 | fields = ('title', 'release_year') 91 | 92 | ``` 93 | 94 | Edite `video/views.py` 95 | 96 | ```python 97 | import json 98 | 99 | from django.http import JsonResponse 100 | from django.shortcuts import get_object_or_404 101 | from django.views.decorators.csrf import csrf_exempt 102 | 103 | from .forms import VideoForm 104 | from .models import Video 105 | 106 | 107 | @csrf_exempt 108 | def videos(request): 109 | ''' 110 | Lista ou cria videos. 111 | ''' 112 | videos = Video.objects.all() 113 | data = [video.to_dict() for video in videos] 114 | form = VideoForm(request.POST or None) 115 | 116 | if request.method == 'POST': 117 | if request.POST: 118 | # Dados obtidos pelo formulário. 119 | if form.is_valid(): 120 | video = form.save() 121 | 122 | elif request.body: 123 | # Dados obtidos via json. 124 | data = json.loads(request.body) 125 | video = Video.objects.create(**data) 126 | 127 | else: 128 | return JsonResponse({'message': 'Algo deu errado.'}) 129 | 130 | return JsonResponse({'data': video.to_dict()}) 131 | 132 | return JsonResponse({'data': data}) 133 | 134 | 135 | @csrf_exempt 136 | def video(request, pk): 137 | ''' 138 | Mostra os detalhes, edita ou deleta um video. 139 | ''' 140 | video = get_object_or_404(Video, pk=pk) 141 | form = VideoForm(request.POST or None, instance=video) 142 | 143 | if request.method == 'GET': 144 | data = video.to_dict() 145 | return JsonResponse({'data': data}) 146 | 147 | if request.method == 'POST': 148 | if request.POST: 149 | # Dados obtidos pelo formulário. 150 | if form.is_valid(): 151 | video = form.save() 152 | 153 | elif request.body: 154 | # Dados obtidos via json. 155 | data = json.loads(request.body) 156 | 157 | for attr, value in data.items(): 158 | setattr(video, attr, value) 159 | video.save() 160 | 161 | else: 162 | return JsonResponse({'message': 'Algo deu errado.'}) 163 | 164 | return JsonResponse({'data': video.to_dict()}) 165 | 166 | if request.method == 'DELETE': 167 | video.delete() 168 | return JsonResponse({'data': 'Item deletado com sucesso.'}) 169 | 170 | ``` 171 | 172 | Edite `video/urls.py` 173 | 174 | ```python 175 | # video/urls.py 176 | from django.urls import include, path 177 | 178 | from backend.video import views as v 179 | 180 | app_name = 'video' 181 | 182 | v1_urlpatterns = [ 183 | path('videos/', v.videos, name='videos'), 184 | path('videos//', v.video, name='video'), 185 | ] 186 | 187 | urlpatterns = [ 188 | path('api/v1/', include(v1_urlpatterns)), 189 | ] 190 | 191 | ``` 192 | 193 | Edite `backend/urls.py` 194 | 195 | ```python 196 | # backend/urls.py 197 | urlpatterns = [ 198 | ... 199 | path('', include('backend.video.urls', namespace='video')), 200 | ] 201 | ``` 202 | 203 | 204 | Edite `nav.html` 205 | 206 | ```html 207 | 208 | 211 | ``` 212 | 213 | **Obs:** *O correto é `href="{\%` (chave porcentagem), mas coloquei a barra invertida por causa do Jekyll.* 214 | 215 | Edite `video/tests.py` 216 | 217 | ```python 218 | # video/tests.py 219 | import json 220 | 221 | from django.test import TestCase 222 | 223 | from .models import Video 224 | 225 | 226 | class VideoTest(TestCase): 227 | 228 | def setUp(self): 229 | self.payload = { 230 | "title": "Matrix", 231 | "release_year": 1999 232 | } 233 | 234 | def test_video_create(self): 235 | response = self.client.post( 236 | '/api/v1/videos/', 237 | data=self.payload, 238 | content_type='application/json' 239 | ) 240 | resultado = json.loads(response.content) 241 | esperado = { 242 | "data": { 243 | "id": 1, 244 | **self.payload 245 | } 246 | } 247 | self.assertEqual(esperado, resultado) 248 | 249 | def test_video_list(self): 250 | Video.objects.create(**self.payload) 251 | 252 | response = self.client.get( 253 | '/api/v1/videos/', 254 | content_type='application/json' 255 | ) 256 | resultado = json.loads(response.content) 257 | esperado = { 258 | "data": [ 259 | { 260 | "id": 1, 261 | **self.payload 262 | } 263 | ] 264 | } 265 | self.assertEqual(esperado, resultado) 266 | 267 | def test_video_detail(self): 268 | Video.objects.create(**self.payload) 269 | 270 | response = self.client.get( 271 | '/api/v1/videos/1/', 272 | content_type='application/json' 273 | ) 274 | resultado = json.loads(response.content) 275 | esperado = { 276 | "data": { 277 | "id": 1, 278 | **self.payload 279 | } 280 | } 281 | self.assertEqual(esperado, resultado) 282 | 283 | def test_video_update(self): 284 | Video.objects.create(**self.payload) 285 | 286 | data = { 287 | "title": "Matrix 2" 288 | } 289 | 290 | response = self.client.post( 291 | '/api/v1/videos/1/', 292 | data=data, 293 | content_type='application/json' 294 | ) 295 | resultado = json.loads(response.content) 296 | esperado = { 297 | "data": 298 | { 299 | "id": 1, 300 | "title": "Matrix 2", 301 | "release_year": 1999 302 | } 303 | } 304 | self.assertEqual(esperado, resultado) 305 | 306 | def test_video_delete(self): 307 | Video.objects.create(**self.payload) 308 | 309 | response = self.client.delete( 310 | '/api/v1/videos/1/', 311 | content_type='application/json' 312 | ) 313 | resultado = json.loads(response.content) 314 | esperado = {"data": "Item deletado com sucesso."} 315 | 316 | self.assertEqual(esperado, resultado) 317 | 318 | ``` 319 | 320 | -------------------------------------------------------------------------------- /passo-a-passo/06_drf_entendendo_viewsets.md: -------------------------------------------------------------------------------- 1 | # Django Experience #06 - DRF: Entendendo Viewsets 2 | 3 | Doc: [Viewsets](https://www.django-rest-framework.org/api-guide/viewsets/) 4 | 5 | ## ViewSets 6 | 7 | https://www.django-rest-framework.org/api-guide/viewsets/ 8 | 9 | Vamos considerar a app `school`. 10 | 11 | Crie um arquivo `school/viewsets.py`. 12 | 13 | ```python 14 | # school/viewsets.py 15 | from django.shortcuts import get_object_or_404 16 | from rest_framework import viewsets 17 | from rest_framework.response import Response 18 | 19 | from backend.school.models import Student 20 | from backend.school.api.serializers import StudentRegistrationSerializer, StudentSerializer 21 | 22 | 23 | class StudentViewSet(viewsets.ViewSet): 24 | """ 25 | A simple ViewSet for listing or retrieving students. 26 | Uma ViewSet simples para listar ou recuperar alunos. 27 | """ 28 | 29 | def list(self, request): 30 | queryset = Student.objects.all() 31 | serializer = StudentSerializer(queryset, many=True) 32 | return Response(serializer.data) 33 | 34 | def retrieve(self, request, pk=None): 35 | queryset = Student.objects.all() 36 | student = get_object_or_404(queryset, pk=pk) 37 | serializer = StudentSerializer(student) 38 | return Response(serializer.data) 39 | 40 | ``` 41 | 42 | Edite `school/urls.py`. 43 | 44 | ```python 45 | # school/urls.py 46 | from django.urls import include, path 47 | from rest_framework import routers 48 | 49 | from school.views import ClassroomViewSet 50 | from school.viewsets import StudentViewSet as SimpleStudentViewSet 51 | 52 | router = routers.DefaultRouter() 53 | 54 | router.register(r'students', SimpleStudentViewSet) 55 | router.register(r'classrooms', ClassroomViewSet) 56 | 57 | urlpatterns = [ 58 | path("", include(router.urls)), 59 | ] 60 | ``` 61 | 62 | Erro: 63 | 64 | ```python 65 | assert queryset is not None, '`basename` argument not specified, and could ' \ 66 | AssertionError: `basename` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute. 67 | ``` 68 | 69 | Então defina o `basename`. 70 | 71 | ```python 72 | ... 73 | router.register(r'students', SimpleStudentViewSet, basename='student') 74 | ... 75 | ``` 76 | 77 | ### Nova rota com action 78 | 79 | ```python 80 | # school/viewsets.py 81 | ... 82 | from rest_framework.decorators import action 83 | from rest_framework.response import Response 84 | 85 | class StudentViewSet(viewsets.ViewSet): 86 | ... 87 | 88 | @action(detail=False, methods=['get']) 89 | def all_students(self, request, pk=None): 90 | queryset = Student.objects.all() 91 | serializer = StudentRegistrationSerializer(queryset, many=True) 92 | return Response(serializer.data) 93 | 94 | ``` 95 | 96 | ### Todas as ações do ViewSet implementadas explicitamente 97 | 98 | Primeiro momento 99 | 100 | ```python 101 | # school/viewsets.py 102 | class StudentViewSet(viewsets.ViewSet): 103 | """ 104 | A simple ViewSet for listing or retrieving students. 105 | Uma ViewSet simples para listar ou recuperar alunos. 106 | """ 107 | 108 | def get_serializer_class(self): 109 | pass 110 | 111 | def get_serializer(self, *args, **kwargs): 112 | pass 113 | 114 | def get_queryset(self): 115 | pass 116 | 117 | def get_object(self): 118 | pass 119 | 120 | def list(self, request): 121 | queryset = Student.objects.all() 122 | serializer = StudentSerializer(queryset, many=True) 123 | return Response(serializer.data) 124 | 125 | def create(self, request): 126 | pass 127 | 128 | def retrieve(self, request, pk=None): 129 | queryset = Student.objects.all() 130 | student = get_object_or_404(queryset, pk=pk) 131 | serializer = StudentSerializer(student) 132 | return Response(serializer.data) 133 | 134 | def update(self, request, pk=None): 135 | pass 136 | 137 | def partial_update(self, request, pk=None): 138 | pass 139 | 140 | def destroy(self, request, pk=None): 141 | pass 142 | ``` 143 | 144 | Se você definir `get_serializer()` diretamente... 145 | 146 | ```python 147 | def get_serializer(self): 148 | return StudentSerializer 149 | ``` 150 | 151 | ... vai dar o seguinte erro: 152 | 153 | `AttributeError: 'property' object has no attribute 'copy'` 154 | 155 | Então defina 156 | 157 | ```python 158 | def get_serializer_class(self): 159 | return StudentSerializer 160 | 161 | def get_serializer(self, *args, **kwargs): 162 | serializer_class = self.get_serializer_class() 163 | return serializer_class(*args, **kwargs) 164 | ``` 165 | 166 | Então podemos reescrever o método `list()` 167 | 168 | ```python 169 | def list(self, request): 170 | # queryset = Student.objects.all() 171 | # serializer = StudentSerializer(queryset, many=True) 172 | # return Response(serializer.data) 173 | serializer = self.get_serializer(self.get_queryset(), many=True) 174 | # Sem paginação 175 | return Response(serializer.data) 176 | 177 | ``` 178 | 179 | Completo 180 | 181 | ```python 182 | # school/viewsets.py 183 | from django.shortcuts import get_object_or_404 184 | from rest_framework import status, viewsets 185 | from rest_framework.decorators import action 186 | from rest_framework.response import Response 187 | 188 | from backend.school.models import Student 189 | from backend.school.api.serializers import StudentRegistrationSerializer, StudentSerializer 190 | 191 | 192 | class StudentViewSet(viewsets.ViewSet): 193 | """ 194 | A simple ViewSet for listing or retrieving students. 195 | Uma ViewSet simples para listar ou recuperar alunos. 196 | """ 197 | 198 | def get_serializer_class(self): 199 | return StudentSerializer 200 | 201 | def get_serializer(self, *args, **kwargs): 202 | serializer_class = self.get_serializer_class() 203 | return serializer_class(*args, **kwargs) 204 | 205 | def get_queryset(self): 206 | queryset = Student.objects.all() 207 | return queryset 208 | 209 | def get_object(self): 210 | queryset = self.get_queryset() 211 | pk = self.kwargs.get('pk') 212 | obj = get_object_or_404(queryset, pk=pk) 213 | return obj 214 | 215 | def list(self, request): 216 | # queryset = Student.objects.all() 217 | # serializer = StudentSerializer(queryset, many=True) 218 | # return Response(serializer.data) 219 | serializer = self.get_serializer(self.get_queryset(), many=True) 220 | # Sem paginação 221 | return Response(serializer.data) 222 | 223 | def create(self, request, *args, **kwargs): 224 | serializer = self.get_serializer(data=request.data) 225 | serializer.is_valid(raise_exception=True) 226 | serializer.save() 227 | return Response(serializer.data, status=status.HTTP_201_CREATED) 228 | 229 | def retrieve(self, request, pk=None): 230 | queryset = Student.objects.all() 231 | student = get_object_or_404(queryset, pk=pk) 232 | serializer = StudentSerializer(student) 233 | return Response(serializer.data) 234 | 235 | def update(self, request, *args, **kwargs): 236 | partial = kwargs.pop('partial', False) 237 | instance = self.get_object() 238 | serializer = self.get_serializer(instance, data=request.data, partial=partial) 239 | serializer.is_valid(raise_exception=True) 240 | serializer.save() 241 | return Response(serializer.data) 242 | 243 | def partial_update(self, request, *args, **kwargs): 244 | kwargs['partial'] = True 245 | return self.update(request, *args, **kwargs) 246 | 247 | def destroy(self, request, pk=None): 248 | item = self.get_object() 249 | item.delete() 250 | return Response(status=status.HTTP_204_NO_CONTENT) 251 | 252 | @action(detail=False, methods=['get']) 253 | def all_students(self, request, pk=None): 254 | queryset = Student.objects.all() 255 | serializer = StudentRegistrationSerializer(queryset, many=True) 256 | return Response(serializer.data) 257 | ``` 258 | 259 | 260 | ## GenericViewSet 261 | 262 | https://www.django-rest-framework.org/api-guide/generic-views/ 263 | 264 | Edite `school/serializers.py` 265 | 266 | 267 | ```python 268 | # school/serializers.py 269 | class ClassroomSerializer(serializers.ModelSerializer): 270 | students = serializers.ListSerializer(child=StudentSerializer(), required=False) 271 | ``` 272 | 273 | Edite `school/viewsets.py` 274 | 275 | ```python 276 | # school/viewsets.py 277 | from rest_framework import generics, status, viewsets 278 | from rest_framework.permissions import AllowAny 279 | 280 | from backend.school.models import Classroom, Student 281 | from backend.school.api.serializers import ( 282 | ClassroomSerializer, 283 | StudentRegistrationSerializer, 284 | StudentSerializer 285 | ) 286 | 287 | class ClassroomSerializer(generics.ListCreateAPIView): 288 | queryset = Classroom.objects.all() 289 | serializer_class = ClassroomSerializer 290 | permission_classes = (AllowAny,) 291 | ``` 292 | 293 | 294 | ## ModelViewSet 295 | 296 | https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset 297 | 298 | ```python 299 | # school/viewsets.py 300 | class ClassroomSerializer(viewsets.ModelViewSet): 301 | queryset = Classroom.objects.all() 302 | serializer_class = ClassroomSerializer 303 | permission_classes = (AllowAny,) 304 | ``` 305 | 306 | 307 | ## ReadOnlyModelViewSet 308 | 309 | https://www.django-rest-framework.org/api-guide/viewsets/#readonlymodelviewset 310 | 311 | 312 | Vamos criar mais um model. 313 | 314 | ``` 315 | python manage.py dr_scaffold school Grade \ 316 | student:foreignkey:Student \ 317 | note:decimalfield 318 | ``` 319 | 320 | Edite `school/models.py` 321 | 322 | ```python 323 | # school/models.py 324 | class Grade(models.Model): 325 | student = models.ForeignKey(Student, on_delete=models.CASCADE, null=True) 326 | note = models.DecimalField(max_digits=5, decimal_places=2, null=True, default=0.0) 327 | created = models.DateTimeField(auto_now_add=True) 328 | 329 | def __str__(self): 330 | return f"{self.student} {self.note}" 331 | 332 | class Meta: 333 | verbose_name = "Nota" 334 | verbose_name_plural = "Notas" 335 | ``` 336 | 337 | Edite `school/viewsets.py` 338 | 339 | ```python 340 | # school/viewsets.py 341 | from backend.school.models import Classroom, Grade, Student 342 | from backend.school.api.serializers import ( 343 | ClassroomSerializer, 344 | GradeSerializer, 345 | StudentRegistrationSerializer, 346 | StudentSerializer 347 | ) 348 | 349 | class GradeViewSet(viewsets.ReadOnlyModelViewSet): 350 | queryset = Grade.objects.all() 351 | serializer_class = GradeSerializer 352 | permission_classes = (AllowAny,) 353 | ``` 354 | 355 | Edite `school/urls.py` 356 | 357 | ```python 358 | # school/urls.py 359 | from school.viewsets import GradeViewSet 360 | ``` 361 | 362 | ``` 363 | python manage.py makemigrations 364 | python manage.py migrate 365 | ``` 366 | 367 | 368 | ## Entendendo os métodos do ModelViewSet 369 | 370 | https://www.cdrf.co/ 371 | 372 | ### get_serializer_class 373 | 374 | Usado para escolher qual serializer você quer usar dependendo de determinadas condições. 375 | 376 | **Exemplo:** Suponha que você queira cadastrar um aluno, mas ao editar você só pode alterar o nome e sobrenome. 377 | 378 | Então vamos criar dois serializers. 379 | 380 | ```python 381 | # school/serializers.py 382 | class StudentSerializer(serializers.ModelSerializer): 383 | 384 | class Meta: 385 | model = Student 386 | fields = '__all__' 387 | 388 | 389 | class StudentUpdateSerializer(serializers.ModelSerializer): 390 | 391 | class Meta: 392 | model = Student 393 | fields = ('first_name', 'last_name') 394 | ``` 395 | 396 | E em `school/viewsets.py` 397 | 398 | ```python 399 | # school/viewsets.py 400 | class StudentViewSet(viewsets.ViewSet): 401 | 402 | def get_serializer_class(self): 403 | # Muda o serializer dependendo da ação. 404 | if self.action == 'create': 405 | return StudentSerializer 406 | 407 | if self.action == 'update': 408 | return StudentUpdateSerializer 409 | 410 | return StudentSerializer 411 | ``` 412 | 413 | ### list 414 | 415 | Suponha que eu queira ver somente os meus alunos. 416 | 417 | Primeiro vamos editar alguns arquivos: 418 | 419 | ```python 420 | # school/models.py 421 | from django.contrib.auth.models import User 422 | 423 | class Class(models.Model): 424 | classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE) 425 | teacher = models.ForeignKey(User, on_delete=models.CASCADE) 426 | created = models.DateTimeField(auto_now_add=True) 427 | 428 | def __str__(self): 429 | return f"{self.classroom} {self.teacher}" 430 | 431 | class Meta: 432 | verbose_name = "Aula" 433 | verbose_name_plural = "Aulas" 434 | ``` 435 | 436 | 437 | ```python 438 | # school/admin.py 439 | @admin.register(Class) 440 | class ClassAdmin(admin.ModelAdmin): 441 | exclude = () 442 | ``` 443 | 444 | 445 | ```python 446 | # school/serializers.py 447 | class ClassSerializer(serializers.ModelSerializer): 448 | 449 | class Meta: 450 | model = Class 451 | fields = '__all__' 452 | # depth = 1 # com ele você não consegue fazer um POST direto pelo browser do Django. 453 | ``` 454 | 455 | 456 | ```python 457 | # school/viewsets.py 458 | class ClassViewSet(viewsets.ModelViewSet): 459 | queryset = Class.objects.all() 460 | serializer_class = ClassSerializer 461 | ``` 462 | 463 | 464 | ```python 465 | # school/urls.py 466 | from school.viewsets import ClassViewSet, GradeViewSet 467 | 468 | router.register(r'class', ClassViewSet) 469 | ``` 470 | 471 | #### Editando ClassViewSet 472 | 473 | Voltemos ao arquivo `school/viewsets.py` 474 | 475 | ```python 476 | # school/viewsets.py 477 | class ClassViewSet(viewsets.ModelViewSet): 478 | queryset = Class.objects.all() 479 | serializer_class = ClassSerializer 480 | 481 | def list(self, request, *args, **kwargs): 482 | user = self.request.user 483 | teacher = User.objects.get(username=user) 484 | 485 | if user is not None: 486 | queryset = Class.objects.filter(teacher=teacher) 487 | else: 488 | queryset = Class.objects.none() 489 | 490 | page = self.paginate_queryset(queryset) 491 | if page is not None: 492 | serializer = self.get_serializer(page, many=True) 493 | return self.get_paginated_response(serializer.data) 494 | 495 | serializer = self.get_serializer(queryset, many=True) 496 | return Response(serializer.data) 497 | ``` 498 | 499 | ### get_queryset 500 | 501 | Talvez modificar o queryset padrão já seja suficiente. 502 | 503 | ```python 504 | # school/viewsets.py 505 | class ClassViewSet(viewsets.ModelViewSet): 506 | queryset = Class.objects.all() 507 | serializer_class = ClassSerializer 508 | 509 | # def list(self, request, *args, **kwargs): 510 | # ... 511 | 512 | def get_queryset(self): 513 | user = self.request.user 514 | teacher = User.objects.get(username=user) 515 | 516 | if user is not None: 517 | queryset = Class.objects.filter(teacher=teacher) 518 | else: 519 | queryset = Class.objects.none() 520 | return queryset 521 | ``` 522 | 523 | 524 | ### perform_create 525 | 526 | Usado quando você quiser mudar o comportamento de como seu objeto é criado. 527 | 528 | Vamos editar 529 | 530 | ```python 531 | # school/serializers.py 532 | class ClassAddSerializer(serializers.ModelSerializer): 533 | 534 | class Meta: 535 | model = Class 536 | fields = ('classroom',) 537 | ``` 538 | 539 | ```python 540 | # school/viewsets.py 541 | class ClassViewSet(viewsets.ModelViewSet): 542 | queryset = Class.objects.all() 543 | # serializer_class = ClassSerializer 544 | 545 | def get_serializer_class(self): 546 | if self.action == 'create': 547 | return ClassAddSerializer 548 | 549 | if self.action == 'update': 550 | return ClassSerializer 551 | 552 | return ClassSerializer 553 | 554 | def perform_create(self, serializer): 555 | user = self.request.user 556 | teacher = User.objects.get(username=user) 557 | 558 | if user is not None: 559 | serializer.save(teacher=teacher) 560 | ``` 561 | 562 | 563 | ### create 564 | 565 | Usado quando você quiser mudar a resposta. Exemplo, adicionar dados extras na resposta, etc. 566 | 567 | Leia também [Quando usar o create () do Serializer e o create () perform_create () do ModelViewset](https://qastack.com.br/programming/41094013/when-to-use-serializers-create-and-modelviewsets-create-perform-create) 568 | 569 | Direto de [ModelViewSet.html#create](https://www.cdrf.co/3.12/rest_framework.viewsets/ModelViewSet.html#create) temos: 570 | 571 | ```python 572 | # school/viewsets.py 573 | class ClassViewSet(viewsets.ModelViewSet): 574 | ... 575 | 576 | def create(self, request, *args, **kwargs): 577 | serializer = self.get_serializer(data=request.data) 578 | serializer.is_valid(raise_exception=True) 579 | self.perform_create(serializer) 580 | headers = self.get_success_headers(serializer.data) 581 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 582 | ``` 583 | 584 | ### perform_update 585 | 586 | Usado quando você quiser mudar o comportamento de como seu objeto é editado. 587 | 588 | Ex: https://www.codegrepper.com/code-examples/python/django+rest+model+viewset+standard+update 589 | 590 | ```python 591 | # school/viewsets.py 592 | class ClassViewSet(viewsets.ModelViewSet): 593 | ... 594 | 595 | >>> REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR REVISAR 596 | 597 | def perform_update(self, serializer): 598 | user = self.request.user 599 | data = self.request.data 600 | teacher_id = data.get('teacher') 601 | 602 | try: 603 | teacher = User.objects.get(pk=teacher_id) 604 | except User.DoesNotExist: 605 | raise DRFValidationError('Usuário não encontrado.') 606 | 607 | if user and user.is_authenticated: 608 | if user == teacher: 609 | serializer.save() 610 | else: 611 | raise DRFValidationError('Você não tem permissão para esta operação.') 612 | ``` 613 | 614 | Vamos experimentar pelo Postman. 615 | 616 | Faça autenticação em `Authorization -> Basic Auth`, logando como `admin`. 617 | 618 | ``` 619 | http://localhost:8000/school/class/1/ 620 | PUT 621 | 622 | { 623 | "classroom": 5, 624 | "teacher": 1 625 | } 626 | ``` 627 | 628 | ### retrieve 629 | 630 | Pega apenas uma instância do objeto. 631 | 632 | ```python 633 | # school/viewsets.py 634 | class ClassViewSet(viewsets.ModelViewSet): 635 | ... 636 | 637 | def retrieve(self, request, *args, **kwargs): 638 | ''' 639 | Método para ver os detalhes. 640 | ''' 641 | instance = self.get_object() 642 | teacher = instance.teacher 643 | user = self.request.user 644 | 645 | if user and user.is_authenticated: 646 | if user == teacher: 647 | serializer = self.get_serializer(instance) 648 | else: 649 | raise DRFValidationError('Você não tem acesso a esta aula.') 650 | 651 | return Response(serializer.data) 652 | ``` 653 | 654 | ### delete 655 | 656 | Ex: Deletar somente os seus dados. 657 | 658 | Ou não deletar nada. 659 | 660 | ```python 661 | # school/viewsets.py 662 | class ClassViewSet(viewsets.ModelViewSet): 663 | ... 664 | 665 | def perform_destroy(self, instance): 666 | ''' 667 | Método para deletar os dados. 668 | ''' 669 | # instance.delete() 670 | raise DRFValidationError('Nenhuma aula pode ser deletada.') 671 | ``` 672 | 673 | 674 | ### Remover o delete da rota 675 | 676 | ```python 677 | from rest_framework.generics import GenericAPIView 678 | from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, ListModelMixin 679 | 680 | class BookViewSet( 681 | CreateModelMixin, 682 | RetrieveModelMixin, 683 | UpdateModelMixin, 684 | ListModelMixin, 685 | GenericAPIView 686 | ) 687 | ``` 688 | 689 | Repare que não temos o `DestroyModelMixin`. 690 | 691 | Ilustrações 692 | 693 | https://testdriven.io/blog/drf-views-part-3/ 694 | 695 | --------------------------------------------------------------------------------