Browse Source

feat: rate service

master
alireza 1 year ago
parent
commit
bced7c6b24
  1. 140
      apps/bookmark/admin.py
  2. 35
      apps/bookmark/migrations/0002_rate.py
  3. 2
      apps/bookmark/models/__init__.py
  4. 2
      apps/bookmark/models/bookmark.py
  5. 117
      apps/bookmark/models/rate.py
  6. 2
      apps/bookmark/serializers/__init__.py
  7. 2
      apps/bookmark/serializers/bookmark.py
  8. 86
      apps/bookmark/serializers/rate.py
  9. 9
      apps/bookmark/urls.py
  10. 2
      apps/bookmark/views/__init__.py
  11. 4
      apps/bookmark/views/bookmark.py
  12. 89
      apps/bookmark/views/rate.py
  13. 10
      apps/library/admin.py
  14. 52
      apps/library/migrations/0006_remove_book_author_book_isbn_book_numnber_of_volume_and_more.py
  15. 15
      apps/library/models.py
  16. 48
      apps/library/serializers.py

140
apps/bookmark/admin.py

@ -1,11 +1,143 @@
from django.contrib import admin
from .models import Bookmark
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin
from unfold.decorators import display, action
from django.utils.html import format_html
from django.urls import reverse
@admin.register(Bookmark)
class BookmarkAdmin(admin.ModelAdmin):
list_display = ('user', 'service', 'content_id', 'status', 'created_at', 'updated_at')
from apps.bookmark.models import Bookmark
from apps.bookmark.models import Rate
from utils.admin import project_admin_site
class BookmarkAdmin(ModelAdmin):
list_display = ('user', 'display_service', 'content_id', 'status', 'created_at')
list_filter = ('service', 'status', 'created_at')
search_fields = ('user__username', 'user__email', 'content_id')
readonly_fields = ('created_at', 'updated_at')
list_per_page = 20
date_hierarchy = 'created_at'
list_filter_submit = True
warn_unsaved_form = True
change_form_show_cancel_button = True
@display(description=_('Service'))
def display_service(self, obj):
service_colors = {
'library': 'primary',
'podcast': 'success',
'hadith': 'warning',
'video': 'danger'
}
color = service_colors.get(obj.service, 'secondary')
return format_html(
'<span class="badge badge-{}">{}</span>',
color,
obj.get_service_display()
)
fieldsets = (
(None, {
'fields': ('user', 'service', 'content_id')
}),
(_('Status'), {
'fields': ('status',)
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at')
}),
)
@action(description=_("View Content"))
def view_content(self, request, obj):
"""
Action to view the related content based on service type
"""
service = obj.service
content_id = obj.content_id
if service == 'library':
url = reverse('admin:library_book_change', args=[content_id])
elif service == 'podcast':
url = reverse('admin:podcast_podcast_change', args=[content_id])
elif service == 'hadith':
url = reverse('admin:hadith_hadith_change', args=[content_id])
elif service == 'video':
url = reverse('admin:video_video_change', args=[content_id])
else:
return None
return url
class RateAdmin(ModelAdmin):
list_display = ('user', 'display_service', 'content_id', 'display_rate', 'status', 'created_at')
list_filter = ('service', 'rate', 'status', 'created_at')
search_fields = ('user__username', 'user__email', 'content_id')
readonly_fields = ('created_at', 'updated_at')
list_per_page = 20
date_hierarchy = 'created_at'
list_filter_submit = True
warn_unsaved_form = True
change_form_show_cancel_button = True
@display(description=_('Service'))
def display_service(self, obj):
service_colors = {
'library': 'primary',
'podcast': 'success',
'hadith': 'warning',
'video': 'danger'
}
color = service_colors.get(obj.service, 'secondary')
return format_html(
'<span class="badge badge-{}">{}</span>',
color,
obj.get_service_display()
)
@display(description=_('Rate'))
def display_rate(self, obj):
# Display stars based on rate value
stars = '' * obj.rate + '' * (5 - obj.rate)
color = 'warning' # Yellow color for stars
return format_html(
'<span style="color: var(--bs-{});">{}</span>',
color,
stars
)
fieldsets = (
(None, {
'fields': ('user', 'service', 'content_id', 'rate')
}),
(_('Status'), {
'fields': ('status',)
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at')
}),
)
@action(description=_("View Content"))
def view_content(self, request, obj):
"""
Action to view the related content based on service type
"""
service = obj.service
content_id = obj.content_id
if service == 'library':
url = reverse('admin:library_book_change', args=[content_id])
elif service == 'podcast':
url = reverse('admin:podcast_podcast_change', args=[content_id])
elif service == 'hadith':
url = reverse('admin:hadith_hadith_change', args=[content_id])
elif service == 'video':
url = reverse('admin:video_video_change', args=[content_id])
else:
return None
return url
# Register with project_admin_site
project_admin_site.register(Bookmark, BookmarkAdmin)
project_admin_site.register(Rate, RateAdmin)

