212 lines
8.3 KiB
Python
212 lines
8.3 KiB
Python
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib.auth.forms import (
|
|
AuthenticationForm,
|
|
PasswordChangeForm,
|
|
PasswordResetForm,
|
|
SetPasswordForm,
|
|
UserCreationForm,
|
|
)
|
|
from django.core.mail import send_mail
|
|
from django.template.loader import render_to_string
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
# Los formularios de auth de Django no traen ninguna clase CSS en sus
|
|
# widgets (piensan en HTML sin estilar): se añade aquí la clase de daisyUI
|
|
# para que login/registro (ver registration/*.html) tengan la misma pinta
|
|
# que el resto de inputs del sitio, sin depender de un paquete aparte
|
|
# (django-widget-tweaks) solo para dos formularios de tres campos.
|
|
INPUT_CLASSES = 'input input-bordered w-full'
|
|
|
|
|
|
def _send_account_notice(subject_template, text_template, html_template, context, to_email):
|
|
# Compartido por los avisos de seguridad de la cuenta (cambio de email,
|
|
# cambio de contraseña): mismo esqueleto de email (texto plano +
|
|
# alternativa HTML de crochet/email/base.html), solo cambian las
|
|
# plantillas y el destinatario.
|
|
subject = ''.join(render_to_string(subject_template, context).splitlines())
|
|
send_mail(
|
|
subject=subject,
|
|
message=render_to_string(text_template, context),
|
|
from_email=None,
|
|
recipient_list=[to_email],
|
|
html_message=render_to_string(html_template, context),
|
|
)
|
|
|
|
|
|
class StyledAuthenticationForm(AuthenticationForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field in self.fields.values():
|
|
field.widget.attrs['class'] = INPUT_CLASSES
|
|
|
|
|
|
class StyledUserCreationForm(UserCreationForm):
|
|
# UserCreationForm no pide email por defecto (piensa solo en
|
|
# usuario/contraseña); sin él, PasswordResetForm no tiene a qué
|
|
# dirección mandar el enlace de recuperación de estos usuarios.
|
|
email = forms.EmailField(label=_('Email'))
|
|
|
|
class Meta(UserCreationForm.Meta):
|
|
fields = ('username', 'email')
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field in self.fields.values():
|
|
field.widget.attrs['class'] = INPUT_CLASSES
|
|
|
|
|
|
class StyledPasswordChangeForm(PasswordChangeForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field in self.fields.values():
|
|
field.widget.attrs['class'] = INPUT_CLASSES
|
|
|
|
def save(self, commit=True):
|
|
user = super().save(commit=commit)
|
|
# A diferencia del cambio de email, aquí no hay una dirección
|
|
# "anterior" distinta: el aviso va al propio email de la cuenta.
|
|
# Sin email no hay a quién avisar (cuentas antiguas sin uno).
|
|
if user.email:
|
|
context = {'user': user, 'site_name': settings.SITE_NAME, 'support_email': settings.SUPPORT_EMAIL}
|
|
_send_account_notice(
|
|
'crochet/email/password_changed_subject.txt',
|
|
'crochet/email/password_changed_notice.txt',
|
|
'crochet/email/password_changed_notice.html',
|
|
context, user.email,
|
|
)
|
|
return user
|
|
|
|
|
|
class StyledPasswordResetForm(PasswordResetForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field in self.fields.values():
|
|
field.widget.attrs['class'] = INPUT_CLASSES
|
|
|
|
|
|
class StyledSetPasswordForm(SetPasswordForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field in self.fields.values():
|
|
field.widget.attrs['class'] = INPUT_CLASSES
|
|
|
|
|
|
class EmailUpdateForm(forms.Form):
|
|
email = forms.EmailField(label=_('Email'), widget=forms.EmailInput(attrs={'class': INPUT_CLASSES}))
|
|
|
|
def __init__(self, *args, user, **kwargs):
|
|
self.user = user
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def save(self):
|
|
old_email = self.user.email
|
|
self.user.email = self.cleaned_data['email']
|
|
self.user.save(update_fields=['email'])
|
|
# Aviso a la dirección ANTERIOR (no a la nueva): así, si alguien más
|
|
# ha cambiado el email de la cuenta (sesión robada, dispositivo
|
|
# compartido...), quien de verdad era el dueño se entera por donde
|
|
# todavía puede leerlo. Si no había email antes, o si "cambiar" no
|
|
# ha cambiado nada, no hay a quién avisar ni de qué.
|
|
if old_email and old_email != self.user.email:
|
|
context = {
|
|
'user': self.user, 'old_email': old_email,
|
|
'site_name': settings.SITE_NAME, 'support_email': settings.SUPPORT_EMAIL,
|
|
}
|
|
_send_account_notice(
|
|
'crochet/email/email_changed_subject.txt',
|
|
'crochet/email/email_changed_notice.txt',
|
|
'crochet/email/email_changed_notice.html',
|
|
context, old_email,
|
|
)
|
|
|
|
|
|
class DisplayNameForm(forms.Form):
|
|
# Se guarda en el propio first_name de auth.User (no se usa para nada
|
|
# más en la app): no hace falta un modelo de perfil aparte solo para un
|
|
# campo de texto opcional. Se propone como autor por defecto al crear
|
|
# un patrón nuevo (ver PatternCreateView) y se muestra en el navbar/
|
|
# saludo de "Mis patrones" en vez del username, si está puesto.
|
|
display_name = forms.CharField(
|
|
label=_('Nombre para mostrar'), required=False, max_length=150,
|
|
widget=forms.TextInput(attrs={'class': INPUT_CLASSES}),
|
|
)
|
|
|
|
def __init__(self, *args, user, **kwargs):
|
|
self.user = user
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def save(self):
|
|
self.user.first_name = self.cleaned_data['display_name']
|
|
self.user.save(update_fields=['first_name'])
|
|
|
|
|
|
class TwoFactorTokenForm(forms.Form):
|
|
# Un único campo de texto tanto para el código de 6 dígitos de la app
|
|
# (TOTP) como para un código de respaldo (StaticDevice): cuál de los
|
|
# dos es se decide al verificarlo (ver TwoFactorVerifyView/
|
|
# TwoFactorSetupView en crochet/views.py), no aquí. autocomplete
|
|
# "one-time-code" es el valor estándar para que el navegador/gestor de
|
|
# contraseñas ofrezca autorrellenar con el código recibido, si puede.
|
|
token = forms.CharField(
|
|
label=_('Código'),
|
|
widget=forms.TextInput(attrs={'class': INPUT_CLASSES, 'autocomplete': 'one-time-code'}),
|
|
)
|
|
|
|
|
|
class TwoFactorVerifyForm(TwoFactorTokenForm):
|
|
# Solo en el login (ver TwoFactorVerifyView), no en el alta
|
|
# (TwoFactorSetupView reusa TwoFactorTokenForm sin este campo): ahí no
|
|
# tiene sentido "recordar" un dispositivo que todavía se está
|
|
# configurando.
|
|
remember_device = forms.BooleanField(
|
|
label=_('Recordar este dispositivo 30 días'),
|
|
required=False,
|
|
widget=forms.CheckboxInput(attrs={'class': 'checkbox checkbox-sm'}),
|
|
)
|
|
|
|
|
|
class TwoFactorDisableForm(forms.Form):
|
|
password = forms.CharField(
|
|
label=_('Contraseña'),
|
|
widget=forms.PasswordInput(attrs={'class': INPUT_CLASSES}),
|
|
)
|
|
|
|
def __init__(self, *args, user, **kwargs):
|
|
self.user = user
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def clean_password(self):
|
|
password = self.cleaned_data['password']
|
|
if not self.user.check_password(password):
|
|
raise forms.ValidationError(_('Contraseña incorrecta.'))
|
|
return password
|
|
|
|
|
|
class AccountDeleteForm(forms.Form):
|
|
password = forms.CharField(
|
|
label=_('Contraseña'), widget=forms.PasswordInput(attrs={'class': INPUT_CLASSES}),
|
|
)
|
|
|
|
def __init__(self, *args, user, **kwargs):
|
|
self.user = user
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def clean_password(self):
|
|
password = self.cleaned_data['password']
|
|
if not self.user.check_password(password):
|
|
raise forms.ValidationError(_('Contraseña incorrecta.'))
|
|
return password
|
|
|
|
def notify_account_deleted(self):
|
|
# Antes de borrar la cuenta (ver AccountDeleteView), mientras el
|
|
# email todavía existe. Sin email no hay a quién avisar.
|
|
if self.user.email:
|
|
context = {'user': self.user, 'site_name': settings.SITE_NAME, 'support_email': settings.SUPPORT_EMAIL}
|
|
_send_account_notice(
|
|
'crochet/email/account_deleted_subject.txt',
|
|
'crochet/email/account_deleted_notice.txt',
|
|
'crochet/email/account_deleted_notice.html',
|
|
context, self.user.email,
|
|
)
|