From 0438a77149f11f9b4507f2768459cf5e8675a43e Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 13:33:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1adir=20sistema=20de=20valoracion?= =?UTF-8?q?es=20con=20formulario,=20vistas=20y=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tienda/forms.py | 41 ++++++++- tienda/templates/tienda/add_review.html | 112 ++++++++++++++++++++++++ tienda/templates/tienda/producto.html | 79 +++++++++++++++++ tienda/urls.py | 4 +- tienda/views.py | 95 +++++++++++++++++++- 5 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 tienda/templates/tienda/add_review.html 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 }} +

+ +
+ {% csrf_token %} + +
+ +
+ {% for i in "12345" %} + + {% endfor %} +
+ + {% if form.rating.errors %} +
{{ form.rating.errors }}
+ {% endif %} +
+ +
+ + {{ form.title }} + {% if form.title.errors %} +
{{ form.title.errors }}
+ {% endif %} +
+ +
+ + {{ form.content }} + {% if form.content.errors %} +
{{ form.content.errors }}
+ {% endif %} +
+ +
+ + {{ form.images }} +
Puedes subir hasta 5 imágenes
+
+ +
+ + Cancelar +
+
+
+
+
+
+
+ + +{% 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 }} + +
+
+

Valoraciones

+
+
+
0.0
+
+
+ 0 valoraciones +
+ {% if can_review %} + Valorar este producto + {% elif user.is_authenticated %} + Ya has valorado este producto + {% else %} + Inicia sesión para valorar + {% endif %} +
+
+
+
+
+ + {% 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() + })