Aula 90 - Tratamento e Encerramento de Sessões de Usuário
O que faremos agora é colocar tudo o que aprendemos com o
Object Viewed em uma sessão de usuário.
Vamos criar o modelo de sessão do usuário e um sinal(
signal), importar algumas coisas, etc.
Antes de iniciar as modificações,
crie uma
branch para trabalhar as mudanças da aula e abra o
models do
analytics.
No código abaixo temos as Importações
django_ecommerce/e_commerce/analytics/models.py
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save, post_save
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from accounts.signals import user_logged_in
from .signals import object_viewed_signal
from .utils import get_client_ip
User = settings.AUTH_USER_MODEL
class ObjectViewed(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # Product, Order, Cart, Address...
object_id = models.PositiveIntegerField() # User id, Product id, Order id
content_object = GenericForeignKey('content_type', 'object_id')
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.content_object} viewed on {self.timestamp}"
class Meta:
ordering = ['-timestamp'] # most recent saved show up first
verbose_name = 'Object viewed'
verbose_name_plural = 'Objects viewed'
def object_viewed_receiver(sender, instance, request, *args, **kwargs):
c_type = ContentType.objects.get_for_model(sender) # instance.__class__
new_view_obj = ObjectViewed.objects.create(
user = request.user,
content_type = c_type,
object_id = instance.id,
ip_address = get_client_ip(request)
)
object_viewed_signal.connect(object_viewed_receiver)
class UserSession(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
session_key = models.CharField(max_length=100, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
ended = models.BooleanField(default=False)
def user_logged_in_receiver(sender, instance, request, *args, **kwargs):
user = instance
ip_address = get_client_ip(request)
session_key = request.session.session_key
UserSession.objects.create(
user = user,
ip_address = ip_address,
session_key = session_key
)
user_logged_in.connect(user_logged_in_receiver)
Faça as Migration:
python manage.py makemigrations
python manage.py migrate
A próxima coisa a fazer é criar um
signal para
login do usuário.
Então vamos criar o
signals.py na pasta
accounts.
django_ecommerce/e_commerce/accounts/signals.py
from django.dispatch import Signal
user_logged_in = Signal()
Importe ele no
view.py do
accounts.
A linha
from .signal import user_logged_in importa o sinal
user_logged_in definido no
signals do
accounts.
Neste caso, o sinal
user_logged_in é destinado a ser emitido sempre que um usuário faz login com sucesso, permitindo que outras partes do aplicativo reajam a esse evento.
A expressão
user_logged_in.send(sender=user.__class__, request=request, user=user) é onde o sinal
user_logged_in é efetivamente emitido, ou "enviado".
Vamos quebrar essa linha para entender melhor:
sender: O remetente do sinal, que é a classe do objeto user (user.__class__). Isso informa aos receptores do sinal qual classe está enviando o sinal.
instance: O objeto user que foi autenticado e fez login.
request: O objeto de solicitação (self.request), que contém informações sobre a solicitação HTTP atual, como parâmetros GET, POST e outros metadados.
Em resumo, essa linha está emitindo um sinal indicando que um usuário fez login com sucesso. Ele envia informações sobre o usuário que fez login e a solicitação HTTP associada ao login.
Os receptores desse sinal podem então executar ações com base nessas informações, como registrar o evento de login, atualizar dados do usuário, iniciar uma sessão, entre outras possibilidades.
Essencialmente, essa linha de código permite que você execute lógica adicional em outras partes do seu aplicativo em resposta a um usuário fazendo login, sem ter que acoplar firmemente essa lógica à sua view de login.
Por exemplo, você pode ter um receiver que registra a hora do login, um que verifica se o perfil do usuário está completo, ou qualquer outra ação que você queira realizar automaticamente quando um usuário faz login.
django_ecommerce/e_commerce/accounts/views.py
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.views.generic import CreateView, FormView, View
from django.http import HttpResponse
from django.shortcuts import render,redirect
from django.utils.http import url_has_allowed_host_and_scheme
from .forms import LoginForm, RegisterForm, GuestForm
from .models import GuestEmail
from .signals import user_logged_in
def guest_register_view(request):
form = GuestForm(request.POST or None)
context = {
"form": form
}
next_ = request.GET.get('next')
next_post = request.POST.get('next')
redirect_path = next_ or next_post or None
if form.is_valid():
email = form.cleaned_data.get("email")
new_guest_email = GuestEmail.objects.create(email=email)
request.session['guest_email_id'] = new_guest_email.id
if url_has_allowed_host_and_scheme(redirect_path, request.get_host()):
return redirect(redirect_path)
else:
return redirect("/register/")
return redirect("/register/")
class LoginView(FormView):
form_class = LoginForm
success_url = '/' # Redireciona para a raiz do projeto
template_name = 'accounts/login.html'
def form_valid(self, form):
email = form.cleaned_data.get("email")
password = form.cleaned_data.get("password")
user = authenticate(request=self.request, username=email, password=password)
if user is not None:
login(self.request, user)
user_logged_in.send(sender=user.__class__, instance=user, request=self.request)
try:
del self.request.session['guest_email_id']
except:
pass
return super(LoginView, self).form_valid(form)
# def login_page(request):
# form = LoginForm(request.POST or None)
# context = {
# "form": form
# }
# next_ = request.GET.get('next')
# next_post = request.POST.get('next')
# redirect_path = next_ or next_post or None
# if form.is_valid():
# username = form.cleaned_data.get("username")
# password = form.cleaned_data.get("password")
# user = authenticate(request, username=username, password=password)
# if user is not None:
# login(request, user)
# try:
# del request.session['guest_email_id']
# except:
# pass
# if url_has_allowed_host_and_scheme( redirect_path, request.get_host() ):
# return redirect( redirect_path )
# else:
# # Redireciona para uma página de sucesso.
# return redirect("/")
# else:
# #Retorna uma mensagem de erro de 'invalid login'.
# print("Login inválido")
# return render(request, "accounts/login.html", context)
class LogoutView(View):
template_name = 'accounts/logout.html'
def get(self, request, *args, **kwargs):
context = {
"content": "Você efetuou o logout com sucesso! :)"
}
logout(request)
return render(request, self.template_name, context)
# def logout_page(request):
# context = {
# "content": "Você efetuou o logout com sucesso! :)"
# }
# logout(request)
# return render(request, "accounts/logout.html", context)
class RegisterView(CreateView):
form_class = RegisterForm
template_name = 'accounts/register.html'
success_url = '/login/'
# User = get_user_model()
# def register_page(request):
# form = RegisterForm(request.POST or None)
# context = {
# "form": form
# }
# if form.is_valid():
# form.save()
# return render(request, "accounts/register.html", context)
Precisamos de um método para encerrar a sessão depois de salvar no banco.
django_ecommerce/e_commerce/analytics/admin.py
from django.contrib import admin
from .models import ObjectViewed
from .models import UserSession
admin.site.register(ObjectViewed)
admin.site.register(UserSession)
A linha
admin.site.register(UserSession) no arquivo
admin.py do seu aplicativo Django está registrando o modelo
UserSession no site de administração do Django.
Isso significa que o Django irá gerar automaticamente uma interface no painel de administração para que você possa visualizar, adicionar, editar e deletar instâncias do modelo
UserSession.
Certifique-se que o projeto esteja rodando e faça o login no painel admin do django e na parte de
Analytics clique em
UserSession e em
UserSessionObject.
localhost:8000/admin
Veja que você tem uma
session key no
UserSessionObject.
Vamos explorar o session no shell do django.
Abra outro terminal para poder deixar o servidor rodando e digite:
python manage.py shell
Copie o número da
session key do
UserSessionObject e atribua a
session_key.
>>>session_key = 'qus0xwjinskfitjdn8dhrnhcwhu9f2ds'
Importe o Session
>>>from django.contrib.sessions.models import Session
>>>Session.objects.get(pk=session_key )
>>>Session.objects.get(pk=session_key ).delete()
Agora dê um refresh na página e veja que você foi deslogado.
Essa é a forma como django gerencia as sessões, como ele cria e deleta as sessões.
O
Django usa uma sessão consistente entre o
frontend e o
backend.
Isso significa que, se o mesmo usuário estiver logando no
admin e na
loja com as mesmas credenciais e a sessão ainda estiver ativa, ele permanecerá logado em ambos os lugares.
A sincronização de sessão entre o
admin e o
frontend acontece naturalmente com o
Django, porque o
Django usa um sistema de sessão centralizado.
Quando um usuário faz login em qualquer parte do site, seja no admin ou no frontend, o
Django cria uma sessão para esse usuário, que é válida em todo o site, a menos que você tenha configurado algo para separar explicitamente as sessões entre o
admin e o
frontend.
Voltando ao código
O código em
azul abaixo, é a lógica para
encerrar sessões de usuário no Django, juntamente com a definição de
receivers para os sinais
post_save que são conectados aos modelos
UserSession e
User.
Vamos detalhar cada parte:
FORCE_SESSION_TO_ONE e FORCE_INACTIVE_USER_ENDSESSION
Essas duas linhas de código estão obtendo valores de configuração do arquivo
settings.py do projeto Django, com opções de
fallback caso essas configurações não estejam explicitamente definidas.
FORCE_SESSION_TO_ONE = getattr(settings, 'FORCE_SESSION_TO_ONE', False): Esta linha busca a configuração
FORCE_SESSION_TO_ONE dentro do objeto settings do Django. O método
getattr é usado para tentar obter o valor dessa configuração, caso não esteja definido em
settings.py, o valor padrão
False é utilizado. Se
FORCE_SESSION_TO_ONE for
True, indica que o sistema deve forçar apenas uma sessão ativa por usuário a qualquer momento, encerrando automaticamente sessões anteriores quando uma nova sessão é iniciada.
FORCE_INACTIVE_USER_ENDSESSION = getattr(settings, 'FORCE_INACTIVE_USER_ENDSESSION', False): De maneira semelhante, esta linha busca a configuração
FORCE_INACTIVE_USER_ENDSESSION. Se for
True, o sistema encerra automaticamente todas as sessões ativas de um usuário quando sua conta é marcada como inativa (
is_active = False).
Essas configurações permitem controlar o comportamento das sessões de usuário no seu projeto Django de maneira flexível.
Você pode habilitá-las adicionando as seguintes linhas ao seu arquivo
settings.py:
FORCE_SESSION_TO_ONE = True # Força apenas uma sessão por usuário
FORCE_INACTIVE_USER_ENDSESSION = True
No
settings.py vamos colocar como False ambas as opções, o código já tá controlando isso.
Método end_session()
O método
end_session é um método de instância, o que significa que ele é chamado em um objeto específico de
UserSession. Vamos passar por cada linha:
session_key = self.session_key: Isso pega a chave da sessão (um identificador único para cada sessão) do objeto UserSession atual.
try:: Este bloco é usado para "tentar" executar um código que pode potencialmente falhar, ou seja, pode lançar uma exceção.
Session.objects.get(pk=session_key).delete(): Aqui, tentamos encontrar uma sessão do Django (não uma UserSession, mas a sessão real usada pelo Django para manter o estado de login) que tenha a chave primária (pk) igual à session_key do nosso UserSession. Se encontrada, a sessão é deletada, o que efetivamente desloga o usuário do ponto de vista do Django.
except Session.DoesNotExist:: Se a sessão com a session_key dada não existir, o Django lançará uma Session.DoesNotExist exceção. Neste caso, usamos pass para simplesmente ignorar esse erro e continuar. Isso significa que se a sessão do Django já foi excluída, não faremos nada.
except Exception as e:: Se ocorrer qualquer outro tipo de exceção ao tentar excluir a sessão, capturamos essa exceção e a imprimimos no console. Isso é para fins de depuração e deve ser manuseado com cuidado em um ambiente de produção, pois a impressão de erros pode não ser a melhor maneira de lidar com exceções inesperadas.
self.active = False: Independente de a sessão do Django existir ou não, definimos o campo active do nosso objeto UserSession como False, indicando que esta UserSession não deve mais ser considerada ativa.
self.ended = True: Também definimos o campo ended como True, indicando que esta sessão de usuário foi encerrada.
self.save(): Finalmente, salvamos o objeto UserSession com as atualizações que fizemos nos campos active e ended.
Método save()
O método
save é o método padrão do Django que é chamado quando você salva um objeto no banco de dados.
if not self.active and not self.ended:: Antes de salvar o objeto, verificamos se o campo active é False e o campo ended é False. Se ambos forem False, chamamos o método end_session que acabamos de descrever. Isso pode ser usado para garantir que, se por algum motivo estivermos salvando uma sessão que deveria ser encerrada, encerramos corretamente.
super().save(*args, **kwargs): Depois de lidar com o estado da sessão, chamamos o método save da superclasse (no caso, o save padrão do Django para modelos) para continuar o processo normal de salvar o objeto no banco de dados. Os *args e **kwargs são argumentos e argumentos nomeados que podem ser passados para o método save padrão do Django e são aqui repassados.
django_ecommerce/e_commerce/analytics/models.py
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save, post_save
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from accounts.signals import user_logged_in
from .signals import object_viewed_signal
from .utils import get_client_ip
User = settings.AUTH_USER_MODEL
FORCE_SESSION_TO_ONE = getattr(settings, 'FORCE_SESSION_TO_ONE', False)
FORCE_INACTIVE_USER_ENDSESSION= getattr(settings, 'FORCE_INACTIVE_USER_ENDSESSION', False)
class ObjectViewed(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # Product, Order, Cart, Address...
object_id = models.PositiveIntegerField() # User id, Product id, Order id
content_object = GenericForeignKey('content_type', 'object_id')
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.content_object} viewed on {self.timestamp}"
class Meta:
ordering = ['-timestamp'] # most recent saved show up first
verbose_name = 'Object viewed'
verbose_name_plural = 'Objects viewed'
def object_viewed_receiver(sender, instance, request, *args, **kwargs):
c_type = ContentType.objects.get_for_model(sender) # instance.__class__
new_view_obj = ObjectViewed.objects.create(
user = request.user,
content_type = c_type,
object_id = instance.id,
ip_address = get_client_ip(request)
)
object_viewed_signal.connect(object_viewed_receiver)
class UserSession(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
session_key = models.CharField(max_length=100, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
ended = models.BooleanField(default=False)
def end_session(self):
session_key = self.session_key
try:
Session.objects.get(pk=session_key).delete()
except Session.DoesNotExist:
pass
except Exception as e:
print(f"Unexpected error ending session: {e}")
self.active = False
self.ended = True
self.save()
def save(self, *args, **kwargs):
# Se 'active' for False e 'ended' ainda não for True, encerra a sessão
if not self.active and not self.ended:
self.end_session()
super().save(*args, **kwargs)
def post_save_session_receiver(sender, instance, created, *args, **kwargs):
if created:
qs = UserSession.objects.filter(user=instance.user, ended=False, active=False).exclude(id=instance.id)
for i in qs:
i.end_session()
if not instance.active and not instance.ended:
instance.end_session()
if FORCE_SESSION_TO_ONE:
post_save.connect(post_save_session_receiver, sender=UserSession)
def post_save_user_changed_receiver(sender, instance, created, *args, **kwargs):
if not created:
if instance.is_active == False:
qs = UserSession.objects.filter(user=instance.user, ended=False, active=False)
for i in qs:
i.end_session()
if FORCE_INACTIVE_USER_ENDSESSION:
post_save.connect(post_save_user_changed_receiver, sender=User)
def user_logged_in_receiver(sender, instance, request, *args, **kwargs):
user = instance
ip_address = get_client_ip(request)
session_key = request.session.session_key
UserSession.objects.create(
user=user,
ip_address=ip_address,
session_key=session_key
)
user_logged_in.connect(user_logged_in_receiver)
django_ecommerce/e_commerce/e_commerce/settings.py
"""
Django settings for e_commerce project.
Generated by 'django-admin startproject' using Django 2.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xjmv-0^l__duq4-xp54m94bsf02lx4&1xka_ykd_(7(5#9^1o^'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#our apps
'addresses',
'analytics',
'billing',
'accounts',
'carts',
'orders',
'products',
'search',
'tags',
]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = 'accounts.User' # changes the built-in user model to ours
FORCE_SESSION_TO_ONE = False
FORCE_INACTIVE_USER_ENDSESSION= False
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
LOGOUT_REDIRECT_URL = '/login/'
ROOT_URLCONF = 'e_commerce.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'e_commerce.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static_local")
]
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "static_root")
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "media_root")
Deixar essas configurações como
False no
settings.py é uma abordagem deliberada que oferece várias vantagens.
Ao definir
FORCE_SESSION_TO_ONE e
FORCE_INACTIVE_USER_ENDSESSION como
False por padrão, o desenvolvedor ou administrador do sistema tem a flexibilidade para ativar essas funcionalidades apenas se e quando necessário. Isso permite uma personalização mais granular do comportamento do aplicativo sem modificar o código fonte, apenas alterando a configuração no
settings.py.
Deixar
FORCE_SESSION_TO_ONE e
FORCE_INACTIVE_USER_ENDSESSION como
False no
settings.py é uma prática que permite uma maior flexibilidade e minimiza surpresas para os desenvolvedores e administradores do sistema.
Essa abordagem reflete uma consideração cuidadosa dos princípios de design de software, onde mudanças significativas no comportamento do aplicativo devem ser opcionais, em vez de impostas por padrão.
Vamos testar
Queremos permitir multiplas sessões de usuário, se a gente quiser apenas uma sessão por usuário realmente mais a frente, será através do código que iremos controlar isso, e não configurando no
settings.py.
Abra o ecommerce:
localhost:8000/ e faça o login no ecommerce.
Abra o painel admin:
localhost:8000/admin e faça o login também.
Abra uma janela anônima e tente fazer o login com o mesmo usuário.
Veja que ele agora não vai desconectar o login em uma das duas janelas, ele vai permitir mais de uma sessão aberta por usuário.
Agora desmarque o active da última sessão criada, e dê um refresh onde você logou por último, veja que agora você foi deslogado.
Por essa aula é só, na próxima a gente vai fazer o handle do sinal emitido pelos objetos visualizados.
Código final da aula:
Canais do Youtube
Dêem um joinha 👍 na página do Código Fluente no
Facebook.
Sigam o Código Fluente no Instagram e no TikTok.
Código Fluente no Pinterest.
Meus links de afiliados:
Nos vemos na próxima então, \o/ 😉 Bons Estudos!