fix: resolver 12 issues CRITICAL de SonarQube Cloud

- forms.py: cambiar import wildcard por imports explícitos (S2208)
- views.py: definir constantes para strings duplicados (S1192)
- views.py: refactorizar login, create_order_from_cart, editar_producto (S3776)
This commit is contained in:
Chroot
2026-05-26 10:53:18 +00:00
parent f0a638be2e
commit 424ffcffaf
2 changed files with 263 additions and 207 deletions
+1 -1
View File
@@ -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
View File
@@ -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):