Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72def373e3 | |||
| a50cadc873 | |||
| 551057b067 | |||
| ad7ddbe887 | |||
| d6b7cdfe6a | |||
| 56286c2fd9 | |||
| ba4f6ad65d | |||
| ed7041ae40 | |||
| fa948a98e2 | |||
| e8a5091dfd | |||
| a0ee6ecd14 | |||
| d6c9aa3db3 | |||
| 9751d19401 | |||
| cda9adb986 | |||
| e7e7fd118d | |||
| 132b1e1722 |
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"python.REPL.enableREPLSmartSend": false
|
||||
"python.REPL.enableREPLSmartSend": false,
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
@@ -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
|
||||
+10
-9
@@ -14,6 +14,7 @@ import logging
|
||||
import os, sys
|
||||
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
|
||||
|
||||
@@ -101,6 +102,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.forms',
|
||||
'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'
|
||||
@@ -429,4 +423,11 @@ CELERY_RESULT_SERIALIZER = 'json'
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
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
|
||||
asgiref==3.11.0
|
||||
asgiref==3.11.1
|
||||
billiard==4.2.4
|
||||
celery==5.6.2
|
||||
certifi==2026.1.4
|
||||
boto3==1.43.5
|
||||
botocore==1.43.5
|
||||
celery==5.6.3
|
||||
certifi==2026.4.22
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
charset-normalizer==3.4.7
|
||||
click==8.3.3
|
||||
click-didyoumean==0.3.1
|
||||
click-plugins==1.1.1.2
|
||||
click-repl==0.3.0
|
||||
cryptography==46.0.7
|
||||
Django==6.0.4
|
||||
cryptography==48.0.0
|
||||
defusedxml==0.7.1
|
||||
Django==6.0.5
|
||||
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-storages[boto3]==1.14.6
|
||||
gunicorn==25.1.0
|
||||
idna==3.11
|
||||
Jinja2==3.1.6
|
||||
fonttools==4.62.1
|
||||
fpdf2==2.8.7
|
||||
gunicorn==26.0.0
|
||||
idna==3.13
|
||||
|
||||
jmespath==1.1.0
|
||||
kombu==5.6.2
|
||||
MarkupSafe==3.0.3
|
||||
packaging==26.0
|
||||
packaging==26.2
|
||||
paypalrestsdk==1.13.3
|
||||
pillow==12.2.0
|
||||
boto3==1.42.97
|
||||
prompt_toolkit==3.0.52
|
||||
psycopg2-binary==2.9.12
|
||||
pycparser==3.0
|
||||
pyOpenSSL==26.0.0
|
||||
pyOpenSSL==26.2.0
|
||||
python-dateutil==2.9.0.post0
|
||||
rcssmin==1.2.2
|
||||
redis==5.2.1
|
||||
requests==2.33.0
|
||||
redis==7.4.0
|
||||
requests==2.33.1
|
||||
rjsmin==1.2.5
|
||||
s3transfer==0.17.0
|
||||
six==1.17.0
|
||||
sqlparse==0.5.5
|
||||
stripe==14.3.0
|
||||
stripe==15.1.0
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
tzdata==2026.2
|
||||
tzlocal==5.3.1
|
||||
urllib3==2.6.3
|
||||
vine==5.1.0
|
||||
wcwidth==0.6.0
|
||||
wcwidth==0.7.0
|
||||
whitenoise==6.12.0
|
||||
fpdf2==2.8.7
|
||||
psycopg2-binary==2.9.11
|
||||
+48
-21
@@ -3,6 +3,7 @@ from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem,
|
||||
# Register your models here.
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import path
|
||||
from django.contrib import messages
|
||||
from . import tasks
|
||||
|
||||
admin.site.register(Category)
|
||||
@@ -12,31 +13,57 @@ admin.site.register(VerificationCode)
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
search_fields = ('username',)
|
||||
|
||||
actions = ['banear_usuario_action', 'desbanear_usuario_action']
|
||||
def has_change_permission(self, request, obj = ...):
|
||||
return super().has_change_permission(request, obj)
|
||||
|
||||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
my_urls = [
|
||||
path("<int:pk>/ban/", self.admin_site.admin_view(self.ban_view)),
|
||||
]
|
||||
return my_urls + urls
|
||||
def banear_usuario_action(self, request, queryset):
|
||||
usuarios_baneados = 0
|
||||
for user in queryset:
|
||||
user: User = user
|
||||
# Desactiva usuario
|
||||
if user.registration_status == User.RegisterStatus.BANNED:
|
||||
continue
|
||||
|
||||
user.is_active = False
|
||||
user.registration_status = User.RegisterStatus.BANNED
|
||||
user.save()
|
||||
|
||||
# Enviar task a Worker
|
||||
tasks.banear_usuario.delay(user.email)
|
||||
|
||||
# Borrar productos
|
||||
Product.objects.filter(creator=user).delete()
|
||||
usuarios_baneados+=1
|
||||
self.message_user(
|
||||
request,
|
||||
f"Se ha(n) baneado {usuarios_baneados} usuario(s) correctamente.",
|
||||
level=messages.SUCCESS
|
||||
)
|
||||
def desbanear_usuario_action(self, request, queryset):
|
||||
user_desbaneados = 0
|
||||
for user in queryset:
|
||||
user: User = user
|
||||
if user.registration_status != User.RegisterStatus.BANNED:
|
||||
continue
|
||||
|
||||
user.is_active = True
|
||||
user.registration_status = User.RegisterStatus.ACTIVE
|
||||
user.save()
|
||||
|
||||
tasks.desbanear_usuario.delay(user.email)
|
||||
|
||||
user_desbaneados -= 1
|
||||
self.message_user(
|
||||
request,
|
||||
f"Se ha(n) desbaneado {user_desbaneados} usuario(s)",
|
||||
level=messages.SUCCESS
|
||||
)
|
||||
|
||||
|
||||
|
||||
def ban_view(self, request, pk):
|
||||
user = User.objects.get(pk=pk)
|
||||
|
||||
user.is_active = False # Desactivar cuenta de usuario
|
||||
user.save() # Guardar datos
|
||||
|
||||
tasks.banear_usuario.delay(user.email)
|
||||
|
||||
# Borrar productos
|
||||
productos = Product.objects.filter(user=user).all()
|
||||
for producto in productos:
|
||||
producto.delete()
|
||||
return redirect("admin")
|
||||
|
||||
banear_usuario_action.short_description = "Banear usuarios seleccionados"
|
||||
desbanear_usuario_action.short_description = "Desbanear usuarios seleccionados"
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
|
||||
+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),
|
||||
),
|
||||
]
|
||||
+2
-2
@@ -85,8 +85,8 @@ class Image(models.Model):
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=200, default="")
|
||||
sku = models.CharField(max_length=50, unique=True, blank=True, null=True)
|
||||
description = models.TextField(default = "")
|
||||
briefdesc = models.TextField(default = "")
|
||||
description = models.TextField(default = "", max_length=5000)
|
||||
briefdesc = models.TextField(default = "", max_length=250)
|
||||
price = models.FloatField(default = 0)
|
||||
stock = models.PositiveIntegerField(default=0)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||
|
||||
+13
-6
@@ -11,24 +11,32 @@ from .models import User, VerificationCode
|
||||
@shared_task
|
||||
def enviar_correo_bienvenida(email_usuario: str, nombre_usuario: str):
|
||||
html_content = render_to_string(
|
||||
'emails/welcome.html',
|
||||
'tienda/emails/welcome.html',
|
||||
{
|
||||
"name": nombre_usuario
|
||||
},
|
||||
using='jinja2'
|
||||
)
|
||||
send_hemail(email_usuario, "Inicio de Sesión correcto", html_content, "Has iniciado sesión...")
|
||||
|
||||
@shared_task
|
||||
def banear_usuario(email_usuario: str):
|
||||
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...")
|
||||
|
||||
@shared_task
|
||||
def desbanear_usuario(email_usuario: str):
|
||||
html_content = render_to_string(
|
||||
'tienda/emails/unban.html',
|
||||
{},
|
||||
)
|
||||
|
||||
send_hemail(email_usuario, "Cuenta Desbloqueada", html_content, "Tu cuenta ha sido desbloqueada...")
|
||||
|
||||
@shared_task
|
||||
def enviar_correo_confirmacion(id: int):
|
||||
usuario = User.objects.get(id=id)
|
||||
@@ -56,14 +64,13 @@ def enviar_correo_recuperacion(email: str):
|
||||
)
|
||||
ver_code.save()
|
||||
html_content = render_to_string(
|
||||
'emails/reset_pass.html',
|
||||
'tienda/emails/reset_pass.html',
|
||||
{
|
||||
"name": usuario.get_full_name(),
|
||||
"domain": settings.DOMAIN,
|
||||
"protocol": settings.PROTOCOL,
|
||||
"code": ver_code.code
|
||||
},
|
||||
using='jinja2'
|
||||
)
|
||||
|
||||
send_hemail(email, "Reset de Contraseña", html_content, "Estas reseteando la contraseña...")
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{% extends "admin/submit_line.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
{% block submit-row %}
|
||||
<a href="../ban/" class="closelink" style="background:red;">BAN USER</a>
|
||||
{% endblock %}
|
||||
@@ -13,74 +13,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{{ form }}
|
||||
<!-- Botones -->
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="{% url 'mis_productos' %}" class="btn btn-secondary">Cancelar</a>
|
||||
|
||||
@@ -21,52 +21,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
<div class="d-flex gap-2">
|
||||
<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>
|
||||
@@ -80,7 +35,6 @@
|
||||
<script>
|
||||
(function () {
|
||||
const cityInput = document.getElementById('city');
|
||||
const cityValidationMessage = document.getElementById('city-validation-message');
|
||||
const form = cityInput ? cityInput.form : null;
|
||||
|
||||
if (!cityInput || !form) {
|
||||
@@ -123,8 +77,6 @@
|
||||
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
||||
cityInput.classList.add('is-invalid');
|
||||
}
|
||||
|
||||
cityValidationMessage.textContent = cityInput.validationMessage || 'El pueblo/ciudad debe pertenecer a la provincia de Almería.';
|
||||
}
|
||||
|
||||
cityInput.addEventListener('input', validateTown);
|
||||
|
||||
@@ -37,18 +37,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nombre de Usuario</label>
|
||||
<input type="text" class="form-control" id="username" value="{{ user.username }}" disabled>
|
||||
@@ -69,19 +58,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{% url 'cambiar_contrasena' %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ password_form.as_p }}
|
||||
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,67 +13,9 @@
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- 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"
|
||||
value="{{ producto.name }}" placeholder="Ej: iPhone 15 Pro Max">
|
||||
</div>
|
||||
{{ form.as_p }}
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- Imágenes secundarias (no incluidas en el form) -->
|
||||
<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"
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
+1
-1
@@ -18,4 +18,4 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
+1
-1
@@ -24,4 +24,4 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
@@ -0,0 +1,27 @@
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||
<tr>
|
||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido desbloqueada</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px">
|
||||
<svg fill="#FF0000" width="128px" height="128px" viewBox="-3.2 -3.2 38.40 38.40" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke="#FF0000" stroke-width="0.00032"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(6.4,6.4), scale(0.6)"><rect x="-3.2" y="-3.2" width="38.40" height="38.40" rx="19.2" fill="#1a5fb4" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.152"> <title>alert</title> <path d="M14.611 18.856c-0.346 0.352-0.52 0.782-0.52 1.292 0 0.551 0.197 1.014 0.59 1.389 0.363 0.346 0.799 0.519 1.309 0.519 0.521 0 0.971-0.188 1.346-0.566s0.562-0.828 0.562-1.35c0-0.504-0.182-0.943-0.545-1.318-0.363-0.381-0.801-0.571-1.311-0.571-0.567-0.001-1.044 0.201-1.431 0.605v0zM14.391 10.788c-0.299 0.451-0.447 1.011-0.447 1.679 0 0.545 0.092 1.146 0.276 1.802s0.435 1.271 0.751 1.846c0.428 0.779 0.76 1.169 0.994 1.169 0.24 0 0.557-0.305 0.949-0.914 0.346-0.539 0.622-1.152 0.83-1.841s0.312-1.332 0.312-1.93c0-0.902-0.244-1.6-0.73-2.092-0.363-0.375-0.805-0.563-1.326-0.563-0.703 0-1.24 0.282-1.609 0.844v0z"></path> </g><g id="SVGRepo_iconCarrier"> <title>alert</title> <path d="M14.611 18.856c-0.346 0.352-0.52 0.782-0.52 1.292 0 0.551 0.197 1.014 0.59 1.389 0.363 0.346 0.799 0.519 1.309 0.519 0.521 0 0.971-0.188 1.346-0.566s0.562-0.828 0.562-1.35c0-0.504-0.182-0.943-0.545-1.318-0.363-0.381-0.801-0.571-1.311-0.571-0.567-0.001-1.044 0.201-1.431 0.605v0zM14.391 10.788c-0.299 0.451-0.447 1.011-0.447 1.679 0 0.545 0.092 1.146 0.276 1.802s0.435 1.271 0.751 1.846c0.428 0.779 0.76 1.169 0.994 1.169 0.24 0 0.557-0.305 0.949-0.914 0.346-0.539 0.622-1.152 0.83-1.841s0.312-1.332 0.312-1.93c0-0.902-0.244-1.6-0.73-2.092-0.363-0.375-0.805-0.563-1.326-0.563-0.703 0-1.24 0.282-1.609 0.844v0z"></path> </g></svg>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
||||
<p>Hemos aceptado la apelación del previo baneo de su cuenta</p>
|
||||
<p>Su cuenta ha sido desbloqueada y ya puede entrar, pero los productos seguirán eliminados, por lo que deberá recrearlos para seguir vendiendolos</p>
|
||||
<p>Muchas gracias por su paciencia</p>
|
||||
<p></p>
|
||||
<p style="color: gray;">Este email ha sido enviado automaticamente, no responda a este correo.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -23,4 +23,4 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
@@ -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' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<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="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>
|
||||
{{ form }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<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">
|
||||
<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>
|
||||
<form method="POST" action="{% url 'borrar_producto' producto.id %}" onsubmit="return confirm('¿Seguro que quieres borrar este producto?');">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -12,33 +12,7 @@
|
||||
<form method="post" action="{% url 'register' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ form }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Crear Cuenta</button>
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'reset_password' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||
|
||||
@@ -11,16 +11,7 @@
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'reset_password_phase2' code %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||
|
||||
+181
@@ -14,6 +14,7 @@ from .models import (
|
||||
StockReservation, StockReservationItem, Cart, CartItem,
|
||||
Order, OrderItem, OrderMessage, SavedPaymentMethod, ShippingAddress
|
||||
)
|
||||
from .forms import UserRegisterForm, UserLoginForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
||||
import string
|
||||
import random
|
||||
@@ -22,6 +23,185 @@ import random
|
||||
# ==================== USER MODEL TESTS ====================
|
||||
class UserModelTests(TestCase):
|
||||
"""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):
|
||||
self.user_data = {
|
||||
@@ -1455,6 +1635,7 @@ class EndpointViewTests(TestCase):
|
||||
"email": "nuevo@example.com",
|
||||
"password": self.password,
|
||||
"password_confirm": self.password,
|
||||
"terms": "on",
|
||||
})
|
||||
self.assertEqual(register_response.status_code, 302)
|
||||
confirm_delay.assert_called_once()
|
||||
|
||||
@@ -18,6 +18,8 @@ urlpatterns = [
|
||||
path("venta/crear-producto/", views.crear_producto, name="crear_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/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
|
||||
path("cart/", views.view_cart, name="view_cart"),
|
||||
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
||||
|
||||
+387
-417
@@ -1,10 +1,11 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
|
||||
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
|
||||
|
||||
from django.db.utils import DataError
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
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 .vars import (
|
||||
PAGE_SIZE,
|
||||
@@ -15,7 +16,7 @@ from .vars import (
|
||||
STOCK_RESERVATION_MINUTES,
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt, csrf_protect
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
@@ -86,9 +87,10 @@ def _is_almeria_city(city: str) -> bool:
|
||||
return _normalize_location_text(city) in ALMERIA_MUNICIPALITIES
|
||||
|
||||
|
||||
def _address_form_context(direccion=None):
|
||||
def _address_form_context(direccion=None, form=None):
|
||||
return {
|
||||
"direccion": direccion,
|
||||
"form": form,
|
||||
"almeria_municipalities": ALMERIA_MUNICIPALITIES_DISPLAY,
|
||||
}
|
||||
|
||||
@@ -221,144 +223,144 @@ def index(request: HttpRequest):
|
||||
|
||||
def login(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
email = request.POST.get("email")
|
||||
password = request.POST.get("password")
|
||||
remember = request.POST.get("remember")
|
||||
client_ip = _get_client_ip(request)
|
||||
|
||||
# Buscar usuario por email
|
||||
try:
|
||||
user_obj = User.objects.get(email=email)
|
||||
username = user_obj.username
|
||||
except User.DoesNotExist:
|
||||
audit_logger.warning(
|
||||
"LOGIN_FAILED email=%s reason=user_not_found ip=%s",
|
||||
email,
|
||||
client_ip,
|
||||
)
|
||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||
return render(request, "tienda/login.html")
|
||||
|
||||
# Autenticar usuario
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is None:
|
||||
data: str = cache.get(f"tries_login_{username}")
|
||||
logins: int
|
||||
if data is None:
|
||||
logins = 0
|
||||
else:
|
||||
logins = int(data)
|
||||
form: UserLoginForm = UserLoginForm(request.POST)
|
||||
if form.is_valid():
|
||||
email: str = form.cleaned_data["email"]
|
||||
password: str = form.cleaned_data["password"]
|
||||
remember: bool = form.cleaned_data["remember"]
|
||||
client_ip = _get_client_ip(request)
|
||||
try:
|
||||
user: User = User.objects.get(email=email)
|
||||
username = user.username
|
||||
except User.DoesNotExist:
|
||||
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", email, client_ip)
|
||||
messages.error(request, "El email o la contraseña es incorrecta")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
if user.registration_status == User.RegisterStatus.BANNED:
|
||||
# Usuario baneado.
|
||||
messages.error(request, "Esta cuenta esta bloqueada.")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
|
||||
if logins >= 5:
|
||||
# Si ha fallado 5 intentos de login...
|
||||
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")
|
||||
return render(request, "tienda/login.html")
|
||||
|
||||
logins+=1
|
||||
cache.set(f"tries_login_{username}", str(logins), 600)
|
||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||
return render(request, "tienda/login.html")
|
||||
user = User.objects.get(username=user.username)
|
||||
if user.registration_status == "CR":
|
||||
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")
|
||||
return render(request, "tienda/login.html")
|
||||
|
||||
if user is not None:
|
||||
user = authenticate(request, username = username, password=password)
|
||||
|
||||
if user is None:
|
||||
data: str = cache.get(f"tries_login_{username}")
|
||||
logins: int
|
||||
if data is None:
|
||||
logins = 0
|
||||
else:
|
||||
logins = int(data)
|
||||
|
||||
if logins >= 5:
|
||||
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", email)
|
||||
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
logins+=1
|
||||
cache.set(f"tries_login_{username}", str(logins), 600)
|
||||
messages.error(request, "El email o la contraseña es incorrecta")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
if user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
||||
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")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
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)))
|
||||
request.session.set_expiry(1209600)
|
||||
|
||||
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, f"{user.first_name} {user.last_name}")
|
||||
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
||||
return redirect("index")
|
||||
else:
|
||||
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")
|
||||
else:
|
||||
form = UserLoginForm()
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
|
||||
#
|
||||
# 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):
|
||||
if request.user.is_authenticated:
|
||||
return redirect("index")
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("name")
|
||||
email = request.POST.get("email")
|
||||
password = request.POST.get("password")
|
||||
password_confirm = request.POST.get("password_confirm")
|
||||
client_ip = _get_client_ip(request)
|
||||
|
||||
# Validaciones
|
||||
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():
|
||||
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.")
|
||||
return render(request, "tienda/register.html")
|
||||
|
||||
# Crear username a partir del email
|
||||
username = email.split("@")[0]
|
||||
|
||||
# Si el username ya existe, agregar un número
|
||||
base_username = username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
|
||||
# Crear usuario
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
first_name=name
|
||||
)
|
||||
form = UserRegisterForm(request.POST)
|
||||
if form.is_valid():
|
||||
name = form.cleaned_data.get("name")
|
||||
email = form.cleaned_data.get("email")
|
||||
password = form.cleaned_data.get("password")
|
||||
client_ip = _get_client_ip(request)
|
||||
|
||||
audit_logger.info(
|
||||
"REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s",
|
||||
user.id,
|
||||
user.username,
|
||||
user.email,
|
||||
client_ip,
|
||||
)
|
||||
# Validación email
|
||||
if User.objects.filter(email=email).exists():
|
||||
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")
|
||||
return render(request, "tienda/register.html", {"form":form})
|
||||
|
||||
username = email.split("@")[0]
|
||||
base_username = username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
user = User.objects.create_user(
|
||||
username = username,
|
||||
email = email,
|
||||
password = password,
|
||||
first_name = name
|
||||
)
|
||||
audit_logger.info(
|
||||
"REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s",
|
||||
user.id,
|
||||
user.username,
|
||||
user.email,
|
||||
client_ip,
|
||||
)
|
||||
|
||||
|
||||
tasks.enviar_correo_confirmacion.delay(user.id)
|
||||
messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.")
|
||||
return redirect("index")
|
||||
|
||||
return render(request, "tienda/register.html")
|
||||
tasks.enviar_correo_confirmacion.delay(user.id)
|
||||
messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.")
|
||||
return redirect("index")
|
||||
else:
|
||||
form = UserRegisterForm()
|
||||
return render(request, "tienda/register.html", {"form":form})
|
||||
|
||||
|
||||
def logout(request: HttpRequest):
|
||||
@@ -948,88 +950,32 @@ def enviar_mensaje_pedido(request: HttpRequest, item_id: int):
|
||||
|
||||
@login_required
|
||||
def crear_producto(request: HttpRequest):
|
||||
"""Crea un nuevo producto"""
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("name")
|
||||
briefdesc = request.POST.get("briefdesc")
|
||||
description = request.POST.get("description")
|
||||
price = request.POST.get("price")
|
||||
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:
|
||||
primary_image = Image.objects.create(
|
||||
name=f"{name}_principal",
|
||||
image=primary_image_file
|
||||
)
|
||||
if stock > 4294967295:
|
||||
messages.error(request, "No se puede tener mas de 4294967295 existencias. Por favor, intentelo de nuevo")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
# Crear producto
|
||||
producto = Product.objects.create(
|
||||
name=name,
|
||||
briefdesc=briefdesc or "",
|
||||
description=description,
|
||||
price=price,
|
||||
stock=stock,
|
||||
category=category,
|
||||
primary_image=primary_image,
|
||||
creator=request.user
|
||||
)
|
||||
_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
|
||||
form = ProductForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
primary_image_file = form.cleaned_data.get("primary_image")
|
||||
image = None
|
||||
if primary_image_file:
|
||||
image = Image(
|
||||
name = f"{form.cleaned_data['name']}_principal",
|
||||
image = primary_image_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})
|
||||
|
||||
image.save()
|
||||
producto: Product = Product(
|
||||
name = form.cleaned_data["name"],
|
||||
briefdesc = form.cleaned_data["briefdesc"],
|
||||
description = form.cleaned_data["description"],
|
||||
price = form.cleaned_data["price"],
|
||||
stock = form.cleaned_data["stock"],
|
||||
category = form.cleaned_data["category"],
|
||||
primary_image = image,
|
||||
creator = request.user
|
||||
)
|
||||
producto.save()
|
||||
return redirect("/")
|
||||
else:
|
||||
form = ProductForm()
|
||||
return render(request, "tienda/crear_producto.html", {"form":form})
|
||||
|
||||
@login_required
|
||||
def editar_producto(request: HttpRequest, id: int):
|
||||
@@ -1037,96 +983,55 @@ def editar_producto(request: HttpRequest, id: int):
|
||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("name")
|
||||
briefdesc = request.POST.get("briefdesc")
|
||||
description = request.POST.get("description")
|
||||
price = request.POST.get("price")
|
||||
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")
|
||||
form = ProductEditForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
producto.name = form.cleaned_data["name"]
|
||||
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
||||
producto.description = form.cleaned_data["description"]
|
||||
producto.price = form.cleaned_data["price"]
|
||||
producto.stock = form.cleaned_data["stock"]
|
||||
producto.category = form.cleaned_data["category"]
|
||||
|
||||
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
|
||||
})
|
||||
primary_image_file = request.FILES.get("primary_image")
|
||||
secondary_images_files = request.FILES.getlist("secondary_images")
|
||||
|
||||
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:
|
||||
primary_image = Image.objects.create(
|
||||
name=f"{name}_principal",
|
||||
image=primary_image_file
|
||||
)
|
||||
producto.primary_image = primary_image
|
||||
|
||||
producto.save()
|
||||
_invalidate_product_cache([producto.id])
|
||||
|
||||
if secondary_images_files:
|
||||
producto.secondary_images.clear()
|
||||
for idx, img_file in enumerate(secondary_images_files):
|
||||
secondary_img = Image.objects.create(
|
||||
name=f"{name}_secundaria_{idx+1}",
|
||||
image=img_file
|
||||
if primary_image_file:
|
||||
primary_image = Image.objects.create(
|
||||
name=f"{producto.name}_principal",
|
||||
image=primary_image_file
|
||||
)
|
||||
producto.secondary_images.add(secondary_img)
|
||||
producto.primary_image = primary_image
|
||||
|
||||
messages.success(request, f"¡Producto '{name}' actualizado exitosamente!")
|
||||
return redirect("mis_productos")
|
||||
producto.save()
|
||||
_invalidate_product_cache([producto.id])
|
||||
|
||||
if secondary_images_files:
|
||||
producto.secondary_images.clear()
|
||||
for idx, img_file in enumerate(secondary_images_files):
|
||||
secondary_img = Image.objects.create(
|
||||
name=f"{producto.name}_secundaria_{idx+1}",
|
||||
image=img_file
|
||||
)
|
||||
producto.secondary_images.add(secondary_img)
|
||||
|
||||
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
||||
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()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"form": form,
|
||||
"producto": producto
|
||||
})
|
||||
|
||||
@@ -1145,6 +1050,55 @@ def borrar_producto(request: HttpRequest, id: int):
|
||||
messages.success(request, f"Producto '{nombre}' eliminado correctamente.")
|
||||
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
|
||||
def checkout(request: HttpRequest):
|
||||
cart = get_or_create_cart(request)
|
||||
@@ -1176,7 +1130,7 @@ def stripe_config(request):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@csrf_protect
|
||||
def create_checkout_session(request: HttpRequest):
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
@@ -1515,6 +1469,7 @@ def paypal_execute(request: HttpRequest):
|
||||
# ==================== STRIPE PAYMENT INTENTS ====================
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_payment_intent(request: HttpRequest):
|
||||
"""
|
||||
Crea un Stripe PaymentIntent para el carrito actual.
|
||||
@@ -1598,6 +1553,7 @@ def crear_payment_intent(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def confirmar_pago_tarjeta(request: HttpRequest):
|
||||
"""
|
||||
Verificar que el PaymentIntent fue exitoso y crear el pedido.
|
||||
@@ -1671,6 +1627,7 @@ def confirmar_pago_tarjeta(request: HttpRequest):
|
||||
# ==================== PAYPAL ORDERS API ====================
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_orden_paypal(request: HttpRequest):
|
||||
"""
|
||||
Crea una orden de PayPal con el total del carrito actual (Orders API v2).
|
||||
@@ -1724,6 +1681,7 @@ def crear_orden_paypal(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def capturar_orden_paypal(request: HttpRequest):
|
||||
"""
|
||||
Captura una orden de PayPal aprobada y crea el pedido en nuestra BD.
|
||||
@@ -1834,6 +1792,7 @@ def agregar_tarjeta(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_setup_intent(request: HttpRequest):
|
||||
"""
|
||||
Crea un Stripe SetupIntent y retorna el client_secret para que el frontend
|
||||
@@ -1858,6 +1817,7 @@ def crear_setup_intent(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def confirmar_setup_intent(request: HttpRequest):
|
||||
"""
|
||||
Tras la confirmación del SetupIntent en el frontend, guarda la tarjeta.
|
||||
@@ -1959,6 +1919,7 @@ def agregar_paypal(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_orden_paypal_setup(request: HttpRequest):
|
||||
"""
|
||||
Crea una orden PayPal de 0.01 € para verificar/guardar la cuenta.
|
||||
@@ -1975,6 +1936,7 @@ def crear_orden_paypal_setup(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def capturar_orden_paypal_setup(request: HttpRequest):
|
||||
"""
|
||||
Captura la orden de verificación de PayPal y guarda la cuenta del usuario.
|
||||
@@ -2083,58 +2045,59 @@ def mis_recibos(request: HttpRequest):
|
||||
def editar_perfil(request: HttpRequest):
|
||||
"""Edita la información del perfil del usuario"""
|
||||
if request.method == "POST":
|
||||
first_name = request.POST.get("first_name", "").strip()
|
||||
last_name = request.POST.get("last_name", "").strip()
|
||||
email = request.POST.get("email", "").strip()
|
||||
|
||||
# Validar email único (excepto el propio)
|
||||
if email != request.user.email and User.objects.filter(email=email).exists():
|
||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
|
||||
# Actualizar usuario
|
||||
request.user.first_name = first_name
|
||||
request.user.last_name = last_name
|
||||
request.user.email = email
|
||||
request.user.save()
|
||||
|
||||
messages.success(request, "Perfil actualizado correctamente.")
|
||||
return redirect("portal_usuario")
|
||||
form = EditProfileForm(request.POST)
|
||||
if form.is_valid():
|
||||
email = form.cleaned_data["email"]
|
||||
|
||||
if email != request.user.email and User.objects.filter(email=email).exists():
|
||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
||||
|
||||
request.user.first_name = form.cleaned_data["first_name"]
|
||||
request.user.last_name = form.cleaned_data["last_name"]
|
||||
request.user.email = email
|
||||
request.user.save()
|
||||
|
||||
messages.success(request, "Perfil actualizado correctamente.")
|
||||
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
|
||||
def cambiar_contrasena(request: HttpRequest):
|
||||
"""Cambia la contraseña del usuario"""
|
||||
if request.method == "POST":
|
||||
current_password = request.POST.get("current_password")
|
||||
new_password = request.POST.get("new_password")
|
||||
confirm_password = request.POST.get("confirm_password")
|
||||
|
||||
# Verificar contraseña actual
|
||||
if not request.user.check_password(current_password):
|
||||
messages.error(request, "La contraseña actual es incorrecta.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
|
||||
# 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:
|
||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
|
||||
# Cambiar contraseña
|
||||
request.user.set_password(new_password)
|
||||
request.user.save()
|
||||
|
||||
# Mantener la sesión activa
|
||||
auth_login(request, request.user)
|
||||
|
||||
messages.success(request, "Contraseña actualizada correctamente.")
|
||||
return redirect("portal_usuario")
|
||||
form = ChangePasswordForm(request.POST)
|
||||
if form.is_valid():
|
||||
current_password = form.cleaned_data["current_password"]
|
||||
new_password = form.cleaned_data["new_password"]
|
||||
|
||||
if not request.user.check_password(current_password):
|
||||
messages.error(request, "La contraseña actual es incorrecta.")
|
||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
||||
|
||||
if len(new_password) < 8:
|
||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
||||
|
||||
request.user.set_password(new_password)
|
||||
request.user.save()
|
||||
|
||||
auth_login(request, request.user)
|
||||
|
||||
messages.success(request, "Contraseña actualizada correctamente.")
|
||||
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")
|
||||
|
||||
@@ -2153,45 +2116,39 @@ def direcciones_usuario(request: HttpRequest):
|
||||
def crear_direccion(request: HttpRequest):
|
||||
"""Crea una nueva dirección de entrega"""
|
||||
if request.method == "POST":
|
||||
full_name = request.POST.get("full_name", "").strip()
|
||||
address_line_1 = request.POST.get("address_line_1", "").strip()
|
||||
address_line_2 = request.POST.get("address_line_2", "").strip()
|
||||
city = request.POST.get("city", "").strip()
|
||||
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]):
|
||||
form = ShippingAddressForm(request.POST)
|
||||
if form.is_valid():
|
||||
city = form.cleaned_data["city"]
|
||||
postal_code = form.cleaned_data["postal_code"]
|
||||
|
||||
if not _is_almeria_city(city):
|
||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||
|
||||
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).")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||
|
||||
ShippingAddress.objects.create(
|
||||
user=request.user,
|
||||
full_name=form.cleaned_data["full_name"],
|
||||
address_line_1=form.cleaned_data["address_line_1"],
|
||||
address_line_2=form.cleaned_data.get("address_line_2", "") or "",
|
||||
city=city,
|
||||
postal_code=postal_code,
|
||||
country=SHIPPING_COUNTRY,
|
||||
phone=form.cleaned_data["phone"],
|
||||
is_default=form.cleaned_data.get("is_default", False)
|
||||
)
|
||||
|
||||
messages.success(request, "Dirección creada correctamente.")
|
||||
return redirect("direcciones_usuario")
|
||||
else:
|
||||
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):
|
||||
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))
|
||||
|
||||
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).")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||
|
||||
# Crear dirección
|
||||
ShippingAddress.objects.create(
|
||||
user=request.user,
|
||||
full_name=full_name,
|
||||
address_line_1=address_line_1,
|
||||
address_line_2=address_line_2,
|
||||
city=city,
|
||||
postal_code=postal_code,
|
||||
country=country,
|
||||
phone=phone,
|
||||
is_default=is_default
|
||||
)
|
||||
|
||||
messages.success(request, "Dirección creada correctamente.")
|
||||
return redirect("direcciones_usuario")
|
||||
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
|
||||
@@ -2200,34 +2157,47 @@ def editar_direccion(request: HttpRequest, id: int):
|
||||
direccion = get_object_or_404(ShippingAddress, id=id, user=request.user)
|
||||
|
||||
if request.method == "POST":
|
||||
direccion.full_name = request.POST.get("full_name", "").strip()
|
||||
direccion.address_line_1 = request.POST.get("address_line_1", "").strip()
|
||||
direccion.address_line_2 = request.POST.get("address_line_2", "").strip()
|
||||
direccion.city = request.POST.get("city", "").strip()
|
||||
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 all([direccion.full_name, direccion.address_line_1, direccion.city,
|
||||
direccion.postal_code, direccion.phone]):
|
||||
form = ShippingAddressForm(request.POST)
|
||||
if form.is_valid():
|
||||
city = form.cleaned_data["city"]
|
||||
postal_code = form.cleaned_data["postal_code"]
|
||||
|
||||
if not _is_almeria_city(city):
|
||||
messages.error(request, "El pueblo/ciudad debe pertenece a la provincia de Almería.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||
|
||||
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).")
|
||||
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()
|
||||
messages.success(request, "Dirección actualizada correctamente.")
|
||||
return redirect("direcciones_usuario")
|
||||
else:
|
||||
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):
|
||||
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).")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||
|
||||
direccion.save()
|
||||
messages.success(request, "Dirección actualizada correctamente.")
|
||||
return redirect("direcciones_usuario")
|
||||
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
|
||||
@@ -2313,10 +2283,13 @@ def ayuda(request: HttpRequest):
|
||||
|
||||
def reset_password(request: HttpRequest):
|
||||
if request.method == "GET":
|
||||
return render(request, "tienda/reset_password.html", {})
|
||||
form = ResetPasswordForm()
|
||||
return render(request, "tienda/reset_password.html", {"form": form})
|
||||
else:
|
||||
tasks.enviar_correo_recuperacion.delay(request.POST["email"])
|
||||
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
||||
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")
|
||||
return render(request, "tienda/index.html", {})
|
||||
|
||||
def reset_password_phase2(request: HttpRequest, code: str):
|
||||
@@ -2329,22 +2302,19 @@ def reset_password_phase2(request: HttpRequest, code: str):
|
||||
|
||||
|
||||
if request.method == "GET":
|
||||
return render(request, "tienda/reset_password_phase2.html", {
|
||||
"code": code
|
||||
})
|
||||
form = ResetPasswordPhase2Form()
|
||||
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
|
||||
elif request.method == "POST":
|
||||
password = request.POST["password"]
|
||||
vpassword = request.POST["verify_password"]
|
||||
if password != vpassword:
|
||||
form = ResetPasswordPhase2Form(request.POST)
|
||||
if form.is_valid():
|
||||
user = ver_code.user
|
||||
user.set_password(form.cleaned_data["password"])
|
||||
user.save()
|
||||
ver_code.delete()
|
||||
messages.success(request, "Se ha cambiado la contraseña!")
|
||||
return redirect(reverse("index"))
|
||||
else:
|
||||
messages.error(request, "Las contraseñas no coinciden")
|
||||
return render(request, "tienda/reset_password_phase2.html", {"code": code})
|
||||
|
||||
user = ver_code.user
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
ver_code.delete() # Delete Verification code after changing password
|
||||
messages.success(request, "Se ha cambiado la contraseña!")
|
||||
return redirect(reverse("index"))
|
||||
|
||||
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
|
||||
else:
|
||||
raise Http404()
|
||||
|
||||
Reference in New Issue
Block a user