├── myproject ├── __init__.py ├── core │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── static │ │ ├── css │ │ │ ├── style.css │ │ │ ├── form.css │ │ │ └── icons │ │ │ │ └── simple-line-icons.min.css │ │ ├── fonts │ │ │ ├── gijgo-material.eot │ │ │ ├── gijgo-material.ttf │ │ │ ├── gijgo-material.woff │ │ │ ├── Simple-Line-Icons.eot │ │ │ ├── Simple-Line-Icons.ttf │ │ │ ├── Simple-Line-Icons.woff │ │ │ ├── Simple-Line-Icons.woff2 │ │ │ └── gijgo-material.svg │ │ ├── img │ │ │ └── django-logo-negative.png │ │ └── js │ │ │ ├── popper.min.js │ │ │ └── bootstrap.min.js │ ├── models.py │ ├── admin.py │ ├── tests.py │ ├── apps.py │ ├── urls.py │ ├── views.py │ └── templates │ │ ├── index.html │ │ ├── nav.html │ │ ├── base.html │ │ └── base_login.html ├── accounts │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_profile_id.py │ │ └── 0001_initial.py │ ├── templates │ │ ├── registration │ │ │ ├── password_reset_subject.txt │ │ │ ├── password_reset_complete.html │ │ │ ├── password_reset_email.html │ │ │ ├── password_change_done.html │ │ │ ├── password_reset_done.html │ │ │ ├── password_change_form.html │ │ │ ├── password_reset_form.html │ │ │ └── password_reset_confirm.html │ │ ├── email │ │ │ └── account_activation_email.html │ │ └── accounts │ │ │ ├── account_activation_done.html │ │ │ ├── signup_email_form.html │ │ │ ├── login.html │ │ │ └── signup.html │ ├── tests.py │ ├── apps.py │ ├── admin.py │ ├── tokens.py │ ├── models.py │ ├── urls.py │ ├── forms.py │ └── views.py ├── urls.py ├── asgi.py ├── wsgi.py └── settings.py ├── img ├── youtube.png ├── 01_login.png ├── 02_signup.png ├── 102_signup.png ├── 101_login_logout.png ├── 03_change_password.png ├── 04_forgot_password.png ├── 103_change_password.png └── 104_reset_password.png ├── requirements.txt ├── manage.py ├── contrib └── env_gen.py ├── .gitignore ├── README.md └── passo-a-passo.md /myproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/core/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 60px; 3 | } -------------------------------------------------------------------------------- /img/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/youtube.png -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_reset_subject.txt: -------------------------------------------------------------------------------- 1 | Redefinição de senha 2 | -------------------------------------------------------------------------------- /myproject/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /img/01_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/01_login.png -------------------------------------------------------------------------------- /img/02_signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/02_signup.png -------------------------------------------------------------------------------- /myproject/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /myproject/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /img/102_signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/102_signup.png -------------------------------------------------------------------------------- /myproject/accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /myproject/core/static/css/form.css: -------------------------------------------------------------------------------- 1 | span.required:after { 2 | content: "*"; 3 | color: red; 4 | } -------------------------------------------------------------------------------- /img/101_login_logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/101_login_logout.png -------------------------------------------------------------------------------- /img/03_change_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/03_change_password.png -------------------------------------------------------------------------------- /img/04_forgot_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/04_forgot_password.png -------------------------------------------------------------------------------- /img/103_change_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/103_change_password.png -------------------------------------------------------------------------------- /img/104_reset_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/img/104_reset_password.png -------------------------------------------------------------------------------- /myproject/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'myproject.core' 6 | -------------------------------------------------------------------------------- /myproject/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'myproject.accounts' 6 | -------------------------------------------------------------------------------- /myproject/core/static/fonts/gijgo-material.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/gijgo-material.eot -------------------------------------------------------------------------------- /myproject/core/static/fonts/gijgo-material.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/gijgo-material.ttf -------------------------------------------------------------------------------- /myproject/core/static/fonts/gijgo-material.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/gijgo-material.woff -------------------------------------------------------------------------------- /myproject/core/static/fonts/Simple-Line-Icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/Simple-Line-Icons.eot -------------------------------------------------------------------------------- /myproject/core/static/fonts/Simple-Line-Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/Simple-Line-Icons.ttf -------------------------------------------------------------------------------- /myproject/core/static/fonts/Simple-Line-Icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/Simple-Line-Icons.woff -------------------------------------------------------------------------------- /myproject/core/static/img/django-logo-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/img/django-logo-negative.png -------------------------------------------------------------------------------- /myproject/core/static/fonts/Simple-Line-Icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-auth-tutorial/main/myproject/core/static/fonts/Simple-Line-Icons.woff2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.5.0 2 | django-extensions==3.1.1 3 | django-utils-six==2.0 4 | django-widget-tweaks==1.4.8 5 | Django==4.0.* 6 | isort==5.8.0 7 | python-decouple==3.4 8 | -------------------------------------------------------------------------------- /myproject/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from myproject.core import views as v 4 | 5 | app_name = 'core' 6 | 7 | 8 | urlpatterns = [ 9 | path('', v.index, name='index'), 10 | ] 11 | -------------------------------------------------------------------------------- /myproject/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Profile 4 | 5 | 6 | @admin.register(Profile) 7 | class ProfileAdmin(admin.ModelAdmin): 8 | list_display = ('user', 'cpf', 'rg') 9 | -------------------------------------------------------------------------------- /myproject/core/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import render 3 | 4 | 5 | @login_required 6 | def index(request): 7 | template_name = 'index.html' 8 | return render(request, template_name) 9 | -------------------------------------------------------------------------------- /myproject/accounts/templates/email/account_activation_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | Olá {{ user.username }}, 3 | 4 | Por favor clique no link abaixo para confirmar seu cadastro: 5 | 6 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 7 | {% endautoescape %} 8 | -------------------------------------------------------------------------------- /myproject/accounts/templates/accounts/account_activation_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_login.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 | django-logo-negative.png 7 |

Por favor confirme seu e-mail para completar o cadastro.

8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /myproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path('', include('myproject.core.urls', namespace='core')), 6 | # path('', include('django.contrib.auth.urls')), # sem namespace 7 | path('accounts/', include('myproject.accounts.urls')), # sem namespace 8 | path('admin/', admin.site.urls), 9 | ] 10 | -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_login.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 | django-logo-negative.png 7 |

Sua senha foi definida. Você pode prosseguir e se autenticar agora.

8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /myproject/core/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |

Guia definitivo de autenticação com Django

7 | github.com/rg3915/django-auth-tutorial 8 |

Olá {{ user }}

9 |
10 | {% endblock content %} -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | Para iniciar o processo de redefinição de senha para sua conta {{ user.get_username }}, clique no link abaixo: 3 | 4 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 5 | 6 | Se clicar no link acima não funcionar, por favor copie e cole a URL no navegador. 7 | 8 | Atenciosamente, 9 | Equipe Dev. 10 | {% endautoescape %} 11 | -------------------------------------------------------------------------------- /myproject/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for myproject project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/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', 'myproject.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /myproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for myproject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/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', 'myproject.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /myproject/accounts/tokens.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 2 | from django.utils import six 3 | 4 | 5 | class AccountActivationTokenGenerator(PasswordResetTokenGenerator): 6 | def _make_hash_value(self, user, timestamp): 7 | return ( 8 | six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.email) # noqa E501 9 | ) 10 | 11 | 12 | account_activation_token = AccountActivationTokenGenerator() 13 | -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_login.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 | django-logo-negative.png 7 |

Mudança de senha bem sucedida.

8 |

Sua senha foi alterada.

9 | Login 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /myproject/accounts/migrations/0002_alter_profile_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-10 01:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='profile', 15 | name='id', 16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_login.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 | django-logo-negative.png 7 |

Nós te enviamos um email com instruções para configurar sua senha, se uma conta existe com o email fornecido. Você receberá a mensagem em breve.

8 |

Se você não recebeu um email, por favor certifique-se que você forneceu o endereço que você está cadastrado, e verifique sua pasta de spam.