35
apps/bookmark/migrations/0002_rate.py

@ -0,0 +1,35 @@
# Generated by Django 5.1.8 on 2025-05-04 15:36
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 = [
('bookmark', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Rate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('service', models.CharField(choices=[('library', 'Library'), ('podcast', 'Podcast'), ('hadith', 'Hadith'), ('video', 'Video')], max_length=20, verbose_name='Service')),
('content_id', models.PositiveIntegerField(verbose_name='Content ID')),
('rate', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)], verbose_name='Rate')),
('status', models.BooleanField(default=True, verbose_name='Status')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rates', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Rate',
'verbose_name_plural': 'Rates',
'unique_together': {('user', 'service', 'content_id')},
},
),
]

2
apps/bookmark/models/__init__.py

@ -0,0 +1,2 @@
from .bookmark import *
from .rate import *

2
apps/bookmark/models.py → apps/bookmark/models/bookmark.py

@ -1,3 +1,5 @@
from django.db import models
from django.contrib.auth import get_user_model

117
apps/bookmark/models/rate.py

@ -0,0 +1,117 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db.models import Avg
User = get_user_model()
class Rate(models.Model):
"""
Rate model for different services like library, podcast, hadith, and video.
Users can rate content from 1 to 5.
"""
class ServiceChoices(models.TextChoices):
LIBRARY = 'library', 'Library'
PODCAST = 'podcast', 'Podcast'
HADITH = 'hadith', 'Hadith'
VIDEO = 'video', 'Video'
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='rates', verbose_name='User')
service = models.CharField(max_length=20, choices=ServiceChoices.choices, verbose_name='Service')
content_id = models.PositiveIntegerField(verbose_name='Content ID')
rate = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)],
verbose_name='Rate'
)
status = models.BooleanField(default=True, verbose_name='Status')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created At')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Updated At')
class Meta:
verbose_name = 'Rate'
verbose_name_plural = 'Rates'
unique_together = ('user', 'service', 'content_id')
def __str__(self):
return f"{self.user.username} - {self.get_service_display()} - {self.content_id} - {self.rate}"
@classmethod
def get_user_rate(cls, user, service, content_id):
"""
Get the rate information for a specific content by the user.
Args:
user: User instance
service: Service name (library, podcast, hadith, video)
content_id: ID of the content
Returns:
Dictionary containing:
- is_rated: Boolean indicating if the content is rated by the user
- rate: The rate value given by the user (1-5) or None if not rated
"""
try:
rate_obj = cls.objects.get(
user=user,
service=service,
content_id=content_id,
status=True
)
return {
'is_rated': True,
'rate': rate_obj.rate
}
except cls.DoesNotExist:
return {
'is_rated': False,
'rate': None
}
@classmethod
def validate_content_exists(cls, service, content_id):
"""
Validate if content with the given ID exists in the specified service.
Args:
service: Service name (library, podcast, hadith, video)
content_id: ID of the content to validate
Returns:
Boolean indicating if the content exists
"""
if service == cls.ServiceChoices.LIBRARY:
from apps.library.models import Book
return Book.objects.filter(id=content_id).exists()
elif service == cls.ServiceChoices.PODCAST:
from apps.podcast.models import Podcast
return Podcast.objects.filter(id=content_id).exists()
elif service == cls.ServiceChoices.HADITH:
from apps.hadith.models import Hadith
return Hadith.objects.filter(id=content_id).exists()
elif service == cls.ServiceChoices.VIDEO:
from apps.video.models import Video
return Video.objects.filter(id=content_id).exists()
return False
@classmethod
def get_average_rate(cls, service, content_id):
"""
Get the average rate for a specific content.
Args:
service: Service name (library, podcast, hadith, video)
content_id: ID of the content
Returns:
Float representing the average rate (1-5) or None if no rates
"""
result = cls.objects.filter(
service=service,
content_id=content_id,
status=True
).aggregate(avg_rate=Avg('rate'))
return result['avg_rate']

