first commit
This commit is contained in:
0
reviews/__init__.py
Normal file
0
reviews/__init__.py
Normal file
16
reviews/admin.py
Normal file
16
reviews/admin.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.contrib import admin
|
||||
from .models import Rating
|
||||
|
||||
@admin.register(Rating)
|
||||
class RatingAdmin(admin.ModelAdmin):
|
||||
list_display = ('content_object', 'user', 'score', 'created_at')
|
||||
list_filter = ('score', 'created_at', 'content_type')
|
||||
search_fields = ('user__username', 'user__email', 'comment')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
# GenericForeignKey alanlarını admin panelinde daha düzgün göstermek için
|
||||
# content_object alanını list_display'e ekledik, bu sayede hangi nesneye oy verildiği görülebilir.
|
||||
|
||||
def get_queryset(self, request):
|
||||
# N+1 sorununu önlemek için related alanları prefetch edelim
|
||||
return super().get_queryset(request).select_related('user', 'content_type').prefetch_related('content_object')
|
||||
6
reviews/apps.py
Normal file
6
reviews/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ReviewsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'reviews'
|
||||
37
reviews/migrations/0001_initial.py
Normal file
37
reviews/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 6.0 on 2026-01-20 18:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Rating',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('score', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], verbose_name='Puan')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Yorum')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Değerlendirme',
|
||||
'verbose_name_plural': 'Değerlendirmeler',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('content_type', 'object_id', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
reviews/migrations/__init__.py
Normal file
0
reviews/migrations/__init__.py
Normal file
48
reviews/models.py
Normal file
48
reviews/models.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Avg
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
score = models.PositiveSmallIntegerField(choices=[(i, str(i)) for i in range(1, 6)], verbose_name="Puan")
|
||||
comment = models.TextField(blank=True, null=True, verbose_name="Yorum")
|
||||
|
||||
# Generic Relation Fields
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='ratings')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('content_type', 'object_id', 'user') # Bir kullanıcı bir şeye sadece bir kez oy verebilir
|
||||
ordering = ['-created_at']
|
||||
verbose_name = "Değerlendirme"
|
||||
verbose_name_plural = "Değerlendirmeler"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.score} - {self.content_object}"
|
||||
|
||||
|
||||
# Bu Mixin'i oylanabilir olmasını istediğiniz modellere miras (inherit) verebilirsiniz.
|
||||
class RateableMixin:
|
||||
@property
|
||||
def average_rating(self):
|
||||
# İlgili nesneye ait tüm oyların ortalamasını alır
|
||||
ratings = Rating.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(self),
|
||||
object_id=self.id
|
||||
)
|
||||
return ratings.aggregate(Avg('score'))['score__avg'] or 0.0
|
||||
|
||||
@property
|
||||
def rating_count(self):
|
||||
# Toplam oy sayısını döndürür
|
||||
return Rating.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(self),
|
||||
object_id=self.id
|
||||
).count()
|
||||
37
reviews/serializers.py
Normal file
37
reviews/serializers.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from .models import Rating
|
||||
|
||||
class RatingSerializer(serializers.ModelSerializer):
|
||||
user = serializers.StringRelatedField(read_only=True)
|
||||
model_name = serializers.CharField(write_only=True) # Örn: 'product'
|
||||
object_id = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = Rating
|
||||
fields = ['id', 'score', 'comment', 'user', 'created_at', 'model_name', 'object_id']
|
||||
read_only_fields = ['id', 'created_at', 'user']
|
||||
|
||||
def create(self, validated_data):
|
||||
model_name = validated_data.pop('model_name').lower()
|
||||
object_id = validated_data.pop('object_id')
|
||||
user = self.context['request'].user
|
||||
|
||||
# Model adından ContentType'ı bul
|
||||
try:
|
||||
# app_label='product' varsayımı yapıyoruz veya tüm app'lerde arayabiliriz.
|
||||
# Daha güvenli olması için app_label'ı da parametre alabiliriz ama şimdilik basit tutalım.
|
||||
# Genellikle model isimleri unique olur.
|
||||
content_type = ContentType.objects.get(model=model_name)
|
||||
except ContentType.DoesNotExist:
|
||||
raise serializers.ValidationError(f"Model '{model_name}' bulunamadı.")
|
||||
|
||||
# Kullanıcının daha önce oy verip vermediğini kontrol et (unique_together constraint var ama burada da yakalayalım)
|
||||
rating, created = Rating.objects.update_or_create(
|
||||
content_type=content_type,
|
||||
object_id=object_id,
|
||||
user=user,
|
||||
defaults={'score': validated_data.get('score'), 'comment': validated_data.get('comment')}
|
||||
)
|
||||
|
||||
return rating
|
||||
7
reviews/urls.py
Normal file
7
reviews/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import CreateRatingView, ListRatingsView
|
||||
|
||||
urlpatterns = [
|
||||
path('rate/', CreateRatingView.as_view(), name='create-rating'),
|
||||
path('list/', ListRatingsView.as_view(), name='list-ratings'),
|
||||
]
|
||||
34
reviews/views.py
Normal file
34
reviews/views.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from rest_framework import generics, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from .models import Rating
|
||||
from .serializers import RatingSerializer
|
||||
|
||||
class CreateRatingView(generics.CreateAPIView):
|
||||
serializer_class = RatingSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# Serializer'ın create metodunda user ve content_type işlemleri yapılıyor
|
||||
serializer.save()
|
||||
|
||||
class ListRatingsView(APIView):
|
||||
"""
|
||||
Belirli bir model ve ID için yorumları listeler.
|
||||
Örn: /api/v1/reviews/list/?model=product&id=1
|
||||
"""
|
||||
def get(self, request):
|
||||
model_name = request.query_params.get('model')
|
||||
object_id = request.query_params.get('id')
|
||||
|
||||
if not model_name or not object_id:
|
||||
return Response({"error": "model ve id parametreleri gereklidir."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
content_type = ContentType.objects.get(model=model_name.lower())
|
||||
ratings = Rating.objects.filter(content_type=content_type, object_id=object_id)
|
||||
serializer = RatingSerializer(ratings, many=True)
|
||||
return Response(serializer.data)
|
||||
except ContentType.DoesNotExist:
|
||||
return Response({"error": "Model bulunamadı."}, status=status.HTTP_404_NOT_FOUND)
|
||||
Reference in New Issue
Block a user