9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /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', 'myproject.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 | -------------------------------------------------------------------------------- /myproject/accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | 6 | 7 | class Profile(models.Model): 8 | cpf = models.CharField('CPF', max_length=11, unique=True, null=True, blank=True) # noqa E501 9 | rg = models.CharField('RG', max_length=20, null=True, blank=True) 10 | user = models.OneToOneField(User, on_delete=models.CASCADE) 11 | 12 | class Meta: 13 | ordering = ('cpf',) 14 | verbose_name = 'perfil' 15 | verbose_name_plural = 'perfis' 16 | 17 | def __str__(self): 18 | if self.cpf: 19 | return self.cpf 20 | return self.user.username 21 | 22 | 23 | @receiver(post_save, sender=User) 24 | def update_user_profile(sender, instance, created, **kwargs): 25 | if created: 26 | Profile.objects.create(user=instance) 27 | instance.profile.save() 28 | -------------------------------------------------------------------------------- /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 18 | 19 | #DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME 20 | #DB_NAME= 21 | #DB_USER= 22 | #DB_PASSWORD=%s 23 | #DB_HOST=localhost 24 | 25 | #EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 26 | DEFAULT_FROM_EMAIL=noreply@admin.com 27 | EMAIL_HOST=0.0.0.0 28 | EMAIL_PORT=1025 29 | EMAIL_HOST_USER=noreply@admin.com 30 | EMAIL_HOST_PASSWORD=p4ssw0rd 31 | EMAIL_USE_TLS=False 32 | """.strip() % (secret_key, password) 33 | 34 | # Writing our configuration file to '.env' 35 | with open('.env', 'w') as configfile: 36 | configfile.write(CONFIG_STRING) 37 | 38 | print('Success!') 39 | print('Type: cat .env') 40 | -------------------------------------------------------------------------------- /myproject/core/templates/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /myproject/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.8 on 2021-04-09 00:28 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='Profile', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('cpf', models.CharField(blank=True, max_length=11, null=True, unique=True, verbose_name='CPF')), 22 | ('rg', models.CharField(blank=True, max_length=20, null=True, verbose_name='RG')), 23 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'verbose_name': 'perfil', 27 | 'verbose_name_plural': 'perfis', 28 | 'ordering': ('cpf',), 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /myproject/core/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Django Auth 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% block css %}{% endblock css %} 21 | 22 | 23 | 24 | 25 |
26 | {% include "nav.html" %} 27 | {% block content %}{% endblock content %} 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /myproject/core/templates/base_login.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}{% endblock title %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% block css %}{% endblock css %} 23 | 24 | 25 | 26 |
27 |
28 | {% block content %}{% endblock content %} 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /myproject/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.contrib.auth.views import LoginView, LogoutView 3 | from django.urls import path 4 | 5 | from myproject.accounts import views as v 6 | 7 | # Se usar app_name vai dar erro de redirect em PasswordResetView. 8 | # app_name = 'accounts' 9 | 10 | 11 | urlpatterns = [ 12 | path( 13 | 'login/', 14 | LoginView.as_view(template_name='accounts/login.html'), 15 | name='login' 16 | ), 17 | # Se vc importou django.contrib.auth.urls em urls.py então não precisa do logout abaixo. # noqa E501 18 | path('logout/', LogoutView.as_view(), name='logout'), 19 | path('signup/', v.signup, name='signup'), 20 | # path('signup/', v.SignUpView.as_view(), name='signup'), 21 | path('signup-email/', v.signup_email, name='signup_email'), 22 | path( 23 | 'account-activation-done/', 24 | v.account_activation_done, 25 | name='account_activation_done' 26 | ), 27 | path( 28 | 'password_change/', 29 | v.MyPasswordChange.as_view(), 30 | name='password_change' 31 | ), 32 | path( 33 | 'password_change/done/', 34 | v.MyPasswordChangeDone.as_view(), 35 | name='password_change_done' 36 | ), 37 | path( 38 | 'password_reset/', 39 | v.MyPasswordReset.as_view(), 40 | name='password_reset' 41 | ), 42 | path( 43 | 'password_reset/done/', 44 | v.MyPasswordResetDone.as_view(), 45 | name='password_reset_done' 46 | ), 47 | path( 48 | 'reset///', 49 | v.MyPasswordResetConfirm.as_view(), 50 | name='password_reset_confirm' 51 | ), 52 | path( 53 | 'reset/done/', 54 | v.MyPasswordResetComplete.as_view(), 55 | name='password_reset_complete' 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /myproject/accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class SignupForm(UserCreationForm): 7 | first_name = forms.CharField( 8 | label='Nome', 9 | max_length=30, 10 | required=False, 11 | widget=forms.TextInput(attrs={'autofocus': 'autofocus'}) 12 | ) 13 | last_name = forms.CharField(label='Sobrenome', max_length=30, required=False) # noqa E501 14 | username = forms.CharField(label='Usuário', max_length=150) 15 | email = forms.CharField( 16 | label='E-mail', 17 | max_length=254, 18 | help_text='Requerido. Informe um e-mail válido.', 19 | ) 20 | cpf = forms.CharField(label='CPF') 21 | rg = forms.CharField(label='RG', required=False) 22 | 23 | class Meta: 24 | model = User 25 | fields = ( 26 | 'first_name', 27 | 'last_name', 28 | 'cpf', 29 | 'rg', 30 | 'username', 31 | 'email', 32 | 'password1', 33 | 'password2' 34 | ) 35 | 36 | 37 | class SignupEmailForm(forms.ModelForm): 38 | first_name = forms.CharField( 39 | label='Nome', 40 | max_length=30, 41 | required=False, 42 | widget=forms.TextInput(attrs={'autofocus': 'autofocus'}) 43 | ) 44 | last_name = forms.CharField(label='Sobrenome', max_length=30, required=False) # noqa E501 45 | username = forms.CharField(label='Usuário', max_length=150) 46 | email = forms.CharField( 47 | label='E-mail', 48 | max_length=254, 49 | help_text='Requerido. Informe um e-mail válido.', 50 | ) 51 | cpf = forms.CharField(label='CPF') 52 | rg = forms.CharField(label='RG', required=False) 53 | 54 | class Meta: 55 | model = User 56 | fields = ( 57 | 'first_name', 58 | 'last_name', 59 | 'cpf', 60 | 'rg', 61 | 'username', 62 | 'email', 63 | ) 64 | -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_login.html" %} 2 | {% load static %} 3 | {% load widget_tweaks %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Trocar a senha

11 |

12 |
13 | {% csrf_token %} 14 | {% for field in form.visible_fields %} 15 |
16 | 23 |
24 |
25 | 26 | {% if field.name == 'old_password' or field.name == 'new_password1' or field.name == 'new_password2' %} 27 | 28 | {% else %} 29 | 30 | {% endif %} 31 | 32 |
33 | {% render_field field class="form-control" placeholder=field.label %} 34 |
35 | {{ field.help_text }} 36 | {% for error in field.errors %} 37 |
{{ error }} 38 | {% endfor %} 39 |
40 | {% endfor %} 41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | django-logo-negative.png 49 |

Login

50 |

Faça login.

51 | Login 52 |
53 |
54 |
55 |
56 |
57 | {% endblock content %} 58 | -------------------------------------------------------------------------------- /.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 | .ipynb_checkpoints/ 136 | -------------------------------------------------------------------------------- /myproject/accounts/templates/accounts/signup_email_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_login.html" %} 2 | {% load static %} 3 | {% load widget_tweaks %} 4 | 5 | {% block title %}Signup{% endblock title %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |

Cadastre-se

13 |

Crie sua conta.

14 | 15 |
16 | {% csrf_token %} 17 | {% for field in form.visible_fields %} 18 |
19 | 26 |
27 |
28 | 29 | {% if field.name == 'email' %} 30 | @ 31 | {% elif field.name == 'username' %} 32 | 33 | {% else %} 34 | 35 | {% endif %} 36 | 37 |
38 | {% render_field field class="form-control" placeholder=field.label %} 39 |
40 | {{ field.help_text }} 41 | {% for error in field.errors %} 42 |
{{ error }} 43 | {% endfor %} 44 |
45 | {% endfor %} 46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 | django-logo-negative.png 55 |

Login

56 |

Já possui cadastro?