2
apps/bookmark/serializers/__init__.py

@ -0,0 +1,2 @@
from .bookmark import *
from .rate import *

2
apps/bookmark/serializers.py → apps/bookmark/serializers/bookmark.py

@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import Bookmark
from apps.bookmark.models import Bookmark
class BookmarkSerializer(serializers.ModelSerializer):

86
apps/bookmark/serializers/rate.py

@ -0,0 +1,86 @@
from rest_framework import serializers
from ..models.rate import Rate
class RateSerializer(serializers.ModelSerializer):
"""
Serializer for the Rate model.
"""
class Meta:
model = Rate
fields = ('id', 'service', 'content_id', 'rate', 'status', 'created_at', 'updated_at')
read_only_fields = ('id', 'created_at', 'updated_at')
def validate(self, data):
"""
Validate that the content exists in the specified service.
"""
service = data.get('service')
content_id = data.get('content_id')
if not Rate.validate_content_exists(service, content_id):
raise serializers.ValidationError(f"Content with ID {content_id} does not exist in {service} service.")
return data
def create(self, validated_data):
"""
Create or update a rate.
If a rate already exists for the user, service, and content_id, update it.
"""
user = self.context['request'].user
service = validated_data.get('service')
content_id = validated_data.get('content_id')
# Try to get an existing rate
try:
rate_obj = Rate.objects.get(
user=user,
service=service,
content_id=content_id
)
# Update existing rate
for attr, value in validated_data.items():
setattr(rate_obj, attr, value)
rate_obj.save()
return rate_obj
except Rate.DoesNotExist:
# Create new rate
return Rate.objects.create(user=user, **validated_data)
class RateStatusSerializer(serializers.Serializer):
"""
Serializer for checking if a user has rated a content and getting the rate value.
"""
service = serializers.ChoiceField(choices=Rate.ServiceChoices.choices)
content_id = serializers.IntegerField(min_value=1)
def validate(self, data):
"""
Validate that the content exists in the specified service.
"""
service = data.get('service')
content_id = data.get('content_id')
if not Rate.validate_content_exists(service, content_id):
raise serializers.ValidationError(f"Content with ID {content_id} does not exist in {service} service.")
return data
class AverageRateSerializer(serializers.Serializer):
"""
Serializer for getting the average rate of a content.
"""
service = serializers.ChoiceField(choices=Rate.ServiceChoices.choices)
content_id = serializers.IntegerField(min_value=1)
def validate(self, data):
"""
Validate that the content exists in the specified service.
"""
service = data.get('service')
content_id = data.get('content_id')
if not Rate.validate_content_exists(service, content_id):
raise serializers.ValidationError(f"Content with ID {content_id} does not exist in {service} service.")
return data

9
apps/bookmark/urls.py

