diff --git a/tienda/admin.py b/tienda/admin.py index 7b91fbf..577ca0f 100644 --- a/tienda/admin.py +++ b/tienda/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod +from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod, Review # Register your models here. from django.shortcuts import redirect from django.urls import path @@ -150,4 +150,11 @@ class StockReservationAdmin(admin.ModelAdmin): class SavedPaymentMethodAdmin(admin.ModelAdmin): list_display = ('id', 'user', 'method_type', 'label', 'is_default', 'created_at') list_filter = ('method_type', 'is_default', 'created_at') - search_fields = ('user__username', 'user__email', 'label', 'paypal_email') \ No newline at end of file + search_fields = ('user__username', 'user__email', 'label', 'paypal_email') + + +@admin.register(Review) +class ReviewAdmin(admin.ModelAdmin): + list_display = ('id', 'product', 'user', 'rating', 'title', 'created_at') + list_filter = ('rating', 'created_at') + search_fields = ('user__username', 'product__name', 'title', 'content') \ No newline at end of file diff --git a/tienda/forms.py b/tienda/forms.py index 0234974..3701a74 100644 --- a/tienda/forms.py +++ b/tienda/forms.py @@ -369,4 +369,33 @@ 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...' + }) + ) \ No newline at end of file diff --git a/tienda/migrations/0009_alter_cartitem_quantity_alter_orderitem_quantity_and_more.py b/tienda/migrations/0009_alter_cartitem_quantity_alter_orderitem_quantity_and_more.py new file mode 100644 index 0000000..694df1f --- /dev/null +++ b/tienda/migrations/0009_alter_cartitem_quantity_alter_orderitem_quantity_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 6.0.5 on 2026-05-08 11:32 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tienda', '0008_alter_product_briefdesc_alter_product_description'), + ] + + operations = [ + migrations.AlterField( + model_name='cartitem', + name='quantity', + field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MaxValueValidator(9999)]), + ), + migrations.AlterField( + model_name='orderitem', + name='quantity', + field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MaxValueValidator(9999)]), + ), + migrations.AlterField( + model_name='stockreservationitem', + name='quantity', + field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MaxValueValidator(9999)]), + ), + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rating', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(5)])), + ('title', models.CharField(default='', max_length=200)), + ('content', models.TextField(default='', max_length=2000)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('images', models.ManyToManyField(blank=True, related_name='product_reviews', to='tienda.image')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='tienda.product')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_reviews', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'unique_together': {('product', 'user')}, + }, + ), + ] diff --git a/tienda/models.py b/tienda/models.py index 25d82be..052a594 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -122,6 +122,26 @@ class Product(models.Model): "creator": self.creator.to_dict() if self.creator else None } + def has_user_purchased(self, user): + """Verifica si el usuario ha comprado este producto al menos una vez""" + if not user or not user.is_authenticated: + return False + return OrderItem.objects.filter( + order__buyer=user, + product=self + ).exists() + + def get_average_rating(self): + """Retorna la nota media de las valoraciones""" + reviews = self.reviews.all() + if not reviews.exists(): + return 0 + return round(reviews.aggregate(models.Avg('rating'))['rating__avg'], 1) + + def get_reviews_count(self): + """Retorna el número total de valoraciones""" + return self.reviews.count() + class StockReservation(models.Model): STATUS_ACTIVE = "active" @@ -331,6 +351,25 @@ class SavedPaymentMethod(models.Model): super().save(*args, **kwargs) +class Review(models.Model): + """Valoraciones de productos por usuarios que han realizado una compra""" + product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='product_reviews') + rating = models.PositiveIntegerField(validators=[MaxValueValidator(5)]) + title = models.CharField(max_length=200, default="") + content = models.TextField(max_length=2000, default="") + images = models.ManyToManyField(Image, related_name='product_reviews', blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ('product', 'user') + ordering = ['-created_at'] + + def __str__(self): + return f"Valoración de {self.user.username} en {self.product.name} ({self.rating}★)" + + class ShippingAddress(models.Model): """Direcciones de entrega de los usuarios""" user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses') diff --git a/tienda/templates/tienda/add_review.html b/tienda/templates/tienda/add_review.html new file mode 100644 index 0000000..8e09d4e --- /dev/null +++ b/tienda/templates/tienda/add_review.html @@ -0,0 +1,106 @@ +{% extends "tienda/base.html" %} + +{% block content %} +