diff --git a/tienda/forms.py b/tienda/forms.py
index 0234974..a108c4d 100644
--- a/tienda/forms.py
+++ b/tienda/forms.py
@@ -369,4 +369,43 @@ class ResetPasswordPhase2Form(forms.Form):
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.")
\ No newline at end of file
+ raise ValidationError("Las contraseñas no coinciden.")
+
+
+class ReviewForm(forms.Form):
+ rating = forms.IntegerField(
+ label="Puntuación",
+ required=True,
+ min_value=1,
+ max_value=5,
+ widget=forms.HiddenInput()
+ )
+ title = forms.CharField(
+ label="Título",
+ max_length=200,
+ required=True,
+ widget=forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'Título de tu valoración'
+ })
+ )
+ content = forms.CharField(
+ label="Descripción",
+ max_length=2000,
+ required=True,
+ widget=forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 5,
+ 'placeholder': 'Comparte tu experiencia con este producto...'
+ })
+ )
+ images = forms.ImageField(
+ label="Imágenes (opcional)",
+ required=False,
+ validators=[validate_image_file],
+ widget=forms.ClearableFileInput(attrs={
+ 'class': 'form-control',
+ 'accept': 'image/*',
+ 'multiple': True
+ })
+ )
\ No newline at end of file
diff --git a/tienda/templates/tienda/add_review.html b/tienda/templates/tienda/add_review.html
new file mode 100644
index 0000000..e64635a
--- /dev/null
+++ b/tienda/templates/tienda/add_review.html
@@ -0,0 +1,112 @@
+{% extends "tienda/base.html" %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ {% if existing_review %}Actualizar{% else %}Añadir{% endif %} valoración: {{ product.name }}
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/tienda/templates/tienda/producto.html b/tienda/templates/tienda/producto.html
index 117aa9f..3c0edfb 100644
--- a/tienda/templates/tienda/producto.html
+++ b/tienda/templates/tienda/producto.html
@@ -62,4 +62,83 @@
{{ product.description }}
+
+
+
+
{% endblock %}
diff --git a/tienda/urls.py b/tienda/urls.py
index 02fd694..cbb924f 100644
--- a/tienda/urls.py
+++ b/tienda/urls.py
@@ -68,5 +68,7 @@ urlpatterns = [
path("sobre-nosotros", views.sobre_nosotros, name="sobre_nosotros"),
path("ayuda", views.ayuda, name="ayuda"),
path("reset-password", views.reset_password, name="reset_password"),
- path("reset-password-phase2/", views.reset_password_phase2, name="reset_password_phase2")
+ path("reset-password-phase2/", views.reset_password_phase2, name="reset_password_phase2"),
+ path("producto//valorar/", views.add_review, name="add_review"),
+ path("api/producto//valoraciones/", views.product_reviews, name="product_reviews"),
]
diff --git a/tienda/views.py b/tienda/views.py
index 5c16a4a..34e9afd 100644
--- a/tienda/views.py
+++ b/tienda/views.py
@@ -6,8 +6,8 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages
from tienda.utilities import send_email
-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 .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, StockReservation, StockReservationItem, VerificationCode, SavedPaymentMethod, Review
+from .forms import ProductForm, SecondaryImageForm, UserLoginForm, UserRegisterForm, ProductEditForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form, ReviewForm
from . import tasks
from .vars import (
PAGE_SIZE,
@@ -400,7 +400,11 @@ def producto(request: HttpRequest, id: int):
# Cachear el producto por 5 minutos (300 segundos)
cache.set(cache_key, product, 300)
- return render(request, "tienda/producto.html", {"product": product})
+ can_review = False
+ if request.user.is_authenticated:
+ can_review = product.has_user_purchased(request.user) and not Review.objects.filter(product=product, user=request.user).exists()
+
+ return render(request, "tienda/producto.html", {"product": product, "can_review": can_review})
def categoria(request: HttpRequest, id: int):
page = 1
@@ -2324,3 +2328,88 @@ def reset_password_phase2(request: HttpRequest, code: str):
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
else:
raise Http404()
+
+
+@login_required
+def add_review(request: HttpRequest, product_id: int):
+ product = get_object_or_404(Product, id=product_id)
+
+ if not product.has_user_purchased(request.user):
+ messages.error(request, "Solo puedes valorar productos que hayas comprado.")
+ return redirect(reverse("product_detail", args=[product_id]))
+
+ existing_review = Review.objects.filter(product=product, user=request.user).first()
+
+ if request.method == "POST":
+ form = ReviewForm(request.POST, request.FILES)
+ if form.is_valid():
+ rating = form.cleaned_data["rating"]
+ title = form.cleaned_data["title"]
+ content = form.cleaned_data["content"]
+
+ if existing_review:
+ existing_review.rating = rating
+ existing_review.title = title
+ existing_review.content = content
+ existing_review.save()
+ existing_review.images.clear()
+ review = existing_review
+ messages.success(request, "¡Tu valoración ha sido actualizada!")
+ else:
+ review = Review.objects.create(
+ product=product,
+ user=request.user,
+ rating=rating,
+ title=title,
+ content=content
+ )
+ messages.success(request, "¡Gracias por tu valoración!")
+
+ uploaded_files = request.FILES.getlist("images")
+ for uploaded_file in uploaded_files:
+ image = Image.objects.create(
+ name=f"Review {product.name} - {request.user.username}",
+ image=uploaded_file
+ )
+ review.images.add(image)
+
+ return redirect(reverse("product_detail", args=[product_id]))
+ else:
+ initial_data = {}
+ if existing_review:
+ initial_data = {
+ "rating": existing_review.rating,
+ "title": existing_review.title,
+ "content": existing_review.content
+ }
+ form = ReviewForm(initial=initial_data)
+
+ return render(request, "tienda/add_review.html", {
+ "product": product,
+ "form": form,
+ "existing_review": existing_review
+ })
+
+
+def product_reviews(request: HttpRequest, product_id: int):
+ product = get_object_or_404(Product, id=product_id)
+ reviews = product.reviews.select_related("user").prefetch_related("images").all()
+
+ return JsonResponse({
+ "reviews": [
+ {
+ "id": r.id,
+ "user": r.user.username,
+ "rating": r.rating,
+ "title": r.title,
+ "content": r.content,
+ "images": [img.to_dict() for img in r.images.all()],
+ "created_at": r.created_at.isoformat(),
+ "is_owner": request.user.is_authenticated and r.user.id == request.user.id
+ }
+ for r in reviews
+ ],
+ "average_rating": product.get_average_rating(),
+ "reviews_count": product.get_reviews_count(),
+ "can_review": request.user.is_authenticated and product.has_user_purchased(request.user) and not Review.objects.filter(product=product, user=request.user).exists()
+ })