57 | Login 58 |
59 |
60 |
61 |
62 |
63 | {% endblock content %} 64 | -------------------------------------------------------------------------------- /myproject/accounts/templates/accounts/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base_login.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Login{% endblock title %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |
11 |

Login

12 | 13 | 14 | {% if form.errors %} 15 | {% for error in form.non_field_errors %} 16 | 17 | {% endfor %} 18 | {% endif %} 19 | 20 |
21 | {% csrf_token %} 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 | 45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 | django-logo-negative.png 54 |

Cadastre-se

55 |

Guia de autenticação do Django.

56 | 57 | Cadastre-se 58 |
59 |
60 |
61 |
62 |
63 | {% endblock content %} 64 | -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_login.html" %} 2 | {% load static %} 3 | {% load widget_tweaks %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Esqueci a senha

11 |

Digite seu e-mail.

12 |
13 | {% csrf_token %} 14 | {% for field in form.visible_fields %} 15 |
16 | 23 |
24 |
25 | 26 | {% if field.name == 'email' %} 27 | @ 28 | {% elif field.name == 'password1' or field.name == 'password2' %} 29 | 30 | {% elif field.name == 'username' %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | 36 |
37 | {% render_field field class="form-control" placeholder=field.label %} 38 |
39 | {{ field.help_text }} 40 | {% for error in field.errors %} 41 |
{{ error }} 42 | {% endfor %} 43 |
44 | {% endfor %} 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | django-logo-negative.png 53 |

Login

54 |

Faça login.

55 | Login 56 |
57 |
58 |
59 |
60 |
61 | {% endblock content %} 62 | -------------------------------------------------------------------------------- /myproject/accounts/templates/accounts/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base_login.html" %} 2 | {% load static %} 3 | {% load widget_tweaks %} 4 | 5 | {% block title %}Signup{% endblock title %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |

Cadastre-se

13 |

Crie sua conta.

14 | 15 |
16 | {% csrf_token %} 17 | {% for field in form.visible_fields %} 18 |
19 | 26 |
27 |
28 | 29 | {% if field.name == 'email' %} 30 | @ 31 | {% elif field.name == 'password1' or field.name == 'password2' %} 32 | 33 | {% elif field.name == 'username' %} 34 | 35 | {% else %} 36 | 37 | {% endif %} 38 | 39 |
40 | {% render_field field class="form-control" placeholder=field.label %} 41 |
42 | {{ field.help_text }} 43 | {% for error in field.errors %} 44 |
{{ error }} 45 | {% endfor %} 46 |
47 | {% endfor %} 48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 | django-logo-negative.png 57 |

Login

58 |

Faça login.

59 | Login 60 |
61 |
62 |
63 |
64 |
65 | {% endblock content %} 66 | -------------------------------------------------------------------------------- /myproject/accounts/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base_login.html" %} 2 | {% load static %} 3 | {% load widget_tweaks %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Trocar senha

11 |

Digite sua nova senha.

12 | {% if validlink %} 13 |
14 | {% csrf_token %} 15 | {% for field in form.visible_fields %} 16 |
17 | 24 |
25 |
26 | 27 | {% if field.name == 'new_password1' or field.name == 'new_password2' %} 28 | 29 | {% else %} 30 | 31 | {% endif %} 32 | 33 |
34 | {% render_field field class="form-control" placeholder=field.label %} 35 |
36 | {{ field.help_text }} 37 | {% for error in field.errors %} 38 |
{{ error }} 39 | {% endfor %} 40 |
41 | {% endfor %} 42 | 43 |
44 | {% else %} 45 |

46 | O link para a recuperação de senha era inválido, possivelmente porque já foi utilizado. Por favor, solicite uma nova recuperação de senha. 47 |

48 | {% endif %} 49 |
50 |
51 |
52 |
53 |
54 | django-logo-negative.png 55 |

Login

56 |

Faça login.

57 | Login 58 |
59 |
60 |
61 |
62 |
63 | {% endblock content %} 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guia de autenticação com Django 2 | 3 | Live parte 1 4 | 5 | 6 | 7 | 8 | 9 | Live parte 2 10 | 11 | 12 | 13 | 14 | 15 | 16 | ## Este projeto foi feito com: 17 | 18 | * [Python 3.8.2](https://www.python.org/) 19 | * [Django 4.0.*](https://www.djangoproject.com/) 20 | * [Bootstrap 4.0](https://getbootstrap.com/) 21 | 22 | ## Como rodar o projeto? 23 | 24 | * Clone esse repositório. 25 | * Crie um virtualenv com Python 3. 26 | * Ative o virtualenv. 27 | * Instale as dependências. 28 | * Rode as migrações. 29 | 30 | ``` 31 | git clone https://github.com/rg3915/django-auth-tutorial.git 32 | cd django-auth-tutorial 33 | python3 -m venv .venv 34 | source .venv/bin/activate 35 | pip install -r requirements.txt 36 | python contrib/env_gen.py 37 | python manage.py migrate 38 | python manage.py createsuperuser --username='admin' --email='' 39 | ``` 40 | 41 | ### Configurar settings.py 42 | 43 | ```python 44 | INSTALLED_APPS = [ 45 | 'myproject.accounts', # <--- 46 | 'django.contrib.admin', 47 | 'django.contrib.auth', 48 | ... 49 | 'django_extensions', 50 | 'widget_tweaks', 51 | 'myproject.core', 52 | ] 53 | 54 | LOGIN_URL = 'login' 55 | LOGIN_REDIRECT_URL = 'core:index' 56 | LOGOUT_REDIRECT_URL = 'core:index' 57 | ``` 58 | 59 | Leia o [passo-a-passo.md](passo-a-passo.md). 60 | 61 | 62 | 63 | ## Telas 64 | 65 | ### Login 66 | 67 | ![01_login.png](img/01_login.png) 68 | 69 | ### Cadastro 70 | 71 | ![02_signup.png](img/02_signup.png) 72 | 73 | ### Trocar senha 74 | 75 | ![03_change_password.png](img/03_change_password.png) 76 | 77 | ### Esqueci minha senha 78 | 79 | ![04_forgot_password.png](img/04_forgot_password.png) 80 | 81 | 82 | 83 | ## Estrutura 84 | 85 | ### Login 86 | 87 | ![101_login_logout.png](img/101_login_logout.png) 88 | 89 | ### Cadastro 90 | 91 | ![102_signup.png](img/102_signup.png) 92 | 93 | ### Trocar senha 94 | 95 | ![103_change_password.png](img/103_change_password.png) 96 | 97 | ### Esqueci minha senha 98 | 99 | ![104_reset_password.png](img/104_reset_password.png) 100 | 101 | 102 | 103 | ## MailHog 104 | 105 | Rodar [MailHog](https://github.com/mailhog/MailHog) via Docker. 106 | 107 | ``` 108 | docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog 109 | ``` 110 | 111 | ### Configurar settings.py 112 | 113 | ```python 114 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 115 | 116 | DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', 'webmaster@localhost') 117 | EMAIL_HOST = config('EMAIL_HOST', '0.0.0.0') # localhost 118 | EMAIL_PORT = config('EMAIL_PORT', 1025, cast=int) 119 | EMAIL_HOST_USER = config('EMAIL_HOST_USER', '') 120 | EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', '') 121 | EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool) 122 | ``` 123 | 124 | 125 | 126 | ## Links 127 | 128 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#module-django.contrib.auth.views 129 | 130 | https://simpleisbetterthancomplex.com/tutorial/2016/09/19/how-to-create-password-reset-view.html 131 | 132 | https://simpleisbetterthancomplex.com/tutorial/2017/02/18/how-to-create-user-sign-up-view.html 133 | 134 | https://simpleisbetterthancomplex.com/tips/2016/08/04/django-tip-9-password-change-form.html 135 | 136 | https://github.com/egorsmkv/simple-django-login-and-register 137 | 138 | https://github.com/Antonio-Neves/Custom-User-Django-pt 139 | 140 | https://github.com/django/django/tree/main/django/contrib/admin/templates/registration 141 | 142 | https://github.com/django/django/blob/main/django/contrib/auth/views.py 143 | 144 | https://github.com/django/django/blob/main/django/contrib/auth/forms.py 145 | 146 | https://github.com/django/django/blob/main/django/contrib/auth/tokens.py 147 | -------------------------------------------------------------------------------- /myproject/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for myproject project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | from decouple import Csv, config 16 | from dj_database_url import parse as dburl 17 | 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | SECRET_KEY = config('SECRET_KEY') 21 | 22 | DEBUG = config('DEBUG', default=False, cast=bool) 23 | 24 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) 25 | 26 | # Application definition 27 | 28 | INSTALLED_APPS = [ 29 | 'myproject.accounts', # <--- 30 | 'django.contrib.admin', 31 | 'django.contrib.auth', 32 | 'django.contrib.contenttypes', 33 | 'django.contrib.sessions', 34 | 'django.contrib.messages', 35 | 'django.contrib.staticfiles', 36 | 'django_extensions', 37 | 'widget_tweaks', 38 | 'myproject.core', 39 | ] 40 | 41 | MIDDLEWARE = [ 42 | 'django.middleware.security.SecurityMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 49 | ] 50 | 51 | ROOT_URLCONF = 'myproject.urls' 52 | 53 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 54 | 55 | DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', 'webmaster@localhost') 56 | EMAIL_HOST = config('EMAIL_HOST', '0.0.0.0') # localhost 57 | EMAIL_PORT = config('EMAIL_PORT', 1025, cast=int) 58 | EMAIL_HOST_USER = config('EMAIL_HOST_USER', '') 59 | EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', '') 60 | EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool) 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'myproject.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 83 | 84 | default_dburl = 'sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3') 85 | DATABASES = { 86 | 'default': config('DATABASE_URL', default=default_dburl, cast=dburl), 87 | } 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'pt-br' 112 | 113 | TIME_ZONE = 'America/Sao_Paulo' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | 127 | LOGIN_URL = 'login' 128 | LOGIN_REDIRECT_URL = 'core:index' 129 | LOGOUT_REDIRECT_URL = 'core:index' 130 | 131 | # Default primary key field type 132 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 133 | 134 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 135 | -------------------------------------------------------------------------------- /myproject/accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate 2 | from django.contrib.auth import login as auth_login 3 | from django.contrib.auth.views import ( 4 | PasswordChangeDoneView, 5 | PasswordChangeView, 6 | PasswordResetCompleteView, 7 | PasswordResetConfirmView, 8 | PasswordResetDoneView, 9 | PasswordResetView 10 | ) 11 | from django.contrib.sites.shortcuts import get_current_site 12 | from django.shortcuts import redirect, render 13 | from django.template.loader import render_to_string 14 | from django.urls import reverse_lazy 15 | from django.utils.encoding import force_bytes 16 | from django.utils.http import urlsafe_base64_encode 17 | from django.views.generic import CreateView 18 | 19 | from myproject.accounts.forms import SignupEmailForm, SignupForm 20 | from myproject.accounts.tokens import account_activation_token 21 | 22 | 23 | def signup(request): 24 | form = SignupForm(request.POST or None) 25 | context = {'form': form} 26 | if request.method == 'POST': 27 | if form.is_valid(): 28 | user = form.save(commit=False) 29 | user.is_active = True 30 | user.save() # precisa salvar para rodar o signal. 31 | # carrega a instância do perfil criada pelo signal. 32 | user.refresh_from_db() 33 | user.profile.cpf = form.cleaned_data.get('cpf') 34 | user.profile.rg = form.cleaned_data.get('rg') 35 | user.save() 36 | 37 | username = form.cleaned_data.get('username') 38 | raw_password = form.cleaned_data.get('password1') 39 | 40 | # Autentica usuário 41 | user_auth = authenticate(username=username, password=raw_password) 42 | 43 | # Faz login 44 | auth_login(request, user_auth) 45 | return redirect(reverse_lazy('core:index')) 46 | 47 | return render(request, 'accounts/signup.html', context) 48 | 49 | 50 | class SignUpView(CreateView): 51 | form_class = SignupForm 52 | success_url = reverse_lazy('login') 53 | template_name = 'accounts/signup.html' 54 | 55 | 56 | def send_mail_to_user(request, user): 57 | current_site = get_current_site(request) 58 | use_https = request.is_secure() 59 | subject = 'Ative sua conta.' 60 | message = render_to_string('email/account_activation_email.html', { 61 | 'user': user, 62 | 'protocol': 'https' if use_https else 'http', 63 | 'domain': current_site.domain, 64 | 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 65 | 'token': account_activation_token.make_token(user), 66 | }) 67 | user.email_user(subject, message) 68 | 69 | 70 | def signup_email(request): 71 | form = SignupEmailForm(request.POST or None) 72 | context = {'form': form} 73 | if request.method == 'POST': 74 | if form.is_valid(): 75 | user = form.save(commit=False) 76 | user.is_active = False 77 | user.save() # precisa salvar para rodar o signal. 78 | # carrega a instância do perfil criada pelo signal. 79 | user.refresh_from_db() 80 | user.profile.cpf = form.cleaned_data.get('cpf') 81 | user.profile.rg = form.cleaned_data.get('rg') 82 | user.save() 83 | send_mail_to_user(request, user) 84 | return redirect('account_activation_done') 85 | 86 | return render(request, 'accounts/signup_email_form.html', context) 87 | 88 | 89 | def account_activation_done(request): 90 | return render(request, 'accounts/account_activation_done.html') 91 | 92 | 93 | class MyPasswordChange(PasswordChangeView): 94 | ... 95 | 96 | 97 | class MyPasswordChangeDone(PasswordChangeDoneView): 98 | 99 | def get(self, request, *args, **kwargs): 100 | return redirect(reverse_lazy('login')) 101 | 102 | 103 | # Requer 104 | # registration/password_reset_email.html 105 | # registration/password_reset_subject.txt 106 | class MyPasswordReset(PasswordResetView): 107 | ... 108 | 109 | 110 | class MyPasswordResetDone(PasswordResetDoneView): 111 | ... 112 | 113 | 114 | class MyPasswordResetConfirm(PasswordResetConfirmView): 115 | 116 | def form_valid(self, form): 117 | self.user.is_active = True 118 | self.user.save() 119 | return super(MyPasswordResetConfirm, self).form_valid(form) 120 | 121 | 122 | class MyPasswordResetComplete(PasswordResetCompleteView): 123 | ... 124 | -------------------------------------------------------------------------------- /myproject/core/static/css/icons/simple-line-icons.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:simple-line-icons;src:url(../../fonts/Simple-Line-Icons.eot);src:url(../../fonts/Simple-Line-Icons.eot) format('embedded-opentype'),url(../../fonts/Simple-Line-Icons.woff2) format('woff2'),url(../../fonts/Simple-Line-Icons.ttf) format('truetype'),url(../../fonts/Simple-Line-Icons.woff) format('woff'),url(../../fonts/Simple-Line-Icons.svg#simple-line-icons) format('svg');font-weight:400;font-style:normal}.icon-action-redo,.icon-action-undo,.icon-anchor,.icon-arrow-down,.icon-arrow-down-circle,.icon-arrow-left,.icon-arrow-left-circle,.icon-arrow-right,.icon-arrow-right-circle,.icon-arrow-up,.icon-arrow-up-circle,.icon-badge,.icon-bag,.icon-ban,.icon-basket,.icon-basket-loaded,.icon-bell,.icon-book-open,.icon-briefcase,.icon-bubble,.icon-bubbles,.icon-bulb,.icon-calculator,.icon-calendar,.icon-call-end,.icon-call-in,.icon-call-out,.icon-camera,.icon-camrecorder,.icon-chart,.icon-check,.icon-chemistry,.icon-clock,.icon-close,.icon-cloud-download,.icon-cloud-upload,.icon-compass,.icon-control-end,.icon-control-forward,.icon-control-pause,.icon-control-play,.icon-control-rewind,.icon-control-start,.icon-credit-card,.icon-crop,.icon-cup,.icon-cursor,.icon-cursor-move,.icon-diamond,.icon-direction,.icon-directions,.icon-disc,.icon-dislike,.icon-doc,.icon-docs,.icon-drawer,.icon-drop,.icon-earphones,.icon-earphones-alt,.icon-emotsmile,.icon-energy,.icon-envelope,.icon-envelope-letter,.icon-envelope-open,.icon-equalizer,.icon-event,.icon-exclamation,.icon-eye,.icon-eyeglass,.icon-feed,.icon-film,.icon-fire,.icon-flag,.icon-folder,.icon-folder-alt,.icon-frame,.icon-game-controller,.icon-ghost,.icon-globe,.icon-globe-alt,.icon-graduation,.icon-graph,.icon-grid,.icon-handbag,.icon-heart,.icon-home,.icon-hourglass,.icon-info,.icon-key,.icon-layers,.icon-like,.icon-link,.icon-list,.icon-location-pin,.icon-lock,.icon-lock-open,.icon-login,.icon-logout,.icon-loop,.icon-magic-wand,.icon-magnet,.icon-magnifier,.icon-magnifier-add,.icon-magnifier-remove,.icon-map,.icon-menu,.icon-microphone,.icon-minus,.icon-mouse,.icon-music-tone,.icon-music-tone-alt,.icon-mustache,.icon-note,.icon-notebook,.icon-options,.icon-options-vertical,.icon-organization,.icon-paper-clip,.icon-paper-plane,.icon-paypal,.icon-pencil,.icon-people,.icon-phone,.icon-picture,.icon-pie-chart,.icon-pin,.icon-plane,.icon-playlist,.icon-plus,.icon-power,.icon-present,.icon-printer,.icon-puzzle,.icon-question,.icon-refresh,.icon-reload,.icon-rocket,.icon-screen-desktop,.icon-screen-smartphone,.icon-screen-tablet,.icon-settings,.icon-share,.icon-share-alt,.icon-shield,.icon-shuffle,.icon-size-actual,.icon-size-fullscreen,.icon-social-behance,.icon-social-dribbble,.icon-social-dropbox,.icon-social-facebook,.icon-social-foursqare,.icon-social-github,.icon-social-google,.icon-social-instagram,.icon-social-linkedin,.icon-social-pinterest,.icon-social-reddit,.icon-social-skype,.icon-social-soundcloud,.icon-social-spotify,.icon-social-steam,.icon-social-stumbleupon,.icon-social-tumblr,.icon-social-twitter,.icon-social-vkontakte,.icon-social-youtube,.icon-speech,.icon-speedometer,.icon-star,.icon-support,.icon-symbol-female,.icon-symbol-male,.icon-tag,.icon-target,.icon-trash,.icon-trophy,.icon-umbrella,.icon-user,.icon-user-female,.icon-user-follow,.icon-user-following,.icon-user-unfollow,.icon-vector,.icon-volume-1,.icon-volume-2,.icon-volume-off,.icon-wallet,.icon-wrench{font-family:simple-line-icons;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-user:before{content:"\e005"}.icon-people:before{content:"\e001"}.icon-user-female:before{content:"\e000"}.icon-user-follow:before{content:"\e002"}.icon-user-following:before{content:"\e003"}.icon-user-unfollow:before{content:"\e004"}.icon-login:before{content:"\e066"}.icon-logout:before{content:"\e065"}.icon-emotsmile:before{content:"\e021"}.icon-phone:before{content:"\e600"}.icon-call-end:before{content:"\e048"}.icon-call-in:before{content:"\e047"}.icon-call-out:before{content:"\e046"}.icon-map:before{content:"\e033"}.icon-location-pin:before{content:"\e096"}.icon-direction:before{content:"\e042"}.icon-directions:before{content:"\e041"}.icon-compass:before{content:"\e045"}.icon-layers:before{content:"\e034"}.icon-menu:before{content:"\e601"}.icon-list:before{content:"\e067"}.icon-options-vertical:before{content:"\e602"}.icon-options:before{content:"\e603"}.icon-arrow-down:before{content:"\e604"}.icon-arrow-left:before{content:"\e605"}.icon-arrow-right:before{content:"\e606"}.icon-arrow-up:before{content:"\e607"}.icon-arrow-up-circle:before{content:"\e078"}.icon-arrow-left-circle:before{content:"\e07a"}.icon-arrow-right-circle:before{content:"\e079"}.icon-arrow-down-circle:before{content:"\e07b"}.icon-check:before{content:"\e080"}.icon-clock:before{content:"\e081"}.icon-plus:before{content:"\e095"}.icon-minus:before{content:"\e615"}.icon-close:before{content:"\e082"}.icon-event:before{content:"\e619"}.icon-exclamation:before{content:"\e617"}.icon-organization:before{content:"\e616"}.icon-trophy:before{content:"\e006"}.icon-screen-smartphone:before{content:"\e010"}.icon-screen-desktop:before{content:"\e011"}.icon-plane:before{content:"\e012"}.icon-notebook:before{content:"\e013"}.icon-mustache:before{content:"\e014"}.icon-mouse:before{content:"\e015"}.icon-magnet:before{content:"\e016"}.icon-energy:before{content:"\e020"}.icon-disc:before{content:"\e022"}.icon-cursor:before{content:"\e06e"}.icon-cursor-move:before{content:"\e023"}.icon-crop:before{content:"\e024"}.icon-chemistry:before{content:"\e026"}.icon-speedometer:before{content:"\e007"}.icon-shield:before{content:"\e00e"}.icon-screen-tablet:before{content:"\e00f"}.icon-magic-wand:before{content:"\e017"}.icon-hourglass:before{content:"\e018"}.icon-graduation:before{content:"\e019"}.icon-ghost:before{content:"\e01a"}.icon-game-controller:before{content:"\e01b"}.icon-fire:before{content:"\e01c"}.icon-eyeglass:before{content:"\e01d"}.icon-envelope-open:before{content:"\e01e"}.icon-envelope-letter:before{content:"\e01f"}.icon-bell:before{content:"\e027"}.icon-badge:before{content:"\e028"}.icon-anchor:before{content:"\e029"}.icon-wallet:before{content:"\e02a"}.icon-vector:before{content:"\e02b"}.icon-speech:before{content:"\e02c"}.icon-puzzle:before{content:"\e02d"}.icon-printer:before{content:"\e02e"}.icon-present:before{content:"\e02f"}.icon-playlist:before{content:"\e030"}.icon-pin:before{content:"\e031"}.icon-picture:before{content:"\e032"}.icon-handbag:before{content:"\e035"}.icon-globe-alt:before{content:"\e036"}.icon-globe:before{content:"\e037"}.icon-folder-alt:before{content:"\e039"}.icon-folder:before{content:"\e089"}.icon-film:before{content:"\e03a"}.icon-feed:before{content:"\e03b"}.icon-drop:before{content:"\e03e"}.icon-drawer:before{content:"\e03f"}.icon-docs:before{content:"\e040"}.icon-doc:before{content:"\e085"}.icon-diamond:before{content:"\e043"}.icon-cup:before{content:"\e044"}.icon-calculator:before{content:"\e049"}.icon-bubbles:before{content:"\e04a"}.icon-briefcase:before{content:"\e04b"}.icon-book-open:before{content:"\e04c"}.icon-basket-loaded:before{content:"\e04d"}.icon-basket:before{content:"\e04e"}.icon-bag:before{content:"\e04f"}.icon-action-undo:before{content:"\e050"}.icon-action-redo:before{content:"\e051"}.icon-wrench:before{content:"\e052"}.icon-umbrella:before{content:"\e053"}.icon-trash:before{content:"\e054"}.icon-tag:before{content:"\e055"}.icon-support:before{content:"\e056"}.icon-frame:before{content:"\e038"}.icon-size-fullscreen:before{content:"\e057"}.icon-size-actual:before{content:"\e058"}.icon-shuffle:before{content:"\e059"}.icon-share-alt:before{content:"\e05a"}.icon-share:before{content:"\e05b"}.icon-rocket:before{content:"\e05c"}.icon-question:before{content:"\e05d"}.icon-pie-chart:before{content:"\e05e"}.icon-pencil:before{content:"\e05f"}.icon-note:before{content:"\e060"}.icon-loop:before{content:"\e064"}.icon-home:before{content:"\e069"}.icon-grid:before{content:"\e06a"}.icon-graph:before{content:"\e06b"}.icon-microphone:before{content:"\e063"}.icon-music-tone-alt:before{content:"\e061"}.icon-music-tone:before{content:"\e062"}.icon-earphones-alt:before{content:"\e03c"}.icon-earphones:before{content:"\e03d"}.icon-equalizer:before{content:"\e06c"}.icon-like:before{content:"\e068"}.icon-dislike:before{content:"\e06d"}.icon-control-start:before{content:"\e06f"}.icon-control-rewind:before{content:"\e070"}.icon-control-play:before{content:"\e071"}.icon-control-pause:before{content:"\e072"}.icon-control-forward:before{content:"\e073"}.icon-control-end:before{content:"\e074"}.icon-volume-1:before{content:"\e09f"}.icon-volume-2:before{content:"\e0a0"}.icon-volume-off:before{content:"\e0a1"}.icon-calendar:before{content:"\e075"}.icon-bulb:before{content:"\e076"}.icon-chart:before{content:"\e077"}.icon-ban:before{content:"\e07c"}.icon-bubble:before{content:"\e07d"}.icon-camrecorder:before{content:"\e07e"}.icon-camera:before{content:"\e07f"}.icon-cloud-download:before{content:"\e083"}.icon-cloud-upload:before{content:"\e084"}.icon-envelope:before{content:"\e086"}.icon-eye:before{content:"\e087"}.icon-flag:before{content:"\e088"}.icon-heart:before{content:"\e08a"}.icon-info:before{content:"\e08b"}.icon-key:before{content:"\e08c"}.icon-link:before{content:"\e08d"}.icon-lock:before{content:"\e08e"}.icon-lock-open:before{content:"\e08f"}.icon-magnifier:before{content:"\e090"}.icon-magnifier-add:before{content:"\e091"}.icon-magnifier-remove:before{content:"\e092"}.icon-paper-clip:before{content:"\e093"}.icon-paper-plane:before{content:"\e094"}.icon-power:before{content:"\e097"}.icon-refresh:before{content:"\e098"}.icon-reload:before{content:"\e099"}.icon-settings:before{content:"\e09a"}.icon-star:before{content:"\e09b"}.icon-symbol-female:before{content:"\e09c"}.icon-symbol-male:before{content:"\e09d"}.icon-target:before{content:"\e09e"}.icon-credit-card:before{content:"\e025"}.icon-paypal:before{content:"\e608"}.icon-social-tumblr:before{content:"\e00a"}.icon-social-twitter:before{content:"\e009"}.icon-social-facebook:before{content:"\e00b"}.icon-social-instagram:before{content:"\e609"}.icon-social-linkedin:before{content:"\e60a"}.icon-social-pinterest:before{content:"\e60b"}.icon-social-github:before{content:"\e60c"}.icon-social-google:before{content:"\e60d"}.icon-social-reddit:before{content:"\e60e"}.icon-social-skype:before{content:"\e60f"}.icon-social-dribbble:before{content:"\e00d"}.icon-social-behance:before{content:"\e610"}.icon-social-foursqare:before{content:"\e611"}.icon-social-soundcloud:before{content:"\e612"}.icon-social-spotify:before{content:"\e613"}.icon-social-stumbleupon:before{content:"\e614"}.icon-social-youtube:before{content:"\e008"}.icon-social-dropbox:before{content:"\e00c"}.icon-social-vkontakte:before{content:"\e618"}.icon-social-steam:before{content:"\e620"}/*# sourceMappingURL=simple-line-icons.min.css.map */ -------------------------------------------------------------------------------- /myproject/core/static/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2018 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=$,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!q(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,y=t(e.instance.popper),w=parseFloat(y['margin'+f],10),E=parseFloat(y['border'+f+'Width'],10),v=b-e.offsets.popper[m]-w-E;return v=J(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,Z(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case he.FLIP:p=[n,i];break;case he.CLOCKWISE:p=G(n);break;case he.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=$,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,y=-1!==['top','bottom'].indexOf(n),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),w&&(r=K(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,D(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!q(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=C(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /passo-a-passo.md: -------------------------------------------------------------------------------- 1 | # Passo a passo 2 | 3 | ``` 4 | git clone https://github.com/rg3915/django-auth-tutorial.git 5 | cd django-auth-tutorial 6 | git branch base origin/base 7 | git checkout base 8 | 9 | python3 -m venv .venv 10 | source .venv/bin/activate 11 | 12 | # Django==3.1.8 django-utils-six django-extensions python-decouple 13 | cat requirements.txt 14 | 15 | pip install -U pip 16 | pip install -r requirements.txt 17 | pip install ipdb 18 | 19 | python contrib/env_gen.py 20 | 21 | python manage.py migrate 22 | python manage.py createsuperuser --username="admin" --email="admin@email.com" 23 | 24 | python manage.py shell_plus 25 | ``` 26 | 27 | https://docs.djangoproject.com/en/3.1/ref/contrib/auth/#django.contrib.auth.models.UserManager.create_user 28 | 29 | 30 | ```python 31 | python manage.py shell_plus 32 | 33 | from django.contrib.auth.models import User 34 | 35 | user = User.objects.create_user( 36 | username='regis', 37 | email='regis@email.com', 38 | password='demodemo', 39 | first_name='Regis', 40 | last_name='Santos', 41 | is_active=True 42 | ) 43 | ``` 44 | 45 | ``` 46 | cat myproject/settings.py 47 | ``` 48 | 49 | 50 | ```python 51 | INSTALLED_APPS = [ 52 | 'myproject.accounts', # <--- 53 | 'django.contrib.admin', 54 | 'django.contrib.auth', 55 | ... 56 | 'django_extensions', 57 | 'widget_tweaks', 58 | 'myproject.core', 59 | ] 60 | 61 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 62 | 63 | DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', 'webmaster@localhost') 64 | EMAIL_HOST = config('EMAIL_HOST', '0.0.0.0') # localhost 65 | EMAIL_PORT = config('EMAIL_PORT', 1025, cast=int) 66 | EMAIL_HOST_USER = config('EMAIL_HOST_USER', '') 67 | EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', '') 68 | EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool) 69 | 70 | 71 | # LOGIN_URL = 'login' 72 | LOGIN_REDIRECT_URL = 'core:index' 73 | LOGOUT_REDIRECT_URL = 'core:index' 74 | ``` 75 | 76 | ## MailHog 77 | 78 | Rodar [MailHog](https://github.com/mailhog/MailHog) via Docker. 79 | 80 | ``` 81 | docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog 82 | ``` 83 | 84 | 85 | ## Estrutura do projeto 86 | 87 | ``` 88 | tree 89 | ``` 90 | 91 | ## Telas 92 | 93 | ### Login 94 | 95 | ![01_login.png](img/01_login.png) 96 | 97 | ### Cadastro 98 | 99 | ![02_signup.png](img/02_signup.png) 100 | 101 | ### Trocar senha 102 | 103 | ![03_change_password.png](img/03_change_password.png) 104 | 105 | ### Esqueci minha senha 106 | 107 | ![04_forgot_password.png](img/04_forgot_password.png) 108 | 109 | 110 | Criando alguns arquivos 111 | 112 | ``` 113 | touch myproject/core/urls.py 114 | touch myproject/accounts/urls.py 115 | ``` 116 | 117 | 118 | 119 | Editando `urls.py` 120 | 121 | ```python 122 | # urls.py 123 | from django.contrib import admin 124 | from django.urls import include, path 125 | 126 | urlpatterns = [ 127 | path('', include('myproject.core.urls', namespace='core')), 128 | # 129 | # path('accounts/', include('myproject.accounts.urls')), # sem namespace 130 | path('admin/', admin.site.urls), 131 | ] 132 | ``` 133 | 134 | Editando `core/urls.py` 135 | 136 | ``` 137 | touch myproject/core/urls.py 138 | ``` 139 | 140 | 141 | ```python 142 | # core/urls.py 143 | from django.urls import path 144 | 145 | from myproject.core import views as v 146 | 147 | app_name = 'core' 148 | 149 | 150 | urlpatterns = [ 151 | path('', v.index, name='index'), 152 | ] 153 | ``` 154 | 155 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#the-login-required-decorator 156 | 157 | Editando `core/views.py` 158 | 159 | ```python 160 | # core/views.py 161 | from django.contrib.auth.decorators import login_required 162 | from django.shortcuts import render 163 | 164 | 165 | # @login_required 166 | def index(request): 167 | template_name = 'index.html' 168 | return render(request, template_name) 169 | ``` 170 | 171 | > Rodar a aplicação. 172 | 173 | 174 | ### Login 175 | 176 | ![101_login_logout.png](img/101_login_logout.png) 177 | 178 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.LoginView 179 | 180 | https://github.com/django/django/blob/main/django/contrib/auth/views.py#L40 181 | 182 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.LogoutView 183 | 184 | Editando `accounts/urls.py` 185 | 186 | ``` 187 | touch myproject/accounts/urls.py 188 | ``` 189 | 190 | 191 | O template padrão é `registration/login.html`, mas vamos mudar 192 | 193 | ```python 194 | # accounts/urls.py 195 | from django.contrib.auth import views as auth_views 196 | from django.contrib.auth.views import LoginView, LogoutView 197 | from django.urls import path 198 | 199 | from myproject.accounts import views as v 200 | 201 | # Se usar app_name vai dar erro de redirect em PasswordResetView. 202 | # app_name = 'accounts' 203 | 204 | 205 | urlpatterns = [ 206 | path( 207 | 'login/', 208 | LoginView.as_view(template_name='accounts/login.html'), 209 | name='login' 210 | ), 211 | path('logout/', LogoutView.as_view(), name='logout'), 212 | ] 213 | ``` 214 | 215 | Em `core/views.py` descomente `@login_required`. 216 | 217 | 218 | Em `settings.py` descomente 219 | 220 | ```python 221 | LOGIN_URL = 'login' 222 | ``` 223 | 224 | Em `myproject/urls.py` descomente 225 | 226 | ``` 227 | ... 228 | path('accounts/', include('myproject.accounts.urls')), # sem namespace 229 | ... 230 | ``` 231 | 232 | 233 | Em `nav.html` corrija 234 | 235 | ```html 236 | href="{% url 'logout' %}">Logout 237 | ``` 238 | 239 | 240 | 241 | > Mostrar a aplicação rodando com login e logout. 242 | 243 | 244 | ### Cadastro 245 | 246 | ![102_signup.png](img/102_signup.png) 247 | 248 | https://simpleisbetterthancomplex.com/tutorial/2017/02/18/how-to-create-user-sign-up-view.html#basic-sign-up 249 | 250 | https://simpleisbetterthancomplex.com/tutorial/2017/02/18/how-to-create-user-sign-up-view.html#sign-up-with-confirmation-mail 251 | 252 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.PasswordResetConfirmView 253 | 254 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.PasswordResetCompleteView 255 | 256 | https://github.com/django/django/blob/main/django/contrib/auth/urls.py#L18 257 | 258 | Editando `accounts/urls.py` 259 | 260 | ```python 261 | # accounts/urls.py 262 | ... 263 | path('signup/', v.signup, name='signup'), 264 | # path('signup/', v.SignUpView.as_view(), name='signup'), 265 | path('signup-email/', v.signup_email, name='signup_email'), 266 | path( 267 | 'account-activation-done/', 268 | v.account_activation_done, 269 | name='account_activation_done' 270 | ), 271 | path( 272 | 'reset///', 273 | v.MyPasswordResetConfirm.as_view(), 274 | name='password_reset_confirm' 275 | ), 276 | path( 277 | 'reset/done/', 278 | v.MyPasswordResetComplete.as_view(), 279 | name='password_reset_complete' 280 | ), 281 | ... 282 | ``` 283 | 284 | Editando `accounts/tokens.py` 285 | 286 | https://simpleisbetterthancomplex.com/tutorial/2017/02/18/how-to-create-user-sign-up-view.html 287 | 288 | ``` 289 | touch myproject/accounts/tokens.py 290 | ``` 291 | 292 | 293 | ```python 294 | # accounts/tokens.py 295 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 296 | from django.utils import six 297 | 298 | 299 | class AccountActivationTokenGenerator(PasswordResetTokenGenerator): 300 | def _make_hash_value(self, user, timestamp): 301 | return ( 302 | six.text_type(user.pk) + six.text_type(timestamp) 303 | ) 304 | 305 | 306 | account_activation_token = AccountActivationTokenGenerator() 307 | ``` 308 | 309 | 310 | Editando `accounts/views.py` 311 | 312 | ```python 313 | # accounts/views.py 314 | from django.contrib.auth import authenticate 315 | from django.contrib.auth import login as auth_login 316 | from django.contrib.auth.views import ( 317 | PasswordChangeDoneView, 318 | PasswordChangeView, 319 | PasswordResetCompleteView, 320 | PasswordResetConfirmView, 321 | PasswordResetDoneView, 322 | PasswordResetView 323 | ) 324 | from django.contrib.sites.shortcuts import get_current_site 325 | from django.shortcuts import redirect, render 326 | from django.template.loader import render_to_string 327 | from django.urls import reverse_lazy 328 | from django.utils.encoding import force_bytes 329 | from django.utils.http import urlsafe_base64_encode 330 | from django.views.generic import CreateView 331 | 332 | from myproject.accounts.forms import SignupEmailForm, SignupForm 333 | from myproject.accounts.tokens import account_activation_token 334 | 335 | 336 | def signup(request): 337 | form = SignupForm(request.POST or None) 338 | context = {'form': form} 339 | if request.method == 'POST': 340 | if form.is_valid(): 341 | form.save() 342 | username = form.cleaned_data.get('username') 343 | raw_password = form.cleaned_data.get('password1') 344 | 345 | # Autentica usuário 346 | user = authenticate(username=username, password=raw_password) 347 | 348 | # Faz login 349 | auth_login(request, user) 350 | return redirect(reverse_lazy('core:index')) 351 | 352 | return render(request, 'accounts/signup.html', context) 353 | 354 | 355 | # Mostrar a aplicação rodando. 356 | 357 | class SignUpView(CreateView): 358 | form_class = SignupForm 359 | success_url = reverse_lazy('login') 360 | template_name = 'accounts/signup.html' 361 | 362 | 363 | # Mostrar a aplicação rodando. 364 | 365 | 366 | def send_mail_to_user(request, user): 367 | current_site = get_current_site(request) 368 | use_https = request.is_secure() 369 | subject = 'Ative sua conta.' 370 | message = render_to_string('email/account_activation_email.html', { 371 | 'user': user, 372 | 'protocol': 'https' if use_https else 'http', 373 | 'domain': current_site.domain, 374 | 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 375 | 'token': account_activation_token.make_token(user), 376 | }) 377 | user.email_user(subject, message) 378 | 379 | 380 | def signup_email(request): 381 | form = SignupEmailForm(request.POST or None) 382 | context = {'form': form} 383 | if request.method == 'POST': 384 | if form.is_valid(): 385 | user = form.save(commit=False) 386 | user.is_active = False 387 | user.save() 388 | send_mail_to_user(request, user) 389 | return redirect('account_activation_done') 390 | 391 | return render(request, 'accounts/signup_email_form.html', context) 392 | 393 | 394 | def account_activation_done(request): 395 | return render(request, 'accounts/account_activation_done.html') 396 | 397 | 398 | class MyPasswordResetConfirm(PasswordResetConfirmView): 399 | 400 | def form_valid(self, form): 401 | self.user.is_active = True 402 | self.user.save() 403 | return super(MyPasswordResetConfirm, self).form_valid(form) 404 | 405 | 406 | class MyPasswordResetComplete(PasswordResetCompleteView): 407 | ... 408 | ``` 409 | 410 | https://github.com/django/django/blob/main/django/contrib/auth/forms.py#L75 411 | 412 | Editando `accounts/forms.py` 413 | 414 | ``` 415 | touch myproject/accounts/forms.py 416 | ``` 417 | 418 | ```python 419 | # accounts/forms.py 420 | from django import forms 421 | from django.contrib.auth.forms import UserCreationForm 422 | from django.contrib.auth.models import User 423 | 424 | # from myproject.accounts.models import UserProfile 425 | 426 | 427 | class SignupForm(UserCreationForm): 428 | first_name = forms.CharField( 429 | label='Nome', 430 | max_length=30, 431 | required=False, 432 | widget=forms.TextInput(attrs={'autofocus': 'autofocus'}) 433 | ) 434 | last_name = forms.CharField(label='Sobrenome', max_length=30, required=False) # noqa E501 435 | username = forms.CharField(label='Usuário', max_length=150) 436 | email = forms.CharField( 437 | label='E-mail', 438 | max_length=254, 439 | help_text='Requerido. Informe um e-mail válido.', 440 | ) 441 | # cpf = forms.CharField(label='CPF') 442 | 443 | class Meta: 444 | model = User 445 | fields = ( 446 | 'first_name', 447 | 'last_name', 448 | 'username', 449 | 'email', 450 | 'password1', 451 | 'password2' 452 | ) 453 | 454 | 455 | class SignupEmailForm(forms.ModelForm): 456 | first_name = forms.CharField( 457 | label='Nome', 458 | max_length=30, 459 | required=False, 460 | widget=forms.TextInput(attrs={'autofocus': 'autofocus'}) 461 | ) 462 | last_name = forms.CharField(label='Sobrenome', max_length=30, required=False) # noqa E501 463 | username = forms.CharField(label='Usuário', max_length=150) 464 | email = forms.CharField( 465 | label='E-mail', 466 | max_length=254, 467 | help_text='Requerido. Informe um e-mail válido.', 468 | ) 469 | 470 | class Meta: 471 | model = User 472 | fields = ( 473 | 'first_name', 474 | 'last_name', 475 | 'username', 476 | 'email', 477 | ) 478 | ``` 479 | 480 | Arrumar link em `accounts/login.html` 481 | 482 | ```html 483 | href="{% url 'signup_email' %}">Cadastre-se 484 | ``` 485 | 486 | Dar um `find` em todos templates e trocar 487 | 488 | ```html 489 | href="{ url 'login' %}">Login 490 | ``` 491 | por 492 | 493 | ```html 494 | href="{% url 'login' %}">Login 495 | ``` 496 | 497 | Arrumar o link em `email/account_activation_email.html` 498 | 499 | ```html 500 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 501 | ``` 502 | 503 | Arrumar o link em `registration/password_reset_complete.html` 504 | 505 | ```html 506 | href="{% url 'login' %} 507 | ``` 508 | 509 | Renomear a pasta `registration` para `temp`. 510 | 511 | ``` 512 | mv myproject/accounts/templates/registration myproject/accounts/templates/temp 513 | ``` 514 | 515 | 516 | > Mostrar a aplicação rodando com **cadastro normal** e **cadastro com senha.** 517 | 518 | Renomear a pasta `temp` para `registration`. 519 | 520 | ``` 521 | mv myproject/accounts/templates/temp myproject/accounts/templates/registration 522 | ``` 523 | 524 | 525 | ### Trocar senha 526 | 527 | ![103_change_password.png](img/103_change_password.png) 528 | 529 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.PasswordChangeView 530 | 531 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.PasswordChangeDoneView 532 | 533 | https://github.com/django/django/blob/main/django/contrib/auth/views.py#L334 534 | 535 | https://github.com/django/django/blob/main/django/views/generic/base.py#L157 536 | 537 | Em `accounts/views.py` 538 | 539 | ```python 540 | # accounts/views.py 541 | class MyPasswordChange(PasswordChangeView): 542 | ... 543 | 544 | 545 | class MyPasswordChangeDone(PasswordChangeDoneView): 546 | 547 | def get(self, request, *args, **kwargs): 548 | return redirect(reverse_lazy('login')) 549 | ``` 550 | 551 | Em `accounts/urls.py` 552 | 553 | ```python 554 | # accounts/urls.py 555 | ... 556 | path( 557 | 'password_change/', 558 | v.MyPasswordChange.as_view(), 559 | name='password_change' 560 | ), 561 | path( 562 | 'password_change/done/', 563 | v.MyPasswordChangeDone.as_view(), 564 | name='password_change_done' 565 | ), 566 | ... 567 | ``` 568 | 569 | > Mostrar a aplicação rodando com a **troca de senha**. 570 | 571 | Arrumar o link em `nav.html` 572 | 573 | ```html 574 | Trocar a senha 575 | ``` 576 | 577 | ### Esqueci minha senha 578 | 579 | ![104_reset_password.png](img/104_reset_password.png) 580 | 581 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.PasswordResetView 582 | 583 | https://github.com/django/django/blob/main/django/contrib/auth/views.py#L212 584 | 585 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.views.PasswordResetDoneView 586 | 587 | https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.forms.PasswordResetForm 588 | 589 | https://github.com/django/django/blob/main/django/contrib/auth/forms.py#L238 590 | 591 | https://github.com/django/django/blob/main/django/contrib/auth/urls.py 592 | 593 | 594 | Em `accounts/views.py` 595 | 596 | ```python 597 | # accounts/views.py 598 | 599 | # Requer 600 | # registration/password_reset_email.html 601 | # registration/password_reset_subject.txt 602 | class MyPasswordReset(PasswordResetView): 603 | ... 604 | 605 | 606 | class MyPasswordResetDone(PasswordResetDoneView): 607 | ... 608 | 609 | 610 | class MyPasswordResetConfirm(PasswordResetConfirmView): 611 | 612 | def form_valid(self, form): 613 | self.user.is_active = True 614 | self.user.save() 615 | return super(MyPasswordResetConfirm, self).form_valid(form) 616 | 617 | 618 | class MyPasswordResetComplete(PasswordResetCompleteView): 619 | ... 620 | 621 | ``` 622 | 623 | Em `accounts/urls.py` 624 | 625 | ```python 626 | # accounts/urls.py 627 | ... 628 | path( 629 | 'password_reset/', 630 | v.MyPasswordReset.as_view(), 631 | name='password_reset' 632 | ), 633 | path( 634 | 'password_reset/done/', 635 | v.MyPasswordResetDone.as_view(), 636 | name='password_reset_done' 637 | ), 638 | path( 639 | 'reset///', 640 | v.MyPasswordResetConfirm.as_view(), 641 | name='password_reset_confirm' 642 | ), 643 | path( 644 | 'reset/done/', 645 | v.MyPasswordResetComplete.as_view(), 646 | name='password_reset_complete' 647 | ), 648 | ... 649 | ``` 650 | 651 | Arrumar o link em `login.html` 652 | 653 | ```html 654 | Esqueci minha senha 655 | ``` 656 | 657 | Arrumar o link em `registration/password_reset_email.html` 658 | 659 | ```html 660 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 661 | ``` 662 | 663 | --- 664 | 665 | Falar de `include('django.contrib.auth.urls')` include em `urls.py` 666 | 667 | https://github.com/django/django/blob/main/django/contrib/auth/urls.py 668 | 669 | ```python 670 | # urls.py 671 | from django.contrib import admin 672 | from django.urls import include, path 673 | 674 | urlpatterns = [ 675 | # path('', include('django.contrib.auth.urls')), # sem namespace 676 | ] 677 | ``` 678 | 679 | --- 680 | 681 | ## Perfil 682 | 683 | ```python 684 | # accounts/models.py 685 | from django.contrib.auth.models import User 686 | from django.db import models 687 | from django.db.models.signals import post_save 688 | from django.dispatch import receiver 689 | 690 | 691 | class Profile(models.Model): 692 | cpf = models.CharField('CPF', max_length=11, unique=True, null=True, blank=True) # noqa E501 693 | rg = models.CharField('RG', max_length=20, null=True, blank=True) 694 | user = models.OneToOneField(User, on_delete=models.CASCADE) 695 | 696 | class Meta: 697 | ordering = ('cpf',) 698 | verbose_name = 'perfil' 699 | verbose_name_plural = 'perfis' 700 | 701 | def __str__(self): 702 | if self.cpf: 703 | return self.cpf 704 | return self.user.username 705 | 706 | 707 | @receiver(post_save, sender=User) 708 | def update_user_profile(sender, instance, created, **kwargs): 709 | if created: 710 | Profile.objects.create(user=instance) 711 | instance.profile.save() 712 | 713 | ``` 714 | 715 | 716 | ```python 717 | # accounts/forms.py 718 | from django import forms 719 | from django.contrib.auth.forms import UserCreationForm 720 | from django.contrib.auth.models import User 721 | 722 | 723 | class SignupForm(UserCreationForm): 724 | ... 725 | cpf = forms.CharField(label='CPF') 726 | rg = forms.CharField(label='RG', required=False) 727 | 728 | class Meta: 729 | model = User 730 | fields = ( 731 | 'first_name', 732 | 'last_name', 733 | 'cpf', 734 | 'rg', 735 | 'username', 736 | 'email', 737 | 'password1', 738 | 'password2' 739 | ) 740 | 741 | 742 | class SignupEmailForm(forms.ModelForm): 743 | ... 744 | cpf = forms.CharField(label='CPF') 745 | rg = forms.CharField(label='RG', required=False) 746 | 747 | class Meta: 748 | model = User 749 | fields = ( 750 | 'first_name', 751 | 'last_name', 752 | 'cpf', 753 | 'rg', 754 | 'username', 755 | 'email', 756 | ) 757 | ``` 758 | 759 | ```python 760 | # accounts/admin.py 761 | from django.contrib import admin 762 | 763 | from .models import Profile 764 | 765 | 766 | @admin.register(Profile) 767 | class ProfileAdmin(admin.ModelAdmin): 768 | list_display = ('user', 'cpf', 'rg') 769 | ``` 770 | 771 | ```python 772 | # accounts/views.py 773 | def signup(request): 774 | ... 775 | if form.is_valid(): 776 | user = form.save(commit=False) 777 | user.is_active = True 778 | user.save() # precisa salvar para rodar o signal. 779 | # carrega a instância do perfil criada pelo signal. 780 | user.refresh_from_db() 781 | user.profile.cpf = form.cleaned_data.get('cpf') 782 | user.profile.rg = form.cleaned_data.get('rg') 783 | user.save() 784 | 785 | username = form.cleaned_data.get('username') 786 | raw_password = form.cleaned_data.get('password1') 787 | 788 | # Autentica usuário 789 | user_auth = authenticate(username=username, password=raw_password) 790 | 791 | # Faz login 792 | auth_login(request, user_auth) 793 | return redirect(reverse_lazy('core:index')) 794 | 795 | return render(request, 'accounts/signup.html', context) 796 | 797 | 798 | def signup_email(request): 799 | ... 800 | if form.is_valid(): 801 | user = form.save(commit=False) 802 | user.is_active = False 803 | user.save() # precisa salvar para rodar o signal. 804 | # carrega a instância do perfil criada pelo signal. 805 | user.refresh_from_db() 806 | user.profile.cpf = form.cleaned_data.get('cpf') 807 | user.profile.rg = form.cleaned_data.get('rg') 808 | user.save() 809 | send_mail_to_user(request, user) 810 | return redirect('account_activation_done') 811 | 812 | return render(request, 'accounts/signup_email_form.html', context) 813 | 814 | ``` 815 | 816 | Corrigindo o erro `RelatedObjectDoesNotExist at /admin/login/ User has no profile` 817 | 818 | ``` 819 | user = User.objects.get(username='admin') 820 | Profile.objects.create(user=user) 821 | ``` 822 | -------------------------------------------------------------------------------- /myproject/core/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,h){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)P(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!(Ie={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"}),selector:!(Se={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)"}),placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},we="out",Ne={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:(De="show")+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Oe="fade",ke="show",Pe=".tooltip-inner",je=".arrow",He="hover",Le="focus",Re="click",xe="manual",We=function(){function i(t,e){if("undefined"==typeof h)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=pe(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(pe(this.getTipElement()).hasClass(ke))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),pe.removeData(this.element,this.constructor.DATA_KEY),pe(this.element).off(this.constructor.EVENT_KEY),pe(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&pe(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===pe(this.element).css("display"))throw new Error("Please use show on visible elements");var t=pe.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){pe(this.element).trigger(t);var n=pe.contains(this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!n)return;var i=this.getTipElement(),r=Fn.getUID(this.constructor.NAME);i.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&pe(i).addClass(Oe);var o="function"==typeof this.config.placement?this.config.placement.call(this,i,this.element):this.config.placement,s=this._getAttachment(o);this.addAttachmentClass(s);var a=!1===this.config.container?document.body:pe(document).find(this.config.container);pe(i).data(this.constructor.DATA_KEY,this),pe.contains(this.element.ownerDocument.documentElement,this.tip)||pe(i).appendTo(a),pe(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new h(this.element,i,{placement:s,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:je},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),pe(i).addClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().on("mouseover",null,pe.noop);var l=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,pe(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(pe(this.tip).hasClass(Oe)){var c=Fn.getTransitionDurationFromElement(this.tip);pe(this.tip).one(Fn.TRANSITION_END,l).emulateTransitionEnd(c)}else l()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=pe.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),pe(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(pe(this.element).trigger(i),!i.isDefaultPrevented()){if(pe(n).removeClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().off("mouseover",null,pe.noop),this._activeTrigger[Re]=!1,this._activeTrigger[Le]=!1,this._activeTrigger[He]=!1,pe(this.tip).hasClass(Oe)){var o=Fn.getTransitionDurationFromElement(n);pe(n).one(Fn.TRANSITION_END,r).emulateTransitionEnd(o)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){pe(this.getTipElement()).addClass(Te+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||pe(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(pe(t.querySelectorAll(Pe)),this.getTitle()),pe(t).removeClass(Oe+" "+ke)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?pe(e).parent().is(t)||t.empty().append(e):t.text(pe(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getAttachment=function(t){return Ie[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)pe(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==xe){var e=t===He?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===He?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;pe(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}pe(i.element).closest(".modal").on("hide.bs.modal",function(){return i.hide()})}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Le:He]=!0),pe(e.getTipElement()).hasClass(ke)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Le:He]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,pe(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),Fn.typeCheckConfig(ve,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=pe(this.getTipElement()),e=t.attr("class").match(be);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(pe(t).removeClass(Oe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=pe(this).data(ye),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),pe(this).data(ye,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}},{key:"Default",get:function(){return Ae}},{key:"NAME",get:function(){return ve}},{key:"DATA_KEY",get:function(){return ye}},{key:"Event",get:function(){return Ne}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}(),pe.fn[ve]=We._jQueryInterface,pe.fn[ve].Constructor=We,pe.fn[ve].noConflict=function(){return pe.fn[ve]=Ce,We._jQueryInterface},We),Jn=(qe="popover",Ke="."+(Fe="bs.popover"),Me=(Ue=e).fn[qe],Qe="bs-popover",Be=new RegExp("(^|\\s)"+Qe+"\\S+","g"),Ve=l({},zn.Default,{placement:"right",trigger:"click",content:"",template:''}),Ye=l({},zn.DefaultType,{content:"(string|element|function)"}),ze="fade",Ze=".popover-header",Ge=".popover-body",$e={HIDE:"hide"+Ke,HIDDEN:"hidden"+Ke,SHOW:(Je="show")+Ke,SHOWN:"shown"+Ke,INSERTED:"inserted"+Ke,CLICK:"click"+Ke,FOCUSIN:"focusin"+Ke,FOCUSOUT:"focusout"+Ke,MOUSEENTER:"mouseenter"+Ke,MOUSELEAVE:"mouseleave"+Ke},Xe=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){Ue(this.getTipElement()).addClass(Qe+"-"+t)},r.getTipElement=function(){return this.tip=this.tip||Ue(this.config.template)[0],this.tip},r.setContent=function(){var t=Ue(this.getTipElement());this.setElementContent(t.find(Ze),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Ge),e),t.removeClass(ze+" "+Je)},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=Ue(this.getTipElement()),e=t.attr("class").match(Be);null!==e&&0=this._offsets[r]&&("undefined"==typeof this._offsets[r+1]||t