16 changed files with 599 additions and 16 deletions
-
140apps/bookmark/admin.py
-
35apps/bookmark/migrations/0002_rate.py
-
2apps/bookmark/models/__init__.py
-
2apps/bookmark/models/bookmark.py
-
117apps/bookmark/models/rate.py
-
2apps/bookmark/serializers/__init__.py
-
2apps/bookmark/serializers/bookmark.py
-
86apps/bookmark/serializers/rate.py
-
9apps/bookmark/urls.py
-
2apps/bookmark/views/__init__.py
-
4apps/bookmark/views/bookmark.py
-
89apps/bookmark/views/rate.py
-
10apps/library/admin.py
-
52apps/library/migrations/0006_remove_book_author_book_isbn_book_numnber_of_volume_and_more.py
-
15apps/library/models.py
-
48apps/library/serializers.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) |
|||
@ -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')}, |
|||
}, |
|||
), |
|||
] |
|||
@ -0,0 +1,2 @@ |
|||
from .bookmark import * |
|||
from .rate import * |
|||
@ -1,3 +1,5 @@ |
|||
|
|||
|
|||
from django.db import models |
|||
from django.contrib.auth import get_user_model |
|||
|
|||
@ -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'] |
|||
|
|||
@ -0,0 +1,2 @@ |
|||
from .bookmark import * |
|||
from .rate import * |
|||
@ -1,5 +1,5 @@ |
|||
from rest_framework import serializers |
|||
from .models import Bookmark |
|||
from apps.bookmark.models import Bookmark |
|||
|
|||
|
|||
class BookmarkSerializer(serializers.ModelSerializer): |
|||
@ -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 |
|||
@ -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'), |
|||
] |
|||
@ -0,0 +1,2 @@ |
|||
from .bookmark import * |
|||
from .rate import * |
|||
@ -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) |
|||
@ -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), |
|||
), |
|||
] |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue