├── 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 |
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 |
17 | {% endblock content %}
--------------------------------------------------------------------------------
/backend/order/templates/order/order_form.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "base.html" %}
3 |
4 | {% block content %}
5 | Adicionar Pedido
6 |
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 |
8 | - Tarefa: {{ object.task }}
9 | - Descrição: {{ object.description }}
10 | - Feito?
11 | {% if object.is_done %}
12 |
13 | {% else %}
14 |
15 | {% endif %}
16 |
17 | - Criado em: {{ object.created|date:'d/m/Y'|default:'---' }}
18 |
19 | {% endblock content %}
20 |
--------------------------------------------------------------------------------
/backend/todo/templates/todo/todo_confirm_delete.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "base.html" %}
3 |
4 | {% block content %}
5 | Deletar
6 |
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 |
16 |
17 |
18 |
19 |
20 | | ID |
21 | Pedido |
22 | Funcionário |
23 |
24 |
25 |
26 | {% for object in object_list %}
27 |
28 | | {{ object.id }} |
29 | {{ object.title }} |
30 | {{ object.employee }} |
31 |
32 | {% endfor %}
33 |
34 |
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 |
9 |
10 |
11 |
12 | | Tarefa |
13 | Descrição |
14 | Feito? |
15 | Ações |
16 |
17 |
18 |
19 | {% for object in object_list %}
20 |
21 | |
22 | {{ object.task }}
23 | |
24 | {{ object.description }} |
25 |
26 | {% if object.is_done %}
27 |
28 | {% else %}
29 |
30 | {% endif %}
31 | |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | |
40 |
41 | {% endfor %}
42 |
43 |
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 |
39 |
--------------------------------------------------------------------------------
/passo-a-passo/14_grupos_permissoes.md:
--------------------------------------------------------------------------------
1 | # Django Experience #14 - DRF: Grupos e Permissões
2 |
3 | 
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 | 
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 | 
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 | 
164 |
165 | 
166 |
167 | 
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 |
209 | API V1 Video
210 |
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 |
--------------------------------------------------------------------------------