@ -1,10 +1,17 @@
from django.urls import path
from .views import AddBookmarkView, RemoveBookmarkView, BookmarkStatusView
from .views import AddBookmarkView, RemoveBookmarkView, BookmarkStatusView, AddRateView, RemoveRateView, RateStatusView, AverageRateView
app_name = 'bookmark'
urlpatterns = [
# Bookmark URLs
path('add/', AddBookmarkView.as_view(), name='add_bookmark'),
path('remove/', RemoveBookmarkView.as_view(), name='remove_bookmark'),
path('status/', BookmarkStatusView.as_view(), name='bookmark_status'),
# Rate URLs
path('rate/add/', AddRateView.as_view(), name='add_rate'),
path('rate/remove/', RemoveRateView.as_view(), name='remove_rate'),
# path('rate/status/', RateStatusView.as_view(), name='rate_status'),
# path('rate/average/', AverageRateView.as_view(), name='average_rate'),
]

2
apps/bookmark/views/__init__.py

@ -0,0 +1,2 @@
from .bookmark import *
from .rate import *

4
apps/bookmark/views.py → apps/bookmark/views/bookmark.py

@ -5,8 +5,8 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.generics import CreateAPIView, DestroyAPIView
from rest_framework.exceptions import ValidationError
from .models import Bookmark
from .serializers import BookmarkSerializer
from apps.bookmark.models import Bookmark
from apps.bookmark.serializers import BookmarkSerializer
class AddBookmarkView(CreateAPIView):

89
apps/bookmark/views/rate.py

