Merge pull request #99 from dsaub/fix/sonar-critical-issues
fix: resolver 12 issues CRITICAL de SonarQube Cloud
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import FileExtensionValidator, MinLengthValidator, MaxLengthValidator
|
from django.core.validators import FileExtensionValidator, MinLengthValidator, MaxLengthValidator
|
||||||
from .models import Category
|
from .models import Category
|
||||||
from .constants import *
|
from .constants import IMAGE_TYPE, EMAIL_FORMNAME, INCORRECT_PASSWORDS
|
||||||
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
||||||
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
||||||
|
|
||||||
|
|||||||
+175
-119
@@ -42,6 +42,16 @@ audit_logger = logging.getLogger("tienda.audit")
|
|||||||
STOCK_RESERVATION_SESSION_KEY = "stock_reservation_id"
|
STOCK_RESERVATION_SESSION_KEY = "stock_reservation_id"
|
||||||
STOCK_RESERVATION_PAYMENT_SESSION_KEY = "stock_reservation_payment_method"
|
STOCK_RESERVATION_PAYMENT_SESSION_KEY = "stock_reservation_payment_method"
|
||||||
|
|
||||||
|
# Constantes para plantillas y mensajes reutilizados
|
||||||
|
LOGIN_TEMPLATE = LOGIN_TEMPLATE
|
||||||
|
INDEX_TEMPLATE = INDEX_TEMPLATE
|
||||||
|
EDITAR_PERFIL_TEMPLATE = EDITAR_PERFIL_TEMPLATE
|
||||||
|
EDITAR_DIRECCION_TEMPLATE = EDITAR_DIRECCION_TEMPLATE
|
||||||
|
MSG_CAMPOS_OBLIGATORIOS = MSG_CAMPOS_OBLIGATORIOS
|
||||||
|
MSG_DIRECCION_INVALIDA = MSG_DIRECCION_INVALIDA
|
||||||
|
MSG_CARRITO_VACIO = MSG_CARRITO_VACIO
|
||||||
|
MSG_CUERPO_INVALIDO = MSG_CUERPO_INVALIDO
|
||||||
|
|
||||||
|
|
||||||
def _mask_email(email: str) -> str:
|
def _mask_email(email: str) -> str:
|
||||||
if not email or '@' not in email:
|
if not email or '@' not in email:
|
||||||
@@ -232,65 +242,80 @@ def index(request: HttpRequest):
|
|||||||
|
|
||||||
products = Product.objects.all()[start:end]
|
products = Product.objects.all()[start:end]
|
||||||
categorias = Category.objects.all()
|
categorias = Category.objects.all()
|
||||||
return render(request, "tienda/index.html", {"products": products, "categories": categorias})
|
return render(request, INDEX_TEMPLATE, {"products": products, "categories": categorias})
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
def _try_get_user(email: str):
|
||||||
def login(request: HttpRequest):
|
"""Intenta obtener un usuario por email, retorna (user, username) o (None, None)."""
|
||||||
if request.method == "POST":
|
|
||||||
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:
|
try:
|
||||||
user: User = User.objects.get(email=email)
|
user = User.objects.get(email=email)
|
||||||
username = user.username
|
return user, user.username
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", _mask_email(email), client_ip)
|
return None, None
|
||||||
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})
|
|
||||||
|
|
||||||
user = authenticate(request, username = username, password=password)
|
|
||||||
|
|
||||||
if user is None:
|
def _handle_login_rate_limit(request, username, email):
|
||||||
data: str = cache.get(f"tries_login_{username}")
|
"""Verifica rate limit de intentos de login. Retorna True si está rate-limited."""
|
||||||
logins: int
|
data = cache.get(f"tries_login_{username}")
|
||||||
if data is None:
|
logins = 0 if data is None else int(data)
|
||||||
logins = 0
|
|
||||||
else:
|
|
||||||
logins = int(data)
|
|
||||||
|
|
||||||
if logins >= 5:
|
if logins >= 5:
|
||||||
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", _mask_email(email))
|
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", _mask_email(email))
|
||||||
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
return True
|
||||||
logins+=1
|
|
||||||
|
logins += 1
|
||||||
cache.set(f"tries_login_{username}", str(logins), 600)
|
cache.set(f"tries_login_{username}", str(logins), 600)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def login(request: HttpRequest):
|
||||||
|
if request.method != "POST":
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": UserLoginForm()})
|
||||||
|
|
||||||
|
form = UserLoginForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
email = form.cleaned_data["email"]
|
||||||
|
password = form.cleaned_data["password"]
|
||||||
|
remember = form.cleaned_data["remember"]
|
||||||
|
client_ip = _get_client_ip(request)
|
||||||
|
|
||||||
|
user, username = _try_get_user(email)
|
||||||
|
if user is None:
|
||||||
|
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", _mask_email(email), client_ip)
|
||||||
messages.error(request, "El email o la contraseña es incorrecta")
|
messages.error(request, "El email o la contraseña es incorrecta")
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
if user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
|
||||||
|
if user.registration_status == User.RegisterStatus.BANNED:
|
||||||
|
messages.error(request, "Esta cuenta esta bloqueada.")
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
authenticated_user = authenticate(request, username=username, password=password)
|
||||||
|
|
||||||
|
if authenticated_user is None:
|
||||||
|
if _handle_login_rate_limit(request, username, email):
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
messages.error(request, "El email o la contraseña es incorrecta")
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
if authenticated_user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
||||||
audit_logger.info("LOGIN_FAILED email=%s reason=not_verified", _mask_email(email))
|
audit_logger.info("LOGIN_FAILED email=%s reason=not_verified", _mask_email(email))
|
||||||
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
auth_login(request, user)
|
|
||||||
|
auth_login(request, authenticated_user)
|
||||||
|
|
||||||
if not remember:
|
if not remember:
|
||||||
request.session.set_expiry(0)
|
request.session.set_expiry(0)
|
||||||
else:
|
else:
|
||||||
request.session.set_expiry(1209600)
|
request.session.set_expiry(1209600)
|
||||||
|
|
||||||
audit_logger.info("LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", user.id, _mask_email(user.email), client_ip, bool(remember))
|
audit_logger.info("LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", authenticated_user.id, _mask_email(authenticated_user.email), client_ip, bool(remember))
|
||||||
tasks.enviar_correo_bienvenida.delay(user.email, f"{user.first_name} {user.last_name}")
|
tasks.enviar_correo_bienvenida.delay(authenticated_user.email, f"{authenticated_user.first_name} {authenticated_user.last_name}")
|
||||||
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
messages.success(request, f"¡Bienvenido {authenticated_user.first_name or authenticated_user.username}!")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
else:
|
|
||||||
form = UserLoginForm()
|
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def register(request: HttpRequest):
|
def register(request: HttpRequest):
|
||||||
@@ -390,7 +415,7 @@ def categoria(request: HttpRequest, id: int):
|
|||||||
category = Category.objects.get(id=id)
|
category = Category.objects.get(id=id)
|
||||||
categories = Category.objects.all()
|
categories = Category.objects.all()
|
||||||
products = Product.objects.filter(category=category)[start:end]
|
products = Product.objects.filter(category=category)[start:end]
|
||||||
return render(request, "tienda/index.html", {"products": products, "categories": categories})
|
return render(request, INDEX_TEMPLATE, {"products": products, "categories": categories})
|
||||||
|
|
||||||
|
|
||||||
# Funciones auxiliares para el carrito
|
# Funciones auxiliares para el carrito
|
||||||
@@ -610,15 +635,9 @@ def _get_selected_shipping_address(request: HttpRequest):
|
|||||||
|
|
||||||
return ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
return ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def create_order_from_cart(request, payment_method, payment_reference="", shipping_address=None, stock_reservation=None):
|
|
||||||
"""Crea un pedido a partir del carrito actual, validando y descontando stock."""
|
|
||||||
cart = get_or_create_cart(request)
|
|
||||||
cart_items = list(cart.items.select_related("product", "product__creator"))
|
|
||||||
|
|
||||||
if not cart_items:
|
|
||||||
return None, "El carrito está vacío."
|
|
||||||
|
|
||||||
|
def _calculate_order_totals(cart_items):
|
||||||
|
"""Calcula los totales de los items del carrito."""
|
||||||
order_total = Decimal("0.00")
|
order_total = Decimal("0.00")
|
||||||
items_with_totals = []
|
items_with_totals = []
|
||||||
purchased_items = []
|
purchased_items = []
|
||||||
@@ -627,75 +646,73 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
|
|||||||
product = item.product
|
product = item.product
|
||||||
unit_price_with_vat = get_price_with_vat_decimal(product.price)
|
unit_price_with_vat = get_price_with_vat_decimal(product.price)
|
||||||
line_total_with_vat = (unit_price_with_vat * item.quantity).quantize(
|
line_total_with_vat = (unit_price_with_vat * item.quantity).quantize(
|
||||||
Decimal("0.01"),
|
Decimal("0.01"), rounding=ROUND_HALF_UP,
|
||||||
rounding=ROUND_HALF_UP,
|
|
||||||
)
|
)
|
||||||
order_total += line_total_with_vat
|
order_total += line_total_with_vat
|
||||||
items_with_totals.append((item, unit_price_with_vat, line_total_with_vat))
|
items_with_totals.append((item, unit_price_with_vat, line_total_with_vat))
|
||||||
purchased_items.append(
|
purchased_items.append({
|
||||||
{
|
|
||||||
"amount": item.quantity,
|
"amount": item.quantity,
|
||||||
"product_name": product.name,
|
"product_name": product.name,
|
||||||
"price": float(unit_price_with_vat),
|
"price": float(unit_price_with_vat),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
_release_expired_stock_reservations()
|
return order_total, items_with_totals, purchased_items
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
locked_reservation = None
|
|
||||||
reserved_by_product = {}
|
|
||||||
|
|
||||||
if stock_reservation is not None:
|
def _validate_locked_reservation(stock_reservation):
|
||||||
|
"""Valida y carga la reserva de stock, retorna (locked_reservation, reserved_by_product).
|
||||||
|
Si stock_reservation es None, retorna (None, {}). Si la reserva ha caducado, retorna (None, None)."""
|
||||||
|
if stock_reservation is None:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
locked_reservation = StockReservation.objects.select_for_update().filter(
|
locked_reservation = StockReservation.objects.select_for_update().filter(
|
||||||
id=stock_reservation.id,
|
id=stock_reservation.id,
|
||||||
status=StockReservation.STATUS_ACTIVE,
|
status=StockReservation.STATUS_ACTIVE,
|
||||||
expires_at__gt=timezone.now(),
|
expires_at__gt=timezone.now(),
|
||||||
).first()
|
).first()
|
||||||
if locked_reservation is None:
|
if locked_reservation is None:
|
||||||
return None, (
|
return None, None
|
||||||
f"La reserva de stock ha caducado. Tienes {STOCK_RESERVATION_MINUTES} minutos "
|
|
||||||
"desde que pulsas pagar. Revisa el carrito y vuelve a intentarlo."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
reserved_by_product = {}
|
||||||
for reservation_item in locked_reservation.items.all():
|
for reservation_item in locked_reservation.items.all():
|
||||||
reserved_by_product[reservation_item.product_id] = reservation_item.quantity
|
reserved_by_product[reservation_item.product_id] = reservation_item.quantity
|
||||||
|
|
||||||
product_ids = [item.product_id for item in cart_items]
|
return locked_reservation, reserved_by_product
|
||||||
products = Product.objects.select_for_update().filter(id__in=product_ids)
|
|
||||||
product_map = {product.id: product for product in products}
|
|
||||||
|
|
||||||
reserved_from_others = _get_reserved_quantities_by_product(
|
|
||||||
product_ids,
|
|
||||||
exclude_reservation_ids=[locked_reservation.id] if locked_reservation else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def _validate_order_items(cart_items, product_map, locked_reservation, reserved_by_product, reserved_from_others):
|
||||||
|
"""Valida disponibilidad y stock de cada item. Retorna None si ok, o str con el error."""
|
||||||
for item in cart_items:
|
for item in cart_items:
|
||||||
product = product_map.get(item.product_id)
|
product = product_map.get(item.product_id)
|
||||||
if product is None:
|
if product is None:
|
||||||
return None, f"El producto '{item.product.name}' ya no está disponible."
|
return f"El producto '{item.product.name}' ya no está disponible."
|
||||||
|
|
||||||
if locked_reservation is not None and item.quantity > reserved_by_product.get(item.product_id, 0):
|
if locked_reservation is not None and item.quantity > reserved_by_product.get(item.product_id, 0):
|
||||||
return None, (
|
return (
|
||||||
f"La cantidad de '{item.product.name}' ha cambiado desde la reserva. "
|
f"La cantidad de '{item.product.name}' ha cambiado desde la reserva. "
|
||||||
"Vuelve a intentar el pago."
|
"Vuelve a intentar el pago."
|
||||||
)
|
)
|
||||||
|
|
||||||
available = max(product.stock - reserved_from_others.get(item.product_id, 0), 0)
|
available = max(product.stock - reserved_from_others.get(item.product_id, 0), 0)
|
||||||
if item.quantity > available:
|
if item.quantity > available:
|
||||||
return None, _build_stock_issue_message({
|
return _build_stock_issue_message({
|
||||||
"product_name": item.product.name,
|
"product_name": item.product.name,
|
||||||
"requested": item.quantity,
|
"requested": item.quantity,
|
||||||
"available": available,
|
"available": available,
|
||||||
})
|
})
|
||||||
|
|
||||||
if product.stock < item.quantity:
|
if product.stock < item.quantity:
|
||||||
return None, _build_stock_issue_message({
|
return _build_stock_issue_message({
|
||||||
"product_name": item.product.name,
|
"product_name": item.product.name,
|
||||||
"requested": item.quantity,
|
"requested": item.quantity,
|
||||||
"available": product.stock,
|
"available": product.stock,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_order_and_items(request, order_total, cart_items, items_with_totals, product_map, product_ids, payment_method, payment_reference, shipping_address, locked_reservation):
|
||||||
|
"""Crea la orden y sus items, descuenta stock y marca reserva como completada."""
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
buyer=request.user if request.user.is_authenticated else None,
|
buyer=request.user if request.user.is_authenticated else None,
|
||||||
shipping_address=shipping_address,
|
shipping_address=shipping_address,
|
||||||
@@ -717,19 +734,55 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
|
|||||||
unit_price=float(unit_price_with_vat),
|
unit_price=float(unit_price_with_vat),
|
||||||
total_price=float(line_total_with_vat),
|
total_price=float(line_total_with_vat),
|
||||||
)
|
)
|
||||||
|
|
||||||
product_row = product_map.get(item.product_id)
|
product_row = product_map.get(item.product_id)
|
||||||
product_row.stock = F('stock') - item.quantity
|
product_row.stock = F('stock') - item.quantity
|
||||||
product_row.save(update_fields=["stock"])
|
product_row.save(update_fields=["stock"])
|
||||||
|
|
||||||
_invalidate_product_cache(product_ids)
|
|
||||||
|
|
||||||
cart.items.all().delete()
|
|
||||||
|
|
||||||
if locked_reservation is not None:
|
if locked_reservation is not None:
|
||||||
locked_reservation.status = StockReservation.STATUS_COMPLETED
|
locked_reservation.status = StockReservation.STATUS_COMPLETED
|
||||||
locked_reservation.save(update_fields=["status", "updated_at"])
|
locked_reservation.save(update_fields=["status", "updated_at"])
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
@require_GET
|
||||||
|
def create_order_from_cart(request, payment_method, payment_reference="", shipping_address=None, stock_reservation=None):
|
||||||
|
"""Crea un pedido a partir del carrito actual, validando y descontando stock."""
|
||||||
|
cart = get_or_create_cart(request)
|
||||||
|
cart_items = list(cart.items.select_related("product", "product__creator"))
|
||||||
|
|
||||||
|
if not cart_items:
|
||||||
|
return None, MSG_CARRITO_VACIO + "."
|
||||||
|
|
||||||
|
order_total, items_with_totals, purchased_items = _calculate_order_totals(cart_items)
|
||||||
|
_release_expired_stock_reservations()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
locked_reservation, reserved_by_product = _validate_locked_reservation(stock_reservation)
|
||||||
|
if locked_reservation is None and stock_reservation is not None:
|
||||||
|
return None, (
|
||||||
|
f"La reserva de stock ha caducado. Tienes {STOCK_RESERVATION_MINUTES} minutos "
|
||||||
|
"desde que pulsas pagar. Revisa el carrito y vuelve a intentarlo."
|
||||||
|
)
|
||||||
|
|
||||||
|
product_ids = [item.product_id for item in cart_items]
|
||||||
|
products = Product.objects.select_for_update().filter(id__in=product_ids)
|
||||||
|
product_map = {product.id: product for product in products}
|
||||||
|
|
||||||
|
reserved_from_others = _get_reserved_quantities_by_product(
|
||||||
|
product_ids,
|
||||||
|
exclude_reservation_ids=[locked_reservation.id] if locked_reservation else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
error_msg = _validate_order_items(cart_items, product_map, locked_reservation, reserved_by_product, reserved_from_others)
|
||||||
|
if error_msg:
|
||||||
|
return None, error_msg
|
||||||
|
|
||||||
|
order = _create_order_and_items(request, order_total, cart_items, items_with_totals, product_map, product_ids, payment_method, payment_reference, shipping_address, locked_reservation)
|
||||||
|
|
||||||
|
_invalidate_product_cache(product_ids)
|
||||||
|
cart.items.all().delete()
|
||||||
|
|
||||||
if request.user.is_authenticated and purchased_items:
|
if request.user.is_authenticated and purchased_items:
|
||||||
tasks.process_purchase.delay(
|
tasks.process_purchase.delay(
|
||||||
request.user.id,
|
request.user.id,
|
||||||
@@ -970,15 +1023,8 @@ def crear_producto(request: HttpRequest):
|
|||||||
form = ProductForm()
|
form = ProductForm()
|
||||||
return render(request, "tienda/crear_producto.html", {"form":form})
|
return render(request, "tienda/crear_producto.html", {"form":form})
|
||||||
|
|
||||||
@require_http_methods(["GET","POST"])
|
def _handle_edit_product_post(request, producto, form):
|
||||||
@login_required
|
"""Procesa el POST de editar producto. Retorna respuesta HTTP o None si hay error."""
|
||||||
def editar_producto(request: HttpRequest, id: int):
|
|
||||||
"""Edita un producto del usuario autenticado"""
|
|
||||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
form = ProductEditForm(request.POST, request.FILES)
|
|
||||||
if form.is_valid():
|
|
||||||
producto.name = form.cleaned_data["name"]
|
producto.name = form.cleaned_data["name"]
|
||||||
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
||||||
producto.description = form.cleaned_data["description"]
|
producto.description = form.cleaned_data["description"]
|
||||||
@@ -1010,8 +1056,19 @@ def editar_producto(request: HttpRequest, id: int):
|
|||||||
|
|
||||||
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
||||||
return redirect("mis_productos")
|
return redirect("mis_productos")
|
||||||
else:
|
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
|
||||||
|
@require_http_methods(["GET","POST"])
|
||||||
|
@login_required
|
||||||
|
def editar_producto(request: HttpRequest, id: int):
|
||||||
|
"""Edita un producto del usuario autenticado"""
|
||||||
|
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = ProductEditForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
return _handle_edit_product_post(request, producto, form)
|
||||||
|
messages.error(request, MSG_CAMPOS_OBLIGATORIOS)
|
||||||
else:
|
else:
|
||||||
initial = {
|
initial = {
|
||||||
"name": producto.name,
|
"name": producto.name,
|
||||||
@@ -1023,10 +1080,9 @@ def editar_producto(request: HttpRequest, id: int):
|
|||||||
}
|
}
|
||||||
form = ProductEditForm(initial=initial)
|
form = ProductEditForm(initial=initial)
|
||||||
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
return render(request, "tienda/editar_producto.html", {
|
||||||
"form": form,
|
"form": form,
|
||||||
"producto": producto
|
"producto": producto,
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -1130,13 +1186,13 @@ def create_checkout_session(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1282,7 +1338,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
import paypalrestsdk
|
import paypalrestsdk
|
||||||
|
|
||||||
@@ -1290,7 +1346,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1469,17 +1525,17 @@ def crear_payment_intent(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1551,7 +1607,7 @@ def confirmar_pago_tarjeta(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
payment_intent_id = payload.get("payment_intent_id")
|
payment_intent_id = payload.get("payment_intent_id")
|
||||||
if not payment_intent_id:
|
if not payment_intent_id:
|
||||||
@@ -1624,13 +1680,13 @@ def crear_orden_paypal(request: HttpRequest):
|
|||||||
|
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1677,7 +1733,7 @@ def capturar_orden_paypal(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
paypal_order_id = payload.get("orderID")
|
paypal_order_id = payload.get("orderID")
|
||||||
if not paypal_order_id:
|
if not paypal_order_id:
|
||||||
@@ -1812,7 +1868,7 @@ def confirmar_setup_intent(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
payment_method_id = payload.get("payment_method_id")
|
payment_method_id = payload.get("payment_method_id")
|
||||||
if not payment_method_id:
|
if not payment_method_id:
|
||||||
@@ -1931,7 +1987,7 @@ def capturar_orden_paypal_setup(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
paypal_order_id = payload.get("orderID")
|
paypal_order_id = payload.get("orderID")
|
||||||
if not paypal_order_id:
|
if not paypal_order_id:
|
||||||
@@ -2037,7 +2093,7 @@ def editar_perfil(request: HttpRequest):
|
|||||||
|
|
||||||
if email != request.user.email and User.objects.filter(email=email).exists():
|
if email != request.user.email and User.objects.filter(email=email).exists():
|
||||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
request.user.first_name = form.cleaned_data["first_name"]
|
request.user.first_name = form.cleaned_data["first_name"]
|
||||||
request.user.last_name = form.cleaned_data["last_name"]
|
request.user.last_name = form.cleaned_data["last_name"]
|
||||||
@@ -2054,7 +2110,7 @@ def editar_perfil(request: HttpRequest):
|
|||||||
}
|
}
|
||||||
form = EditProfileForm(initial=initial)
|
form = EditProfileForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2069,11 +2125,11 @@ def cambiar_contrasena(request: HttpRequest):
|
|||||||
|
|
||||||
if not request.user.check_password(current_password):
|
if not request.user.check_password(current_password):
|
||||||
messages.error(request, "La contraseña actual es incorrecta.")
|
messages.error(request, "La contraseña actual es incorrecta.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"password_form": ChangePasswordForm()})
|
||||||
|
|
||||||
if len(new_password) < 8:
|
if len(new_password) < 8:
|
||||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"password_form": ChangePasswordForm()})
|
||||||
|
|
||||||
request.user.set_password(new_password)
|
request.user.set_password(new_password)
|
||||||
request.user.save()
|
request.user.save()
|
||||||
@@ -2084,7 +2140,7 @@ def cambiar_contrasena(request: HttpRequest):
|
|||||||
return redirect("portal_usuario")
|
return redirect("portal_usuario")
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Las contraseñas nuevas no coinciden o son inválidas.")
|
messages.error(request, "Las contraseñas nuevas no coinciden o son inválidas.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"password_form": form})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"password_form": form})
|
||||||
|
|
||||||
return redirect("editar_perfil")
|
return redirect("editar_perfil")
|
||||||
|
|
||||||
@@ -2112,11 +2168,11 @@ def crear_direccion(request: HttpRequest):
|
|||||||
|
|
||||||
if not _is_almeria_city(city):
|
if not _is_almeria_city(city):
|
||||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(form=form))
|
||||||
|
|
||||||
if not _is_almeria_postal_code(postal_code):
|
if not _is_almeria_postal_code(postal_code):
|
||||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(form=form))
|
||||||
|
|
||||||
ShippingAddress.objects.create(
|
ShippingAddress.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
@@ -2133,11 +2189,11 @@ def crear_direccion(request: HttpRequest):
|
|||||||
messages.success(request, "Dirección creada correctamente.")
|
messages.success(request, "Dirección creada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
messages.error(request, MSG_CAMPOS_OBLIGATORIOS)
|
||||||
else:
|
else:
|
||||||
form = ShippingAddressForm()
|
form = ShippingAddressForm()
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(form=form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2154,11 +2210,11 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
|
|
||||||
if not _is_almeria_city(city):
|
if not _is_almeria_city(city):
|
||||||
messages.error(request, "El pueblo/ciudad debe pertenece a la provincia de Almería.")
|
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))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
if not _is_almeria_postal_code(postal_code):
|
if not _is_almeria_postal_code(postal_code):
|
||||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
direccion.full_name = form.cleaned_data["full_name"]
|
direccion.full_name = form.cleaned_data["full_name"]
|
||||||
direccion.address_line_1 = form.cleaned_data["address_line_1"]
|
direccion.address_line_1 = form.cleaned_data["address_line_1"]
|
||||||
@@ -2173,7 +2229,7 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
messages.success(request, "Dirección actualizada correctamente.")
|
messages.success(request, "Dirección actualizada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
messages.error(request, MSG_CAMPOS_OBLIGATORIOS)
|
||||||
else:
|
else:
|
||||||
initial = {
|
initial = {
|
||||||
"full_name": direccion.full_name,
|
"full_name": direccion.full_name,
|
||||||
@@ -2187,7 +2243,7 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
}
|
}
|
||||||
form = ShippingAddressForm(initial=initial)
|
form = ShippingAddressForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2275,7 +2331,7 @@ def reset_password(request: HttpRequest):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
tasks.enviar_correo_recuperacion.delay(form.cleaned_data["email"])
|
tasks.enviar_correo_recuperacion.delay(form.cleaned_data["email"])
|
||||||
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
||||||
return render(request, "tienda/index.html", {})
|
return render(request, INDEX_TEMPLATE, {})
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def reset_password_phase2(request: HttpRequest, code: str):
|
def reset_password_phase2(request: HttpRequest, code: str):
|
||||||
|
|||||||
Reference in New Issue
Block a user