Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72def373e3 | |||
| a50cadc873 | |||
| 551057b067 |
@@ -1,11 +0,0 @@
|
|||||||
from jinja2 import Environment
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.templatetags.static import static
|
|
||||||
|
|
||||||
def environment(**options):
|
|
||||||
env = Environment(**options)
|
|
||||||
env.globals.update({
|
|
||||||
'static': static,
|
|
||||||
'url': reverse,
|
|
||||||
})
|
|
||||||
return env
|
|
||||||
@@ -14,6 +14,7 @@ import logging
|
|||||||
import os, sys
|
import os, sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEV_ENV = (sys.argv[1] == 'runserver')
|
||||||
|
|
||||||
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django.forms',
|
||||||
'compressor',
|
'compressor',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -136,14 +138,6 @@ TEMPLATES = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.jinja2.Jinja2',
|
|
||||||
'DIRS': [BASE_DIR / 'templates/jinja2'],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'environment': 'proyecto.jinja2.environment',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'proyecto.wsgi.application'
|
WSGI_APPLICATION = 'proyecto.wsgi.application'
|
||||||
@@ -430,3 +424,10 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|||||||
|
|
||||||
USE_X_FORWARDED_HOST = True
|
USE_X_FORWARDED_HOST = True
|
||||||
SECURE_REFERER_POLICY = "strict-origin-when-cross-origin"
|
SECURE_REFERER_POLICY = "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
from django.forms.renderers import TemplatesSetting
|
||||||
|
|
||||||
|
class CustomFormRenderer(TemplatesSetting):
|
||||||
|
form_template_name = "tienda/form_snippet.html"
|
||||||
|
|
||||||
|
FORM_RENDERER = "proyecto.settings.CustomFormRenderer"
|
||||||
+27
-22
@@ -1,46 +1,51 @@
|
|||||||
amqp==5.3.1
|
amqp==5.3.1
|
||||||
asgiref==3.11.0
|
asgiref==3.11.1
|
||||||
billiard==4.2.4
|
billiard==4.2.4
|
||||||
celery==5.6.2
|
boto3==1.43.5
|
||||||
certifi==2026.1.4
|
botocore==1.43.5
|
||||||
|
celery==5.6.3
|
||||||
|
certifi==2026.4.22
|
||||||
cffi==2.0.0
|
cffi==2.0.0
|
||||||
charset-normalizer==3.4.4
|
charset-normalizer==3.4.7
|
||||||
click==8.3.1
|
click==8.3.3
|
||||||
click-didyoumean==0.3.1
|
click-didyoumean==0.3.1
|
||||||
click-plugins==1.1.1.2
|
click-plugins==1.1.1.2
|
||||||
click-repl==0.3.0
|
click-repl==0.3.0
|
||||||
cryptography==46.0.7
|
cryptography==48.0.0
|
||||||
Django==6.0.4
|
defusedxml==0.7.1
|
||||||
|
Django==6.0.5
|
||||||
django-appconf==1.2.0
|
django-appconf==1.2.0
|
||||||
django-redis==5.4.0
|
django-redis==6.0.0
|
||||||
|
django-storages==1.14.6
|
||||||
django_compressor==4.6.0
|
django_compressor==4.6.0
|
||||||
django-storages[boto3]==1.14.6
|
fonttools==4.62.1
|
||||||
gunicorn==25.1.0
|
fpdf2==2.8.7
|
||||||
idna==3.11
|
gunicorn==26.0.0
|
||||||
Jinja2==3.1.6
|
idna==3.13
|
||||||
|
|
||||||
|
jmespath==1.1.0
|
||||||
kombu==5.6.2
|
kombu==5.6.2
|
||||||
MarkupSafe==3.0.3
|
MarkupSafe==3.0.3
|
||||||
packaging==26.0
|
packaging==26.2
|
||||||
paypalrestsdk==1.13.3
|
paypalrestsdk==1.13.3
|
||||||
pillow==12.2.0
|
pillow==12.2.0
|
||||||
boto3==1.42.97
|
|
||||||
prompt_toolkit==3.0.52
|
prompt_toolkit==3.0.52
|
||||||
|
psycopg2-binary==2.9.12
|
||||||
pycparser==3.0
|
pycparser==3.0
|
||||||
pyOpenSSL==26.0.0
|
pyOpenSSL==26.2.0
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
rcssmin==1.2.2
|
rcssmin==1.2.2
|
||||||
redis==5.2.1
|
redis==7.4.0
|
||||||
requests==2.33.0
|
requests==2.33.1
|
||||||
rjsmin==1.2.5
|
rjsmin==1.2.5
|
||||||
|
s3transfer==0.17.0
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
sqlparse==0.5.5
|
sqlparse==0.5.5
|
||||||
stripe==14.3.0
|
stripe==15.1.0
|
||||||
typing_extensions==4.15.0
|
typing_extensions==4.15.0
|
||||||
tzdata==2025.3
|
tzdata==2026.2
|
||||||
tzlocal==5.3.1
|
tzlocal==5.3.1
|
||||||
urllib3==2.6.3
|
urllib3==2.6.3
|
||||||
vine==5.1.0
|
vine==5.1.0
|
||||||
wcwidth==0.6.0
|
wcwidth==0.7.0
|
||||||
whitenoise==6.12.0
|
whitenoise==6.12.0
|
||||||
fpdf2==2.8.7
|
|
||||||
psycopg2-binary==2.9.11
|
|
||||||
+354
@@ -0,0 +1,354 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from .models import Category
|
||||||
|
|
||||||
|
|
||||||
|
class ProductForm(forms.Form):
|
||||||
|
name = forms.CharField(
|
||||||
|
label="Nombre del Producto",
|
||||||
|
max_length=200,
|
||||||
|
required = True,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Ej: iPhone 15 Pro Max'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
briefdesc = forms.CharField(
|
||||||
|
label="Descripción Breve",
|
||||||
|
max_length=250,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Una descripción corta para mostrar en las tarjetas de producto'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
widget=forms.Textarea(attrs={"rows": "5", "class": "form-control"}),
|
||||||
|
max_length=5000,
|
||||||
|
label="Descripción completa",
|
||||||
|
required = True
|
||||||
|
)
|
||||||
|
price = forms.FloatField(
|
||||||
|
label="Precio (en €)",
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': '15.99'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stock = forms.IntegerField(
|
||||||
|
label="Stock Disponible",
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
category = forms.ModelChoiceField(
|
||||||
|
queryset=Category.objects.all(),
|
||||||
|
label="Categoría",
|
||||||
|
required=True,
|
||||||
|
widget=forms.Select(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
primary_image = forms.ImageField(
|
||||||
|
label="Imagen Principal",
|
||||||
|
required = False,
|
||||||
|
widget = forms.ClearableFileInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': 'image/*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductEditForm(forms.Form):
|
||||||
|
name = forms.CharField(
|
||||||
|
label="Nombre del Producto",
|
||||||
|
max_length=200,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Ej: iPhone 15 Pro Max'})
|
||||||
|
)
|
||||||
|
briefdesc = forms.CharField(
|
||||||
|
label="Descripción Breve",
|
||||||
|
max_length=250,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Una descripción corta'})
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
widget=forms.Textarea(attrs={"rows": "5", "class": "form-control"}),
|
||||||
|
max_length=5000,
|
||||||
|
label="Descripción completa",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
price = forms.FloatField(
|
||||||
|
label="Precio (en €)",
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '15.99'})
|
||||||
|
)
|
||||||
|
stock = forms.IntegerField(
|
||||||
|
label="Stock Disponible",
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
category = forms.ModelChoiceField(
|
||||||
|
queryset=Category.objects.all(),
|
||||||
|
label="Categoría",
|
||||||
|
required=True,
|
||||||
|
widget=forms.Select(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
primary_image = forms.ImageField(
|
||||||
|
label="Imagen Principal (opcional)",
|
||||||
|
required=False,
|
||||||
|
widget=forms.ClearableFileInput(attrs={'class': 'form-control', 'accept': 'image/*'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SecondaryImageForm(forms.Form):
|
||||||
|
image = forms.ImageField(
|
||||||
|
label="Seleccionar Imagen",
|
||||||
|
required = True,
|
||||||
|
widget = forms.ClearableFileInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': 'image/*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
alt = forms.CharField(
|
||||||
|
label="Texto Alternativo",
|
||||||
|
max_length=255,
|
||||||
|
required = False,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Descripción opcional de la imagen'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoginForm(forms.Form):
|
||||||
|
email = forms.EmailField(
|
||||||
|
label = "Correo Electrónico",
|
||||||
|
max_length=255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Correo Electronico de tu cuenta...'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password = forms.CharField(
|
||||||
|
label="Contraseña",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.PasswordInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Contraseña del usuario'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
remember = forms.BooleanField(
|
||||||
|
required = False,
|
||||||
|
label = "Recuerdame",
|
||||||
|
widget = forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterForm(forms.Form):
|
||||||
|
name = forms.CharField(
|
||||||
|
label = "Nombre Completo",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
email = forms.EmailField(
|
||||||
|
label = "Correo Electrónico",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password = forms.CharField(
|
||||||
|
label = "Contraseña",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.PasswordInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password_confirm = forms.CharField(
|
||||||
|
label = "Verificar Contraseña",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.PasswordInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
terms = forms.BooleanField(
|
||||||
|
required = True,
|
||||||
|
label = "Acepto los terminos y condiciones",
|
||||||
|
widget = forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
password = cleaned_data.get("password")
|
||||||
|
password_confirm = cleaned_data.get("password_confirm")
|
||||||
|
if password and password_confirm and password != password_confirm:
|
||||||
|
raise ValidationError("Las contraseñas no coinciden.")
|
||||||
|
|
||||||
|
|
||||||
|
class EditProfileForm(forms.Form):
|
||||||
|
first_name = forms.CharField(
|
||||||
|
label="Nombre",
|
||||||
|
max_length=150,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
last_name = forms.CharField(
|
||||||
|
label="Apellidos",
|
||||||
|
max_length=150,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
email = forms.EmailField(
|
||||||
|
label="Correo Electrónico",
|
||||||
|
max_length=254,
|
||||||
|
required=True,
|
||||||
|
widget=forms.EmailInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordForm(forms.Form):
|
||||||
|
current_password = forms.CharField(
|
||||||
|
label="Contraseña Actual",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
new_password = forms.CharField(
|
||||||
|
label="Nueva Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
confirm_password = forms.CharField(
|
||||||
|
label="Confirmar Nueva Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
new_password = cleaned_data.get("new_password")
|
||||||
|
confirm_password = cleaned_data.get("confirm_password")
|
||||||
|
if new_password and confirm_password and new_password != confirm_password:
|
||||||
|
raise ValidationError("Las contraseñas no coinciden.")
|
||||||
|
if new_password and len(new_password) < 8:
|
||||||
|
raise ValidationError("La contraseña debe tener al menos 8 caracteres.")
|
||||||
|
|
||||||
|
|
||||||
|
class ShippingAddressForm(forms.Form):
|
||||||
|
full_name = forms.CharField(
|
||||||
|
label="Nombre Completo",
|
||||||
|
max_length=255,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Juan Pérez García'})
|
||||||
|
)
|
||||||
|
address_line_1 = forms.CharField(
|
||||||
|
label="Dirección",
|
||||||
|
max_length=255,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Calle Mayor 123'})
|
||||||
|
)
|
||||||
|
address_line_2 = forms.CharField(
|
||||||
|
label="Dirección (línea 2)",
|
||||||
|
max_length=255,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Piso, puerta, etc.'})
|
||||||
|
)
|
||||||
|
city = forms.CharField(
|
||||||
|
label="Población",
|
||||||
|
max_length=100,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Almería'})
|
||||||
|
)
|
||||||
|
postal_code = forms.CharField(
|
||||||
|
label="Código Postal",
|
||||||
|
max_length=5,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '04001'})
|
||||||
|
)
|
||||||
|
country = forms.CharField(
|
||||||
|
label="País",
|
||||||
|
max_length=100,
|
||||||
|
required=False,
|
||||||
|
initial="España",
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'readonly': True})
|
||||||
|
)
|
||||||
|
phone = forms.CharField(
|
||||||
|
label="Teléfono",
|
||||||
|
max_length=20,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '612 345 678'})
|
||||||
|
)
|
||||||
|
is_default = forms.BooleanField(
|
||||||
|
label="Establecer como dirección predeterminada",
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordForm(forms.Form):
|
||||||
|
email = forms.EmailField(
|
||||||
|
label="Correo Electrónico",
|
||||||
|
max_length=254,
|
||||||
|
required=True,
|
||||||
|
widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'tu@email.com'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordPhase2Form(forms.Form):
|
||||||
|
password = forms.CharField(
|
||||||
|
label="Nueva Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
verify_password = forms.CharField(
|
||||||
|
label="Confirmar Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
password = cleaned_data.get("password")
|
||||||
|
verify_password = cleaned_data.get("verify_password")
|
||||||
|
if password and verify_password and password != verify_password:
|
||||||
|
raise ValidationError("Las contraseñas no coinciden.")
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.4 on 2026-05-07 08:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0007_add_product_sku'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='briefdesc',
|
||||||
|
field=models.TextField(default='', max_length=250),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(default='', max_length=5000),
|
||||||
|
),
|
||||||
|
]
|
||||||
+1
-1
@@ -86,7 +86,7 @@ class Product(models.Model):
|
|||||||
name = models.CharField(max_length=200, default="")
|
name = models.CharField(max_length=200, default="")
|
||||||
sku = models.CharField(max_length=50, unique=True, blank=True, null=True)
|
sku = models.CharField(max_length=50, unique=True, blank=True, null=True)
|
||||||
description = models.TextField(default = "", max_length=5000)
|
description = models.TextField(default = "", max_length=5000)
|
||||||
briefdesc = models.TextField(default = "", max_length=1000)
|
briefdesc = models.TextField(default = "", max_length=250)
|
||||||
price = models.FloatField(default = 0)
|
price = models.FloatField(default = 0)
|
||||||
stock = models.PositiveIntegerField(default=0)
|
stock = models.PositiveIntegerField(default=0)
|
||||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||||
|
|||||||
+4
-8
@@ -11,21 +11,19 @@ from .models import User, VerificationCode
|
|||||||
@shared_task
|
@shared_task
|
||||||
def enviar_correo_bienvenida(email_usuario: str, nombre_usuario: str):
|
def enviar_correo_bienvenida(email_usuario: str, nombre_usuario: str):
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
'emails/welcome.html',
|
'tienda/emails/welcome.html',
|
||||||
{
|
{
|
||||||
"name": nombre_usuario
|
"name": nombre_usuario
|
||||||
},
|
},
|
||||||
using='jinja2'
|
|
||||||
)
|
)
|
||||||
send_hemail(email_usuario, "Inicio de Sesión correcto", html_content, "Has iniciado sesión...")
|
send_hemail(email_usuario, "Inicio de Sesión correcto", html_content, "Has iniciado sesión...")
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def banear_usuario(email_usuario: str):
|
def banear_usuario(email_usuario: str):
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
'emails/ban.html',
|
'tienda/emails/ban.html',
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
using='jinja2'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
send_hemail(email_usuario, "Cuenta Bloqueada", html_content, "Tu cuenta ha sido bloqueada...")
|
send_hemail(email_usuario, "Cuenta Bloqueada", html_content, "Tu cuenta ha sido bloqueada...")
|
||||||
@@ -33,9 +31,8 @@ def banear_usuario(email_usuario: str):
|
|||||||
@shared_task
|
@shared_task
|
||||||
def desbanear_usuario(email_usuario: str):
|
def desbanear_usuario(email_usuario: str):
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
'emails/unban.html',
|
'tienda/emails/unban.html',
|
||||||
{},
|
{},
|
||||||
using='jinja2'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
send_hemail(email_usuario, "Cuenta Desbloqueada", html_content, "Tu cuenta ha sido desbloqueada...")
|
send_hemail(email_usuario, "Cuenta Desbloqueada", html_content, "Tu cuenta ha sido desbloqueada...")
|
||||||
@@ -67,14 +64,13 @@ def enviar_correo_recuperacion(email: str):
|
|||||||
)
|
)
|
||||||
ver_code.save()
|
ver_code.save()
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
'emails/reset_pass.html',
|
'tienda/emails/reset_pass.html',
|
||||||
{
|
{
|
||||||
"name": usuario.get_full_name(),
|
"name": usuario.get_full_name(),
|
||||||
"domain": settings.DOMAIN,
|
"domain": settings.DOMAIN,
|
||||||
"protocol": settings.PROTOCOL,
|
"protocol": settings.PROTOCOL,
|
||||||
"code": ver_code.code
|
"code": ver_code.code
|
||||||
},
|
},
|
||||||
using='jinja2'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
send_hemail(email, "Reset de Contraseña", html_content, "Estas reseteando la contraseña...")
|
send_hemail(email, "Reset de Contraseña", html_content, "Estas reseteando la contraseña...")
|
||||||
|
|||||||
@@ -13,74 +13,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
<!-- Nombre del producto -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Nombre del Producto <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="name" name="name" required maxlength="200"
|
|
||||||
placeholder="Ej: iPhone 15 Pro Max">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción breve -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="briefdesc" class="form-label">Descripción Breve</label>
|
|
||||||
<input type="text" class="form-control" id="briefdesc" name="briefdesc" maxlength="250"
|
|
||||||
placeholder="Una descripción corta para mostrar en las tarjetas de producto">
|
|
||||||
<div class="form-text">Opcional. Se mostrará en las vistas de listado de productos.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción completa -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="description" class="form-label">Descripción Completa <span class="text-danger">*</span></label>
|
|
||||||
<textarea class="form-control" id="description" name="description" rows="5" required
|
|
||||||
placeholder="Describe tu producto en detalle..."></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Precio -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="price" class="form-label">Precio <span class="text-danger">*</span></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
<input type="number" class="form-control" id="price" name="price" required
|
|
||||||
min="0" step="0.01" placeholder="0.00">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stock -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="stock" class="form-label">Stock disponible <span class="text-danger">*</span></label>
|
|
||||||
<input type="number" class="form-control" id="stock" name="stock" required
|
|
||||||
min="0" step="1" placeholder="0">
|
|
||||||
<div class="form-text">Cantidad máxima que podrán comprar los clientes.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Categoría -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="category" class="form-label">Categoría <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" id="category" name="category" required>
|
|
||||||
<option value="" selected disabled>Selecciona una categoría</option>
|
|
||||||
{% for category in categories %}
|
|
||||||
<option value="{{ category.id }}">{{ category.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imagen principal -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="primary_image" class="form-label">Imagen Principal</label>
|
|
||||||
<input type="file" class="form-control" id="primary_image" name="primary_image"
|
|
||||||
accept="image/*">
|
|
||||||
<div class="form-text">Opcional. Esta será la imagen destacada del producto.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imágenes secundarias -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="secondary_images" class="form-label">Imágenes Secundarias</label>
|
|
||||||
<input type="file" class="form-control" id="secondary_images" name="secondary_images"
|
|
||||||
accept="image/*" multiple>
|
|
||||||
<div class="form-text">Opcional. Puedes seleccionar múltiples imágenes adicionales.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Botones -->
|
<!-- Botones -->
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
<a href="{% url 'mis_productos' %}" class="btn btn-secondary">Cancelar</a>
|
<a href="{% url 'mis_productos' %}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
|||||||
@@ -21,52 +21,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{ form.as_p }}
|
||||||
<label for="full_name" class="form-label">Nombre Completo *</label>
|
|
||||||
<input type="text" class="form-control" id="full_name" name="full_name" value="{{ direccion.full_name|default:'' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="address_line_1" class="form-label">Dirección *</label>
|
|
||||||
<input type="text" class="form-control" id="address_line_1" name="address_line_1" value="{{ direccion.address_line_1|default:'' }}" placeholder="Calle, número, piso, puerta" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="address_line_2" class="form-label">Dirección (línea 2)</label>
|
|
||||||
<input type="text" class="form-control" id="address_line_2" name="address_line_2" value="{{ direccion.address_line_2|default:'' }}" placeholder="Edificio, bloque, etc. (opcional)">
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="city" class="form-label">Ciudad/Pueblo (Almería) *</label>
|
|
||||||
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" list="almeria-towns" autocomplete="off" required>
|
|
||||||
<datalist id="almeria-towns">
|
|
||||||
{% for town in almeria_municipalities %}
|
|
||||||
<option value="{{ town }}"></option>
|
|
||||||
{% endfor %}
|
|
||||||
</datalist>
|
|
||||||
<div class="form-text">Selecciona o escribe un municipio de la provincia de Almería.</div>
|
|
||||||
<div class="invalid-feedback" id="city-validation-message">
|
|
||||||
El pueblo/ciudad debe pertenecer a la provincia de Almería.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="postal_code" class="form-label">Código Postal *</label>
|
|
||||||
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ direccion.postal_code|default:'' }}" pattern="04[0-9]{3}" maxlength="5" placeholder="04XXX" required>
|
|
||||||
<div class="form-text">Solo aceptamos códigos postales de Almería (04xxx).</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="country" class="form-label">País *</label>
|
|
||||||
<input type="text" class="form-control" id="country" name="country" value="España" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="phone" class="form-label">Teléfono *</label>
|
|
||||||
<input type="tel" class="form-control" id="phone" name="phone" value="{{ direccion.phone|default:'' }}" placeholder="+34 600 000 000" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="is_default" name="is_default" {% if direccion.is_default %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="is_default">
|
|
||||||
Establecer como dirección predeterminada
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">{% if direccion %}Actualizar{% else %}Crear{% endif %} Dirección</button>
|
<button type="submit" class="btn btn-primary">{% if direccion %}Actualizar{% else %}Crear{% endif %} Dirección</button>
|
||||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-secondary">Cancelar</a>
|
<a href="{% url 'direcciones_usuario' %}" class="btn btn-secondary">Cancelar</a>
|
||||||
@@ -80,7 +35,6 @@
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const cityInput = document.getElementById('city');
|
const cityInput = document.getElementById('city');
|
||||||
const cityValidationMessage = document.getElementById('city-validation-message');
|
|
||||||
const form = cityInput ? cityInput.form : null;
|
const form = cityInput ? cityInput.form : null;
|
||||||
|
|
||||||
if (!cityInput || !form) {
|
if (!cityInput || !form) {
|
||||||
@@ -123,8 +77,6 @@
|
|||||||
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
||||||
cityInput.classList.add('is-invalid');
|
cityInput.classList.add('is-invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
cityValidationMessage.textContent = cityInput.validationMessage || 'El pueblo/ciudad debe pertenecer a la provincia de Almería.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cityInput.addEventListener('input', validateTown);
|
cityInput.addEventListener('input', validateTown);
|
||||||
|
|||||||
@@ -37,18 +37,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{ form.as_p }}
|
||||||
<label for="first_name" class="form-label">Nombre</label>
|
|
||||||
<input type="text" class="form-control" id="first_name" name="first_name" value="{{ user.first_name }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="last_name" class="form-label">Apellidos</label>
|
|
||||||
<input type="text" class="form-control" id="last_name" name="last_name" value="{{ user.last_name }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Nombre de Usuario</label>
|
<label for="username" class="form-label">Nombre de Usuario</label>
|
||||||
<input type="text" class="form-control" id="username" value="{{ user.username }}" disabled>
|
<input type="text" class="form-control" id="username" value="{{ user.username }}" disabled>
|
||||||
@@ -69,19 +58,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{% url 'cambiar_contrasena' %}">
|
<form method="POST" action="{% url 'cambiar_contrasena' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{ password_form.as_p }}
|
||||||
<label for="current_password" class="form-label">Contraseña Actual</label>
|
|
||||||
<input type="password" class="form-control" id="current_password" name="current_password" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_password" class="form-label">Nueva Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
|
||||||
<small class="text-muted">Mínimo 8 caracteres</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="confirm_password" class="form-label">Confirmar Nueva Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
|
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,67 +13,9 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
<!-- Nombre del producto -->
|
<!-- Imágenes secundarias (no incluidas en el form) -->
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Nombre del Producto <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="name" name="name" required maxlength="200"
|
|
||||||
value="{{ producto.name }}" placeholder="Ej: iPhone 15 Pro Max">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción breve -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="briefdesc" class="form-label">Descripción Breve</label>
|
|
||||||
<input type="text" class="form-control" id="briefdesc" name="briefdesc" maxlength="250"
|
|
||||||
value="{{ producto.briefdesc }}" placeholder="Una descripción corta para mostrar en las tarjetas de producto">
|
|
||||||
<div class="form-text">Opcional. Se mostrará en las vistas de listado de productos.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción completa -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="description" class="form-label">Descripción Completa <span class="text-danger">*</span></label>
|
|
||||||
<textarea class="form-control" id="description" name="description" rows="5" required
|
|
||||||
placeholder="Describe tu producto en detalle...">{{ producto.description }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Precio -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="price" class="form-label">Precio <span class="text-danger">*</span></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
<input type="number" class="form-control" id="price" name="price" required
|
|
||||||
min="0" step="0.01" value="{{ producto.price }}" placeholder="0.00">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stock -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="stock" class="form-label">Stock disponible <span class="text-danger">*</span></label>
|
|
||||||
<input type="number" class="form-control" id="stock" name="stock" required
|
|
||||||
min="0" step="1" value="{{ producto.stock }}" placeholder="0">
|
|
||||||
<div class="form-text">Cantidad máxima que podrán comprar los clientes.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Categoría -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="category" class="form-label">Categoría <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" id="category" name="category" required>
|
|
||||||
<option value="" disabled>Selecciona una categoría</option>
|
|
||||||
{% for category in categories %}
|
|
||||||
<option value="{{ category.id }}" {% if producto.category.id == category.id %}selected{% endif %}>{{ category.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imagen principal -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="primary_image" class="form-label">Imagen Principal</label>
|
|
||||||
<input type="file" class="form-control" id="primary_image" name="primary_image"
|
|
||||||
accept="image/*">
|
|
||||||
<div class="form-text">Opcional. Si subes una nueva, reemplazará la actual.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imágenes secundarias -->
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="secondary_images" class="form-label">Imágenes Secundarias</label>
|
<label for="secondary_images" class="form-label">Imágenes Secundarias</label>
|
||||||
<input type="file" class="form-control" id="secondary_images" name="secondary_images"
|
<input type="file" class="form-control" id="secondary_images" name="secondary_images"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field.label_tag }} {{ field }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{% extends "tienda/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4 mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2>Gestionar Imágenes</h2>
|
||||||
|
<p class="text-muted mb-0">Producto: <strong>{{ producto.name }}</strong></p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'mis_productos' %}" class="btn btn-outline-secondary">← Volver a Mis Productos</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Imagen Principal</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if producto.primary_image %}
|
||||||
|
<img src="{{ producto.primary_image.image.url }}" alt="{{ producto.primary_image.alt|default:producto.name }}" class="rounded" style="width: 200px; height: 200px; object-fit: cover;">
|
||||||
|
<p class="mt-2 text-muted mb-0">Esta imagen no se puede cambiar desde aquí.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No hay imagen principal asignada.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Imágenes Secundarias</h5>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#agregarImagenModal">
|
||||||
|
➕ Agregar Imagen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if secondary_images %}
|
||||||
|
<div class="row">
|
||||||
|
{% for img in secondary_images %}
|
||||||
|
<div class="col-md-3 col-sm-4 col-6 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<img src="{{ img.image.url }}" alt="{{ img.alt|default:producto.name }}" class="card-img-top" style="height: 180px; object-fit: cover;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<form method="POST" action="{% url 'eliminar_imagen_secundaria' producto.id img.id %}" onsubmit="return confirm('¿Seguro que quieres eliminar esta imagen?');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm w-100">🗑 Eliminar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted text-center py-4">No hay imágenes secundarias. ¡Agrega una!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="agregarImagenModal" tabindex="-1" aria-labelledby="agregarImagenModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="agregarImagenModalLabel">Agregar Imagen Secundaria</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
{{ form }}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Subir Imagen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,22 +12,7 @@
|
|||||||
<form method="post" action="{% url 'login' %}">
|
<form method="post" action="{% url 'login' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="mb-3">
|
{{ form }}
|
||||||
<label for="loginEmail" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="loginEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="loginPassword" class="form-label">Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="loginPassword" name="password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="rememberMe" name="remember">
|
|
||||||
<label class="form-check-label" for="rememberMe">
|
|
||||||
Recordarme
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Iniciar Sesión</button>
|
<button type="submit" class="btn btn-primary">Iniciar Sesión</button>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<td class="text-end">{{ producto.stock }}</td>
|
<td class="text-end">{{ producto.stock }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<a href="{% url 'gestionar_imagenes' producto.id %}" class="btn btn-outline-secondary btn-sm">Gestionar Imágenes</a>
|
||||||
<a href="{% url 'editar_producto' producto.id %}" class="btn btn-outline-primary btn-sm">Editar</a>
|
<a href="{% url 'editar_producto' producto.id %}" class="btn btn-outline-primary btn-sm">Editar</a>
|
||||||
<form method="POST" action="{% url 'borrar_producto' producto.id %}" onsubmit="return confirm('¿Seguro que quieres borrar este producto?');">
|
<form method="POST" action="{% url 'borrar_producto' producto.id %}" onsubmit="return confirm('¿Seguro que quieres borrar este producto?');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@@ -12,33 +12,7 @@
|
|||||||
<form method="post" action="{% url 'register' %}">
|
<form method="post" action="{% url 'register' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="mb-3">
|
{{ form }}
|
||||||
<label for="registerName" class="form-label">Nombre Completo</label>
|
|
||||||
<input type="text" class="form-control" id="registerName" name="name" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="registerEmail" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="registerEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="registerPassword" class="form-label">Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="registerPassword" name="password" required>
|
|
||||||
<div class="form-text">La contraseña debe tener al menos 8 caracteres.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="registerPasswordConfirm" class="form-label">Confirmar Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="registerPasswordConfirm" name="password_confirm" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="acceptTerms" name="terms" required>
|
|
||||||
<label class="form-check-label" for="acceptTerms">
|
|
||||||
Acepto los <a href="{% url 'terminos' %}" target="_blank">términos y condiciones</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Crear Cuenta</button>
|
<button type="submit" class="btn btn-primary">Crear Cuenta</button>
|
||||||
|
|||||||
@@ -11,11 +11,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="{% url 'reset_password' %}">
|
<form method="post" action="{% url 'reset_password' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
<div class="mb-3">
|
|
||||||
<label for="loginEmail" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="loginEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||||
|
|||||||
@@ -11,16 +11,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="{% url 'reset_password_phase2' code %}">
|
<form method="post" action="{% url 'reset_password_phase2' code %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="verify_password" class="form-label">Verificar contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="verify_password" name="verify_password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||||
|
|||||||
+181
@@ -14,6 +14,7 @@ from .models import (
|
|||||||
StockReservation, StockReservationItem, Cart, CartItem,
|
StockReservation, StockReservationItem, Cart, CartItem,
|
||||||
Order, OrderItem, OrderMessage, SavedPaymentMethod, ShippingAddress
|
Order, OrderItem, OrderMessage, SavedPaymentMethod, ShippingAddress
|
||||||
)
|
)
|
||||||
|
from .forms import UserRegisterForm, UserLoginForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
||||||
import string
|
import string
|
||||||
import random
|
import random
|
||||||
@@ -23,6 +24,185 @@ import random
|
|||||||
class UserModelTests(TestCase):
|
class UserModelTests(TestCase):
|
||||||
"""Tests exhaustivos para el modelo User."""
|
"""Tests exhaustivos para el modelo User."""
|
||||||
|
|
||||||
|
|
||||||
|
class FormTests(TestCase):
|
||||||
|
"""Tests para formularios Django."""
|
||||||
|
|
||||||
|
def test_user_register_form_terms_required(self):
|
||||||
|
"""El campo terms debe ser obligatorio."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("terms", form.errors)
|
||||||
|
|
||||||
|
def test_user_register_form_terms_off_not_checked(self):
|
||||||
|
"""Si terms está en off (None/false), debe fallar."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
"terms": False,
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("terms", form.errors)
|
||||||
|
|
||||||
|
def test_user_register_form_terms_on(self):
|
||||||
|
"""Si terms está marcado, el formulario debe ser válido."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
"terms": True,
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_user_register_form_passwords_mismatch(self):
|
||||||
|
"""Las contraseñas deben coincidir."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "different_password",
|
||||||
|
"terms": True,
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
def test_user_register_form_empty_fields(self):
|
||||||
|
"""Los campos obligatorios no pueden estar vacíos."""
|
||||||
|
form = UserRegisterForm(data={})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("name", form.errors)
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
self.assertIn("password", form.errors)
|
||||||
|
self.assertIn("password_confirm", form.errors)
|
||||||
|
|
||||||
|
def test_user_login_form_valid(self):
|
||||||
|
"""Login con datos válidos."""
|
||||||
|
form = UserLoginForm(data={
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_user_login_form_missing_email(self):
|
||||||
|
"""Email es obligatorio en login."""
|
||||||
|
form = UserLoginForm(data={
|
||||||
|
"password": "password123",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_user_login_form_invalid_email_format(self):
|
||||||
|
"""Email debe tener formato válido."""
|
||||||
|
form = UserLoginForm(data={
|
||||||
|
"email": "not-an-email",
|
||||||
|
"password": "password123",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_edit_profile_form_valid(self):
|
||||||
|
"""Formulario de edición de perfil válido."""
|
||||||
|
form = EditProfileForm(data={
|
||||||
|
"first_name": "Juan",
|
||||||
|
"last_name": "Pérez",
|
||||||
|
"email": "juan@example.com",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_edit_profile_form_missing_email(self):
|
||||||
|
"""Email es obligatorio en perfil."""
|
||||||
|
form = EditProfileForm(data={
|
||||||
|
"first_name": "Juan",
|
||||||
|
"last_name": "Pérez",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_change_password_form_passwords_mismatch(self):
|
||||||
|
"""Las nuevas contraseñas deben coincidir."""
|
||||||
|
form = ChangePasswordForm(data={
|
||||||
|
"current_password": "oldpass123",
|
||||||
|
"new_password": "newpass123",
|
||||||
|
"confirm_password": "differentpass",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
def test_change_password_form_short_password(self):
|
||||||
|
"""La nueva contraseña debe tener al menos 8 caracteres."""
|
||||||
|
form = ChangePasswordForm(data={
|
||||||
|
"current_password": "oldpass123",
|
||||||
|
"new_password": "short",
|
||||||
|
"confirm_password": "short",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
def test_shipping_address_form_valid(self):
|
||||||
|
"""Dirección con datos válidos."""
|
||||||
|
form = ShippingAddressForm(data={
|
||||||
|
"full_name": "Juan Pérez",
|
||||||
|
"address_line_1": "Calle Mayor 123",
|
||||||
|
"city": "Almería",
|
||||||
|
"postal_code": "04001",
|
||||||
|
"country": "España",
|
||||||
|
"phone": "612345678",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_shipping_address_form_missing_required_fields(self):
|
||||||
|
"""Campos obligatorios no pueden estar vacíos."""
|
||||||
|
form = ShippingAddressForm(data={})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("full_name", form.errors)
|
||||||
|
self.assertIn("address_line_1", form.errors)
|
||||||
|
self.assertIn("city", form.errors)
|
||||||
|
self.assertIn("postal_code", form.errors)
|
||||||
|
self.assertIn("phone", form.errors)
|
||||||
|
|
||||||
|
def test_reset_password_form_valid_email(self):
|
||||||
|
"""Formulario de recuperación de contraseña."""
|
||||||
|
form = ResetPasswordForm(data={
|
||||||
|
"email": "test@example.com",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_reset_password_form_invalid_email(self):
|
||||||
|
"""Email inválido."""
|
||||||
|
form = ResetPasswordForm(data={
|
||||||
|
"email": "not-an-email",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_reset_password_phase2_form_valid(self):
|
||||||
|
"""Cambio de contraseña válido."""
|
||||||
|
form = ResetPasswordPhase2Form(data={
|
||||||
|
"password": "newpass123",
|
||||||
|
"verify_password": "newpass123",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_reset_password_phase2_form_mismatch(self):
|
||||||
|
"""Las contraseñas deben coincidir."""
|
||||||
|
form = ResetPasswordPhase2Form(data={
|
||||||
|
"password": "newpass123",
|
||||||
|
"verify_password": "different",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== ENDPOINT VIEW TESTS ====================
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user_data = {
|
self.user_data = {
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
@@ -1455,6 +1635,7 @@ class EndpointViewTests(TestCase):
|
|||||||
"email": "nuevo@example.com",
|
"email": "nuevo@example.com",
|
||||||
"password": self.password,
|
"password": self.password,
|
||||||
"password_confirm": self.password,
|
"password_confirm": self.password,
|
||||||
|
"terms": "on",
|
||||||
})
|
})
|
||||||
self.assertEqual(register_response.status_code, 302)
|
self.assertEqual(register_response.status_code, 302)
|
||||||
confirm_delay.assert_called_once()
|
confirm_delay.assert_called_once()
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ urlpatterns = [
|
|||||||
path("venta/crear-producto/", views.crear_producto, name="crear_producto"),
|
path("venta/crear-producto/", views.crear_producto, name="crear_producto"),
|
||||||
path("venta/editar-producto/<int:id>/", views.editar_producto, name="editar_producto"),
|
path("venta/editar-producto/<int:id>/", views.editar_producto, name="editar_producto"),
|
||||||
path("venta/borrar-producto/<int:id>/", views.borrar_producto, name="borrar_producto"),
|
path("venta/borrar-producto/<int:id>/", views.borrar_producto, name="borrar_producto"),
|
||||||
|
path("venta/gestionar-imagenes/<int:id>/", views.gestionar_imagenes, name="gestionar_imagenes"),
|
||||||
|
path("venta/gestionar-imagenes/<int:product_id>/eliminar/<int:image_id>/", views.eliminar_imagen_secundaria, name="eliminar_imagen_secundaria"),
|
||||||
# Carrito
|
# Carrito
|
||||||
path("cart/", views.view_cart, name="view_cart"),
|
path("cart/", views.view_cart, name="view_cart"),
|
||||||
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
||||||
|
|||||||
+269
-322
@@ -5,6 +5,7 @@ from django.db.utils import DataError
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, StockReservation, StockReservationItem, VerificationCode, SavedPaymentMethod
|
from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, StockReservation, StockReservationItem, VerificationCode, SavedPaymentMethod
|
||||||
|
from .forms import ProductForm, SecondaryImageForm, UserLoginForm, UserRegisterForm, ProductEditForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||||
from . import tasks
|
from . import tasks
|
||||||
from .vars import (
|
from .vars import (
|
||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
@@ -86,9 +87,10 @@ def _is_almeria_city(city: str) -> bool:
|
|||||||
return _normalize_location_text(city) in ALMERIA_MUNICIPALITIES
|
return _normalize_location_text(city) in ALMERIA_MUNICIPALITIES
|
||||||
|
|
||||||
|
|
||||||
def _address_form_context(direccion=None):
|
def _address_form_context(direccion=None, form=None):
|
||||||
return {
|
return {
|
||||||
"direccion": direccion,
|
"direccion": direccion,
|
||||||
|
"form": form,
|
||||||
"almeria_municipalities": ALMERIA_MUNICIPALITIES_DISPLAY,
|
"almeria_municipalities": ALMERIA_MUNICIPALITIES_DISPLAY,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,26 +223,26 @@ def index(request: HttpRequest):
|
|||||||
|
|
||||||
def login(request: HttpRequest):
|
def login(request: HttpRequest):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
email = request.POST.get("email")
|
form: UserLoginForm = UserLoginForm(request.POST)
|
||||||
password = request.POST.get("password")
|
if form.is_valid():
|
||||||
remember = request.POST.get("remember")
|
email: str = form.cleaned_data["email"]
|
||||||
|
password: str = form.cleaned_data["password"]
|
||||||
|
remember: bool = form.cleaned_data["remember"]
|
||||||
client_ip = _get_client_ip(request)
|
client_ip = _get_client_ip(request)
|
||||||
|
|
||||||
# Buscar usuario por email
|
|
||||||
try:
|
try:
|
||||||
user_obj = User.objects.get(email=email)
|
user: User = User.objects.get(email=email)
|
||||||
username = user_obj.username
|
username = user.username
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
audit_logger.warning(
|
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", email, client_ip)
|
||||||
"LOGIN_FAILED email=%s reason=user_not_found ip=%s",
|
messages.error(request, "El email o la contraseña es incorrecta")
|
||||||
email,
|
return render(request, "tienda/login.html", {"form": form})
|
||||||
client_ip,
|
if user.registration_status == User.RegisterStatus.BANNED:
|
||||||
)
|
# Usuario baneado.
|
||||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
messages.error(request, "Esta cuenta esta bloqueada.")
|
||||||
return render(request, "tienda/login.html")
|
return render(request, "tienda/login.html", {"form": form})
|
||||||
|
|
||||||
|
user = authenticate(request, username = username, password=password)
|
||||||
|
|
||||||
# Autenticar usuario
|
|
||||||
user = authenticate(request, username=username, password=password)
|
|
||||||
if user is None:
|
if user is None:
|
||||||
data: str = cache.get(f"tries_login_{username}")
|
data: str = cache.get(f"tries_login_{username}")
|
||||||
logins: int
|
logins: int
|
||||||
@@ -250,110 +252,100 @@ def login(request: HttpRequest):
|
|||||||
logins = int(data)
|
logins = int(data)
|
||||||
|
|
||||||
if logins >= 5:
|
if logins >= 5:
|
||||||
# Si ha fallado 5 intentos de login...
|
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", email)
|
||||||
audit_logger.info(
|
|
||||||
"LOGIN_FAILED email=%s reason=rate_limited", username
|
|
||||||
)
|
|
||||||
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
||||||
return render(request, "tienda/login.html")
|
return render(request, "tienda/login.html", {"form": form})
|
||||||
|
|
||||||
logins+=1
|
logins+=1
|
||||||
cache.set(f"tries_login_{username}", str(logins), 600)
|
cache.set(f"tries_login_{username}", str(logins), 600)
|
||||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
messages.error(request, "El email o la contraseña es incorrecta")
|
||||||
return render(request, "tienda/login.html")
|
return render(request, "tienda/login.html", {"form": form})
|
||||||
user = User.objects.get(username=user.username)
|
if user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
||||||
if user.registration_status == "CR":
|
audit_logger.info("LOGIN_FAILED email=%s reason=not_verified", email)
|
||||||
audit_logger.info(
|
|
||||||
"LOGIN_FAILED email=%s reason=not_verified", email
|
|
||||||
)
|
|
||||||
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
||||||
return render(request, "tienda/login.html")
|
return render(request, "tienda/login.html", {"form": form})
|
||||||
|
|
||||||
if user is not None:
|
|
||||||
auth_login(request, user)
|
auth_login(request, user)
|
||||||
|
|
||||||
# Configurar duración de sesión
|
|
||||||
if not remember:
|
if not remember:
|
||||||
request.session.set_expiry(0)
|
request.session.set_expiry(0)
|
||||||
else:
|
else:
|
||||||
request.session.set_expiry(1209600) # 14 días en segundos
|
request.session.set_expiry(1209600)
|
||||||
|
|
||||||
audit_logger.info(
|
audit_logger.info("LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", user.id, user.email, client_ip, bool(remember))
|
||||||
"LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s",
|
tasks.enviar_correo_bienvenida.delay(user.email, f"{user.first_name} {user.last_name}")
|
||||||
user.id,
|
|
||||||
user.email,
|
|
||||||
client_ip,
|
|
||||||
bool(remember),
|
|
||||||
)
|
|
||||||
tasks.enviar_correo_bienvenida.delay(user.email, "{} {}".format(user.first_name, user.last_name))
|
|
||||||
# result = send_email(user.email, "Inicio de sesión correcto", login_message.format(name = "{} {}".format(user.first_name, user.last_name)))
|
|
||||||
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
else:
|
else:
|
||||||
user1: User = User.objects.get(username=username)
|
form = UserLoginForm()
|
||||||
if user1.registration_status == User.RegisterStatus.BANNED:
|
return render(request, "tienda/login.html", {"form": form})
|
||||||
audit_logger.warning(
|
|
||||||
"LOGIN FAILED email=%s reason=user_banned ip=%s",
|
|
||||||
email,
|
|
||||||
client_ip,
|
|
||||||
)
|
|
||||||
messages.error(request, "Error, La cuenta esta bloqueada")
|
|
||||||
return render(request, "tienda/login.html")
|
|
||||||
audit_logger.warning(
|
|
||||||
"LOGIN_FAILED email=%s reason=invalid_credentials ip=%s",
|
|
||||||
email,
|
|
||||||
client_ip,
|
|
||||||
)
|
|
||||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
|
||||||
return render(request, "tienda/login.html")
|
|
||||||
|
|
||||||
return render(request, "tienda/login.html")
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# if user is not None:
|
||||||
|
# auth_login(request, user)
|
||||||
|
#
|
||||||
|
# # Configurar duración de sesión
|
||||||
|
# if not remember:
|
||||||
|
# request.session.set_expiry(0)
|
||||||
|
# else:
|
||||||
|
# request.session.set_expiry(1209600) # 14 días en segundos
|
||||||
|
#
|
||||||
|
# audit_logger.info(
|
||||||
|
# "LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s",
|
||||||
|
# user.id,
|
||||||
|
# user.email,
|
||||||
|
# client_ip,
|
||||||
|
# bool(remember),
|
||||||
|
# )
|
||||||
|
# tasks.enviar_correo_bienvenida.delay(user.email, "{} {}".format(user.first_name, user.last_name))
|
||||||
|
# # result = send_email(user.email, "Inicio de sesión correcto", login_message.format(name = "{} {}".format(user.first_name, user.last_name)))
|
||||||
|
# messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
||||||
|
# return redirect("index")
|
||||||
|
# else:
|
||||||
|
# user1: User = User.objects.get(username=username)
|
||||||
|
# if user1.registration_status == User.RegisterStatus.BANNED:
|
||||||
|
# audit_logger.warning(
|
||||||
|
# "LOGIN FAILED email=%s reason=user_banned ip=%s",
|
||||||
|
# email,
|
||||||
|
# client_ip,
|
||||||
|
# )
|
||||||
|
# messages.error(request, "Error, La cuenta esta bloqueada")
|
||||||
|
# return render(request, "tienda/login.html")
|
||||||
|
# audit_logger.warning(
|
||||||
|
# "LOGIN_FAILED email=%s reason=invalid_credentials ip=%s",
|
||||||
|
# email,
|
||||||
|
# client_ip,
|
||||||
|
# )
|
||||||
|
# messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||||
|
# return render(request, "tienda/login.html")
|
||||||
|
#
|
||||||
|
# return render(request, "tienda/login.html")
|
||||||
|
|
||||||
def register(request: HttpRequest):
|
def register(request: HttpRequest):
|
||||||
if request.user.is_authenticated:
|
|
||||||
return redirect("index")
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
name = request.POST.get("name")
|
form = UserRegisterForm(request.POST)
|
||||||
email = request.POST.get("email")
|
if form.is_valid():
|
||||||
password = request.POST.get("password")
|
name = form.cleaned_data.get("name")
|
||||||
password_confirm = request.POST.get("password_confirm")
|
email = form.cleaned_data.get("email")
|
||||||
|
password = form.cleaned_data.get("password")
|
||||||
client_ip = _get_client_ip(request)
|
client_ip = _get_client_ip(request)
|
||||||
|
|
||||||
# Validaciones
|
# Validación email
|
||||||
if password != password_confirm:
|
|
||||||
audit_logger.warning("REGISTER_FAILED email=%s reason=password_mismatch ip=%s", email, client_ip)
|
|
||||||
messages.error(request, "Las contraseñas no coinciden.")
|
|
||||||
return render(request, "tienda/register.html")
|
|
||||||
|
|
||||||
if len(password) < 8:
|
|
||||||
audit_logger.warning("REGISTER_FAILED email=%s reason=password_too_short ip=%s", email, client_ip)
|
|
||||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
|
||||||
return render(request, "tienda/register.html")
|
|
||||||
|
|
||||||
if User.objects.filter(email=email).exists():
|
if User.objects.filter(email=email).exists():
|
||||||
audit_logger.warning("REGISTER_FAILED email=%s reason=email_exists ip=%s", email, client_ip)
|
audit_logger.warning("REGISTER_FAILED email=%s reason=email_exists ip=%s", email, client_ip)
|
||||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
messages.error(request, "Ya existe un usuario con este correo electrónico")
|
||||||
return render(request, "tienda/register.html")
|
return render(request, "tienda/register.html", {"form":form})
|
||||||
|
|
||||||
# Crear username a partir del email
|
|
||||||
username = email.split("@")[0]
|
username = email.split("@")[0]
|
||||||
|
|
||||||
# Si el username ya existe, agregar un número
|
|
||||||
base_username = username
|
base_username = username
|
||||||
counter = 1
|
counter = 1
|
||||||
while User.objects.filter(username=username).exists():
|
while User.objects.filter(username=username).exists():
|
||||||
username = f"{base_username}{counter}"
|
username = f"{base_username}{counter}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# Crear usuario
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username=username,
|
username = username,
|
||||||
email=email,
|
email = email,
|
||||||
password=password,
|
password = password,
|
||||||
first_name=name
|
first_name = name
|
||||||
)
|
)
|
||||||
|
|
||||||
audit_logger.info(
|
audit_logger.info(
|
||||||
"REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s",
|
"REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s",
|
||||||
user.id,
|
user.id,
|
||||||
@@ -366,8 +358,9 @@ def register(request: HttpRequest):
|
|||||||
tasks.enviar_correo_confirmacion.delay(user.id)
|
tasks.enviar_correo_confirmacion.delay(user.id)
|
||||||
messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.")
|
messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
|
else:
|
||||||
return render(request, "tienda/register.html")
|
form = UserRegisterForm()
|
||||||
|
return render(request, "tienda/register.html", {"form":form})
|
||||||
|
|
||||||
|
|
||||||
def logout(request: HttpRequest):
|
def logout(request: HttpRequest):
|
||||||
@@ -957,94 +950,32 @@ def enviar_mensaje_pedido(request: HttpRequest, item_id: int):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def crear_producto(request: HttpRequest):
|
def crear_producto(request: HttpRequest):
|
||||||
"""Crea un nuevo producto"""
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
name = request.POST.get("name")
|
form = ProductForm(request.POST, request.FILES)
|
||||||
briefdesc = request.POST.get("briefdesc")
|
if form.is_valid():
|
||||||
description = request.POST.get("description")
|
primary_image_file = form.cleaned_data.get("primary_image")
|
||||||
price = request.POST.get("price")
|
image = None
|
||||||
stock = request.POST.get("stock")
|
|
||||||
category_id = request.POST.get("category")
|
|
||||||
primary_image_file = request.FILES.get("primary_image")
|
|
||||||
secondary_images_files = request.FILES.getlist("secondary_images")
|
|
||||||
|
|
||||||
# Validaciones
|
|
||||||
if not all([name, description, price, stock, category_id]):
|
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
|
||||||
|
|
||||||
try:
|
|
||||||
price = float(price)
|
|
||||||
if price < 0:
|
|
||||||
raise ValueError("El precio no puede ser negativo")
|
|
||||||
except ValueError:
|
|
||||||
messages.error(request, "El precio debe ser un número válido.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
|
||||||
|
|
||||||
try:
|
|
||||||
stock = int(stock)
|
|
||||||
if stock < 0:
|
|
||||||
raise ValueError("El stock no puede ser negativo")
|
|
||||||
except ValueError:
|
|
||||||
messages.error(request, "El stock debe ser un número entero válido.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
|
||||||
|
|
||||||
try:
|
|
||||||
category = Category.objects.get(id=category_id)
|
|
||||||
except Category.DoesNotExist:
|
|
||||||
messages.error(request, "Categoría no válida.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
|
||||||
|
|
||||||
# Crear imagen principal si se proporciona
|
|
||||||
primary_image = None
|
|
||||||
if primary_image_file:
|
if primary_image_file:
|
||||||
primary_image = Image.objects.create(
|
image = Image(
|
||||||
name=f"{name}_principal",
|
name = f"{form.cleaned_data['name']}_principal",
|
||||||
image=primary_image_file
|
image = primary_image_file,
|
||||||
)
|
)
|
||||||
if stock > 4294967295:
|
image.save()
|
||||||
messages.error(request, "No se puede tener mas de 4294967295 existencias. Por favor, intentelo de nuevo")
|
producto: Product = Product(
|
||||||
categories = Category.objects.all()
|
name = form.cleaned_data["name"],
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
briefdesc = form.cleaned_data["briefdesc"],
|
||||||
# Crear producto
|
description = form.cleaned_data["description"],
|
||||||
try:
|
price = form.cleaned_data["price"],
|
||||||
producto = Product.objects.create(
|
stock = form.cleaned_data["stock"],
|
||||||
name=name,
|
category = form.cleaned_data["category"],
|
||||||
briefdesc=briefdesc or "",
|
primary_image = image,
|
||||||
description=description,
|
creator = request.user
|
||||||
price=price,
|
|
||||||
stock=stock,
|
|
||||||
category=category,
|
|
||||||
primary_image=primary_image,
|
|
||||||
creator=request.user
|
|
||||||
)
|
)
|
||||||
except DataError as e:
|
producto.save()
|
||||||
logger.exception("ERROR Creating product: " + str(e))
|
return redirect("/")
|
||||||
messages.error(request, "Se ha excedido el limite de 1000 caracteres en Descripción corta o el limite de 5000 caracteres en Descripción.")
|
else:
|
||||||
categories = Category.objects.all()
|
form = ProductForm()
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
return render(request, "tienda/crear_producto.html", {"form":form})
|
||||||
_invalidate_product_cache([producto.id])
|
|
||||||
|
|
||||||
# Agregar imágenes secundarias si se proporcionan
|
|
||||||
if secondary_images_files:
|
|
||||||
for idx, img_file in enumerate(secondary_images_files):
|
|
||||||
secondary_img = Image.objects.create(
|
|
||||||
name=f"{name}_secundaria_{idx+1}",
|
|
||||||
image=img_file
|
|
||||||
)
|
|
||||||
producto.secondary_images.add(secondary_img)
|
|
||||||
|
|
||||||
messages.success(request, f"¡Producto '{name}' creado exitosamente!")
|
|
||||||
return redirect("mis_productos")
|
|
||||||
|
|
||||||
# GET request - mostrar formulario
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def editar_producto(request: HttpRequest, id: int):
|
def editar_producto(request: HttpRequest, id: int):
|
||||||
@@ -1052,74 +983,21 @@ def editar_producto(request: HttpRequest, id: int):
|
|||||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
name = request.POST.get("name")
|
form = ProductEditForm(request.POST, request.FILES)
|
||||||
briefdesc = request.POST.get("briefdesc")
|
if form.is_valid():
|
||||||
description = request.POST.get("description")
|
producto.name = form.cleaned_data["name"]
|
||||||
price = request.POST.get("price")
|
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
||||||
stock = request.POST.get("stock")
|
producto.description = form.cleaned_data["description"]
|
||||||
category_id = request.POST.get("category")
|
producto.price = form.cleaned_data["price"]
|
||||||
|
producto.stock = form.cleaned_data["stock"]
|
||||||
|
producto.category = form.cleaned_data["category"]
|
||||||
|
|
||||||
primary_image_file = request.FILES.get("primary_image")
|
primary_image_file = request.FILES.get("primary_image")
|
||||||
secondary_images_files = request.FILES.getlist("secondary_images")
|
secondary_images_files = request.FILES.getlist("secondary_images")
|
||||||
|
|
||||||
if not all([name, description, price, stock, category_id]):
|
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
|
||||||
"categories": categories,
|
|
||||||
"producto": producto
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
|
||||||
price = float(price)
|
|
||||||
if price < 0:
|
|
||||||
raise ValueError("El precio no puede ser negativo")
|
|
||||||
except ValueError:
|
|
||||||
messages.error(request, "El precio debe ser un número válido.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
|
||||||
"categories": categories,
|
|
||||||
"producto": producto
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
|
||||||
stock = int(stock)
|
|
||||||
if stock < 0:
|
|
||||||
raise ValueError("El stock no puede ser negativo")
|
|
||||||
if stock > 4294967295:
|
|
||||||
messages.error(request, "No se puede tener mas de 4294967295 de stock.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
|
||||||
"categories": categories,
|
|
||||||
"producto": producto
|
|
||||||
})
|
|
||||||
except ValueError:
|
|
||||||
messages.error(request, "El stock debe ser un número entero válido.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
|
||||||
"categories": categories,
|
|
||||||
"producto": producto
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
|
||||||
category = Category.objects.get(id=category_id)
|
|
||||||
except Category.DoesNotExist:
|
|
||||||
messages.error(request, "Categoría no válida.")
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
|
||||||
"categories": categories,
|
|
||||||
"producto": producto
|
|
||||||
})
|
|
||||||
|
|
||||||
producto.name = name
|
|
||||||
producto.briefdesc = briefdesc or ""
|
|
||||||
producto.description = description
|
|
||||||
producto.price = price
|
|
||||||
producto.stock = stock
|
|
||||||
producto.category = category
|
|
||||||
|
|
||||||
if primary_image_file:
|
if primary_image_file:
|
||||||
primary_image = Image.objects.create(
|
primary_image = Image.objects.create(
|
||||||
name=f"{name}_principal",
|
name=f"{producto.name}_principal",
|
||||||
image=primary_image_file
|
image=primary_image_file
|
||||||
)
|
)
|
||||||
producto.primary_image = primary_image
|
producto.primary_image = primary_image
|
||||||
@@ -1131,17 +1009,29 @@ def editar_producto(request: HttpRequest, id: int):
|
|||||||
producto.secondary_images.clear()
|
producto.secondary_images.clear()
|
||||||
for idx, img_file in enumerate(secondary_images_files):
|
for idx, img_file in enumerate(secondary_images_files):
|
||||||
secondary_img = Image.objects.create(
|
secondary_img = Image.objects.create(
|
||||||
name=f"{name}_secundaria_{idx+1}",
|
name=f"{producto.name}_secundaria_{idx+1}",
|
||||||
image=img_file
|
image=img_file
|
||||||
)
|
)
|
||||||
producto.secondary_images.add(secondary_img)
|
producto.secondary_images.add(secondary_img)
|
||||||
|
|
||||||
messages.success(request, f"¡Producto '{name}' actualizado exitosamente!")
|
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
||||||
return redirect("mis_productos")
|
return redirect("mis_productos")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||||
|
else:
|
||||||
|
initial = {
|
||||||
|
"name": producto.name,
|
||||||
|
"briefdesc": producto.briefdesc,
|
||||||
|
"description": producto.description,
|
||||||
|
"price": producto.price,
|
||||||
|
"stock": producto.stock,
|
||||||
|
"category": producto.category,
|
||||||
|
}
|
||||||
|
form = ProductEditForm(initial=initial)
|
||||||
|
|
||||||
categories = Category.objects.all()
|
categories = Category.objects.all()
|
||||||
return render(request, "tienda/editar_producto.html", {
|
return render(request, "tienda/editar_producto.html", {
|
||||||
"categories": categories,
|
"form": form,
|
||||||
"producto": producto
|
"producto": producto
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1160,6 +1050,55 @@ def borrar_producto(request: HttpRequest, id: int):
|
|||||||
messages.success(request, f"Producto '{nombre}' eliminado correctamente.")
|
messages.success(request, f"Producto '{nombre}' eliminado correctamente.")
|
||||||
return redirect("mis_productos")
|
return redirect("mis_productos")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def gestionar_imagenes(request: HttpRequest, id: int):
|
||||||
|
"""Gestiona las imágenes secundarias de un producto"""
|
||||||
|
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||||
|
secondary_images = producto.secondary_images.all()
|
||||||
|
form = SecondaryImageForm()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = SecondaryImageForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
image = Image(
|
||||||
|
name = f"{producto.name}_secundaria_{secondary_images.count() + 1}",
|
||||||
|
image = form.cleaned_data["image"],
|
||||||
|
alt = form.cleaned_data.get("alt", "")
|
||||||
|
)
|
||||||
|
image.save()
|
||||||
|
producto.secondary_images.add(image)
|
||||||
|
_invalidate_product_cache([producto.id])
|
||||||
|
messages.success(request, "Imagen añadida correctamente.")
|
||||||
|
return redirect("gestionar_imagenes", id=producto.id)
|
||||||
|
|
||||||
|
return render(request, "tienda/gestionar_imagenes.html", {
|
||||||
|
"producto": producto,
|
||||||
|
"secondary_images": secondary_images,
|
||||||
|
"form": form
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def eliminar_imagen_secundaria(request: HttpRequest, product_id: int, image_id: int):
|
||||||
|
"""Elimina una imagen secundaria de un producto"""
|
||||||
|
if request.method != "POST":
|
||||||
|
messages.error(request, "Acción no permitida.")
|
||||||
|
return redirect("gestionar_imagenes", id=product_id)
|
||||||
|
|
||||||
|
producto = get_object_or_404(Product, id=product_id, creator=request.user)
|
||||||
|
image = get_object_or_404(Image, id=image_id)
|
||||||
|
|
||||||
|
if not producto.secondary_images.filter(id=image_id).exists():
|
||||||
|
messages.error(request, "Esta imagen no pertenece al producto.")
|
||||||
|
return redirect("gestionar_imagenes", id=product_id)
|
||||||
|
|
||||||
|
producto.secondary_images.remove(image)
|
||||||
|
image.delete()
|
||||||
|
_invalidate_product_cache([producto.id])
|
||||||
|
messages.success(request, "Imagen eliminada correctamente.")
|
||||||
|
return redirect("gestionar_imagenes", id=product_id)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def checkout(request: HttpRequest):
|
def checkout(request: HttpRequest):
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
@@ -2106,58 +2045,59 @@ def mis_recibos(request: HttpRequest):
|
|||||||
def editar_perfil(request: HttpRequest):
|
def editar_perfil(request: HttpRequest):
|
||||||
"""Edita la información del perfil del usuario"""
|
"""Edita la información del perfil del usuario"""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
first_name = request.POST.get("first_name", "").strip()
|
form = EditProfileForm(request.POST)
|
||||||
last_name = request.POST.get("last_name", "").strip()
|
if form.is_valid():
|
||||||
email = request.POST.get("email", "").strip()
|
email = form.cleaned_data["email"]
|
||||||
|
|
||||||
# Validar email único (excepto el propio)
|
|
||||||
if email != request.user.email and User.objects.filter(email=email).exists():
|
if email != request.user.email and User.objects.filter(email=email).exists():
|
||||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||||
return render(request, "tienda/editar_perfil.html")
|
return render(request, "tienda/editar_perfil.html", {"form": form})
|
||||||
|
|
||||||
# Actualizar usuario
|
request.user.first_name = form.cleaned_data["first_name"]
|
||||||
request.user.first_name = first_name
|
request.user.last_name = form.cleaned_data["last_name"]
|
||||||
request.user.last_name = last_name
|
|
||||||
request.user.email = email
|
request.user.email = email
|
||||||
request.user.save()
|
request.user.save()
|
||||||
|
|
||||||
messages.success(request, "Perfil actualizado correctamente.")
|
messages.success(request, "Perfil actualizado correctamente.")
|
||||||
return redirect("portal_usuario")
|
return redirect("portal_usuario")
|
||||||
|
else:
|
||||||
|
initial = {
|
||||||
|
"first_name": request.user.first_name,
|
||||||
|
"last_name": request.user.last_name,
|
||||||
|
"email": request.user.email,
|
||||||
|
}
|
||||||
|
form = EditProfileForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "tienda/editar_perfil.html")
|
return render(request, "tienda/editar_perfil.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def cambiar_contrasena(request: HttpRequest):
|
def cambiar_contrasena(request: HttpRequest):
|
||||||
"""Cambia la contraseña del usuario"""
|
"""Cambia la contraseña del usuario"""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
current_password = request.POST.get("current_password")
|
form = ChangePasswordForm(request.POST)
|
||||||
new_password = request.POST.get("new_password")
|
if form.is_valid():
|
||||||
confirm_password = request.POST.get("confirm_password")
|
current_password = form.cleaned_data["current_password"]
|
||||||
|
new_password = form.cleaned_data["new_password"]
|
||||||
|
|
||||||
# Verificar contraseña actual
|
|
||||||
if not request.user.check_password(current_password):
|
if not request.user.check_password(current_password):
|
||||||
messages.error(request, "La contraseña actual es incorrecta.")
|
messages.error(request, "La contraseña actual es incorrecta.")
|
||||||
return render(request, "tienda/editar_perfil.html")
|
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
||||||
|
|
||||||
# Validar nueva contraseña
|
|
||||||
if new_password != confirm_password:
|
|
||||||
messages.error(request, "Las contraseñas nuevas no coinciden.")
|
|
||||||
return render(request, "tienda/editar_perfil.html")
|
|
||||||
|
|
||||||
if len(new_password) < 8:
|
if len(new_password) < 8:
|
||||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||||
return render(request, "tienda/editar_perfil.html")
|
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
||||||
|
|
||||||
# Cambiar contraseña
|
|
||||||
request.user.set_password(new_password)
|
request.user.set_password(new_password)
|
||||||
request.user.save()
|
request.user.save()
|
||||||
|
|
||||||
# Mantener la sesión activa
|
|
||||||
auth_login(request, request.user)
|
auth_login(request, request.user)
|
||||||
|
|
||||||
messages.success(request, "Contraseña actualizada correctamente.")
|
messages.success(request, "Contraseña actualizada correctamente.")
|
||||||
return redirect("portal_usuario")
|
return redirect("portal_usuario")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Las contraseñas nuevas no coinciden o son inválidas.")
|
||||||
|
return render(request, "tienda/editar_perfil.html", {"password_form": form})
|
||||||
|
|
||||||
return redirect("editar_perfil")
|
return redirect("editar_perfil")
|
||||||
|
|
||||||
@@ -2176,45 +2116,39 @@ def direcciones_usuario(request: HttpRequest):
|
|||||||
def crear_direccion(request: HttpRequest):
|
def crear_direccion(request: HttpRequest):
|
||||||
"""Crea una nueva dirección de entrega"""
|
"""Crea una nueva dirección de entrega"""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
full_name = request.POST.get("full_name", "").strip()
|
form = ShippingAddressForm(request.POST)
|
||||||
address_line_1 = request.POST.get("address_line_1", "").strip()
|
if form.is_valid():
|
||||||
address_line_2 = request.POST.get("address_line_2", "").strip()
|
city = form.cleaned_data["city"]
|
||||||
city = request.POST.get("city", "").strip()
|
postal_code = form.cleaned_data["postal_code"]
|
||||||
postal_code = request.POST.get("postal_code", "").strip()
|
|
||||||
country = SHIPPING_COUNTRY
|
|
||||||
phone = request.POST.get("phone", "").strip()
|
|
||||||
is_default = request.POST.get("is_default") == "on"
|
|
||||||
|
|
||||||
# Validaciones
|
|
||||||
if not all([full_name, address_line_1, city, postal_code, phone]):
|
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
|
||||||
|
|
||||||
if not _is_almeria_city(city):
|
if not _is_almeria_city(city):
|
||||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||||
|
|
||||||
if not _is_almeria_postal_code(postal_code):
|
if not _is_almeria_postal_code(postal_code):
|
||||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||||
|
|
||||||
# Crear dirección
|
|
||||||
ShippingAddress.objects.create(
|
ShippingAddress.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
full_name=full_name,
|
full_name=form.cleaned_data["full_name"],
|
||||||
address_line_1=address_line_1,
|
address_line_1=form.cleaned_data["address_line_1"],
|
||||||
address_line_2=address_line_2,
|
address_line_2=form.cleaned_data.get("address_line_2", "") or "",
|
||||||
city=city,
|
city=city,
|
||||||
postal_code=postal_code,
|
postal_code=postal_code,
|
||||||
country=country,
|
country=SHIPPING_COUNTRY,
|
||||||
phone=phone,
|
phone=form.cleaned_data["phone"],
|
||||||
is_default=is_default
|
is_default=form.cleaned_data.get("is_default", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, "Dirección creada correctamente.")
|
messages.success(request, "Dirección creada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||||
|
else:
|
||||||
|
form = ShippingAddressForm()
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context())
|
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2223,34 +2157,47 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
direccion = get_object_or_404(ShippingAddress, id=id, user=request.user)
|
direccion = get_object_or_404(ShippingAddress, id=id, user=request.user)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
direccion.full_name = request.POST.get("full_name", "").strip()
|
form = ShippingAddressForm(request.POST)
|
||||||
direccion.address_line_1 = request.POST.get("address_line_1", "").strip()
|
if form.is_valid():
|
||||||
direccion.address_line_2 = request.POST.get("address_line_2", "").strip()
|
city = form.cleaned_data["city"]
|
||||||
direccion.city = request.POST.get("city", "").strip()
|
postal_code = form.cleaned_data["postal_code"]
|
||||||
direccion.postal_code = request.POST.get("postal_code", "").strip()
|
|
||||||
direccion.country = SHIPPING_COUNTRY
|
|
||||||
direccion.phone = request.POST.get("phone", "").strip()
|
|
||||||
direccion.is_default = request.POST.get("is_default") == "on"
|
|
||||||
|
|
||||||
# Validaciones
|
if not _is_almeria_city(city):
|
||||||
if not all([direccion.full_name, direccion.address_line_1, direccion.city,
|
messages.error(request, "El pueblo/ciudad debe pertenece a la provincia de Almería.")
|
||||||
direccion.postal_code, direccion.phone]):
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
|
||||||
|
|
||||||
if not _is_almeria_city(direccion.city):
|
if not _is_almeria_postal_code(postal_code):
|
||||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
|
||||||
|
|
||||||
if not _is_almeria_postal_code(direccion.postal_code):
|
|
||||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
|
direccion.full_name = form.cleaned_data["full_name"]
|
||||||
|
direccion.address_line_1 = form.cleaned_data["address_line_1"]
|
||||||
|
direccion.address_line_2 = form.cleaned_data.get("address_line_2", "") or ""
|
||||||
|
direccion.city = city
|
||||||
|
direccion.postal_code = postal_code
|
||||||
|
direccion.country = SHIPPING_COUNTRY
|
||||||
|
direccion.phone = form.cleaned_data["phone"]
|
||||||
|
direccion.is_default = form.cleaned_data.get("is_default", False)
|
||||||
|
|
||||||
direccion.save()
|
direccion.save()
|
||||||
messages.success(request, "Dirección actualizada correctamente.")
|
messages.success(request, "Dirección actualizada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||||
|
else:
|
||||||
|
initial = {
|
||||||
|
"full_name": direccion.full_name,
|
||||||
|
"address_line_1": direccion.address_line_1,
|
||||||
|
"address_line_2": direccion.address_line_2,
|
||||||
|
"city": direccion.city,
|
||||||
|
"postal_code": direccion.postal_code,
|
||||||
|
"country": direccion.country,
|
||||||
|
"phone": direccion.phone,
|
||||||
|
"is_default": direccion.is_default,
|
||||||
|
}
|
||||||
|
form = ShippingAddressForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2336,9 +2283,12 @@ def ayuda(request: HttpRequest):
|
|||||||
|
|
||||||
def reset_password(request: HttpRequest):
|
def reset_password(request: HttpRequest):
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render(request, "tienda/reset_password.html", {})
|
form = ResetPasswordForm()
|
||||||
|
return render(request, "tienda/reset_password.html", {"form": form})
|
||||||
else:
|
else:
|
||||||
tasks.enviar_correo_recuperacion.delay(request.POST["email"])
|
form = ResetPasswordForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
tasks.enviar_correo_recuperacion.delay(form.cleaned_data["email"])
|
||||||
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
||||||
return render(request, "tienda/index.html", {})
|
return render(request, "tienda/index.html", {})
|
||||||
|
|
||||||
@@ -2352,22 +2302,19 @@ def reset_password_phase2(request: HttpRequest, code: str):
|
|||||||
|
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render(request, "tienda/reset_password_phase2.html", {
|
form = ResetPasswordPhase2Form()
|
||||||
"code": code
|
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
|
||||||
})
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
password = request.POST["password"]
|
form = ResetPasswordPhase2Form(request.POST)
|
||||||
vpassword = request.POST["verify_password"]
|
if form.is_valid():
|
||||||
if password != vpassword:
|
|
||||||
messages.error(request, "Las contraseñas no coinciden")
|
|
||||||
return render(request, "tienda/reset_password_phase2.html", {"code": code})
|
|
||||||
|
|
||||||
user = ver_code.user
|
user = ver_code.user
|
||||||
user.set_password(password)
|
user.set_password(form.cleaned_data["password"])
|
||||||
user.save()
|
user.save()
|
||||||
ver_code.delete() # Delete Verification code after changing password
|
ver_code.delete()
|
||||||
messages.success(request, "Se ha cambiado la contraseña!")
|
messages.success(request, "Se ha cambiado la contraseña!")
|
||||||
return redirect(reverse("index"))
|
return redirect(reverse("index"))
|
||||||
|
else:
|
||||||
|
messages.error(request, "Las contraseñas no coinciden")
|
||||||
|
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
|
||||||
else:
|
else:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|||||||
Reference in New Issue
Block a user