@ -0,0 +1,89 @@
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from apps.bookmark.models import Rate
from apps.bookmark.serializers import RateSerializer, RateStatusSerializer, AverageRateSerializer
class AddRateView(APIView):
"""
API view for adding or updating a rate.
"""
permission_classes = [IsAuthenticated]
def post(self, request):
"""
Add or update a rate for a content.
"""
serializer = RateSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RemoveRateView(APIView):
"""
API view for removing a rate.
"""
permission_classes = [IsAuthenticated]
def post(self, request):
"""
Remove a rate by setting its status to False.
"""
serializer = RateStatusSerializer(data=request.data)
if serializer.is_valid():
service = serializer.validated_data['service']
content_id = serializer.validated_data['content_id']
try:
rate = Rate.objects.get(
user=request.user,
service=service,
content_id=content_id
)
rate.status = False
rate.save()
return Response({'message': 'Rate removed successfully'}, status=status.HTTP_200_OK)
except Rate.DoesNotExist:
return Response({'message': 'Rate not found'}, status=status.HTTP_404_NOT_FOUND)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RateStatusView(APIView):
"""
API view for checking if a user has rated a content and getting the rate value.
"""
permission_classes = [IsAuthenticated]
def post(self, request):
"""
Check if a user has rated a content and get the rate value.
"""
serializer = RateStatusSerializer(data=request.data)
if serializer.is_valid():
service = serializer.validated_data['service']
content_id = serializer.validated_data['content_id']
rate_info = Rate.get_user_rate(request.user, service, content_id)
return Response(rate_info, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AverageRateView(APIView):
"""
API view for getting the average rate of a content.
"""
def post(self, request):
"""
Get the average rate of a content.
"""
serializer = AverageRateSerializer(data=request.data)
if serializer.is_valid():
service = serializer.validated_data['service']
content_id = serializer.validated_data['content_id']
avg_rate = Rate.get_average_rate(service, content_id)
return Response({'average_rate': avg_rate}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

10
apps/library/admin.py

@ -57,13 +57,19 @@ class BookAdmin(ModelAdmin):
fieldsets = (
(None, {
'fields': ('title', 'summary', 'description', 'thumbnail', 'pages_count')
'fields': ()
}),
('Detail', {
'fields': ('title', 'slogan', 'thumbnail', 'pages_count', 'publisher', 'year_of_publication', 'isbn', 'numnber_of_volume')
}),
("Summary", {
'fields': ('summary_title', 'summary')
}),
(_('Status'), {
'fields': ('status', 'pin')
}),
(_('File Information'), {
'fields': ('file_type', 'book_file')
'fields': ('file_type', 'book_file', )
}),
(_('Relations'), {
'fields': ('categories', 'collections')

52
apps/library/migrations/0006_remove_book_author_book_isbn_book_numnber_of_volume_and_more.py

@ -0,0 +1,52 @@
# Generated by Django 5.1.8 on 2025-05-04 15:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0005_bookdownload_delete_bottombookcollection_and_more'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='author',
),
migrations.AddField(
model_name='book',
name='isbn',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='numnber_of_volume',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='book',
name='publisher',
field=models.CharField(blank=True, max_length=655, null=True),
),
migrations.AddField(
model_name='book',
name='slogan',
field=models.CharField(blank=True, max_length=300, null=True),
),
migrations.AddField(
model_name='book',
name='summary_title',
field=models.CharField(blank=True, help_text='Summary Title', max_length=512, null=True),
),
migrations.AddField(
model_name='book',
name='year_of_publication',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='book',
name='summary',
field=models.CharField(blank=True, help_text='Summary', max_length=512, null=True),
),
]

15
apps/library/models.py

@ -98,11 +98,20 @@ class Book(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('could be null'))
slogan = models.CharField(max_length=300, blank=True, null=True)
summary_title = models.CharField(max_length=512, null=True, blank=True, help_text=_('Summary Title'))
summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('Summary'))
description = models.TextField(null=True, blank=True, help_text=_('could be null'))
thumbnail = models.ImageField(upload_to='book_thumbnails/', null=True, blank=True, help_text=_('image allowed'))
author = models.CharField(max_length=255, null=True, blank=True)
publisher = models.CharField(max_length=655, null=True, blank=True)
year_of_publication = models.CharField(max_length=255, null=True, blank=True)
# author = models.CharField(max_length=255, null=True, blank=True)
isbn = models.CharField(max_length=255, null=True, blank=True)
numnber_of_volume = models.CharField(max_length=255, null=True, blank=True)
pages_count = models.CharField(verbose_name=_('Number of Pages'), max_length=255, help_text=_('eg. 34'), null=True)
status = models.BooleanField(default=True, verbose_name=_('status'))
pin = models.BooleanField(default=True, verbose_name=_('Pin to top'))

48
apps/library/serializers.py

@ -48,6 +48,8 @@ class PinnedBookCollectionSerializer(serializers.ModelSerializer):
class BookSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
bookmark = serializers.SerializerMethodField()
user_rate = serializers.SerializerMethodField()
average_rate = serializers.SerializerMethodField()
def get_thumbnail(self, obj):
if obj.thumbnail:
@ -57,9 +59,10 @@ class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = (
'id', 'title', 'slug', 'summary', 'description', 'thumbnail',
'author', 'status', 'pin', 'view_count', 'download_count',
'file_type', 'book_file', 'created_at', 'bookmark'
'id', 'title', 'slug', 'summary', 'summary_title', 'thumbnail', 'slogan',
'status', 'pin', 'view_count', 'download_count', 'publisher', 'year_of_publication', 'isbn', 'numnber_of_volume',
'file_type', 'book_file', 'created_at', 'bookmark', 'user_rate',
'average_rate'
)
def get_bookmark(self, obj):
@ -75,6 +78,45 @@ class BookSerializer(serializers.ModelSerializer):
service='library'
)
return book_mark.get('is_bookmarked', False)
def get_user_rate(self, obj):
"""
Get rate information for this book from the current user.
"""
from apps.bookmark.models.rate import Rate
# Get the current user from the request context
request = self.context.get('request')
user = request.user if request and request.user.is_authenticated else None
if not user:
return {
'is_rated': False,
'rate': None
}
# Get rate information using the Rate model's method
rate_info = Rate.get_user_rate(
user=user,
service='library',
content_id=obj.id
)
return rate_info
def get_average_rate(self, obj):
"""
Get the average rate for this book.
"""
from apps.bookmark.models.rate import Rate
# Get average rate using the Rate model's method
avg_rate = Rate.get_average_rate(
service='library',
content_id=obj.id
)
return avg_rate

Loading…
Cancel
Save