feat(favorites): add EventLike model, favorites API, and notifications module
- EventLike model (user × event unique constraint, indexed) - contributed_by field on Event (EVT ID or email of community contributor) - Favorites API endpoints: toggle-like, my-likes, my-liked-events - Notifications app wired into main urls.py at /api/notifications/ - accounts migration 0014_merge_0013 (resolves split 0013 branches) - requirements.txt updated
This commit is contained in:
13
accounts/migrations/0014_merge_0013.py
Normal file
13
accounts/migrations/0014_merge_0013.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
"""Merge migration to resolve conflicting 0013 migrations."""
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0013_merge_eventify_id'),
|
||||||
|
('accounts', '0013_user_district_changed_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
@@ -36,6 +36,7 @@ urlpatterns = [
|
|||||||
path('banking/', include('banking_operations.urls')),
|
path('banking/', include('banking_operations.urls')),
|
||||||
path('api/', include('mobile_api.urls')),
|
path('api/', include('mobile_api.urls')),
|
||||||
path('api/v1/', include('admin_api.urls')),
|
path('api/v1/', include('admin_api.urls')),
|
||||||
|
path('api/notifications/', include('notifications.urls')),
|
||||||
# path('web-api/', include('web_api.urls')),
|
# path('web-api/', include('web_api.urls')),
|
||||||
|
|
||||||
path('summernote/', include('django_summernote.urls')),
|
path('summernote/', include('django_summernote.urls')),
|
||||||
|
|||||||
73
events/migrations/0011_event_contributed_by.py
Normal file
73
events/migrations/0011_event_contributed_by.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Add contributed_by field to Event and backfill from overloaded source field.
|
||||||
|
|
||||||
|
The admin dashboard stores community contributor identifiers (EVT-XXXXXXXX or email)
|
||||||
|
in the source field. This migration:
|
||||||
|
1. Adds a dedicated contributed_by CharField
|
||||||
|
2. Copies user identifiers from source → contributed_by
|
||||||
|
3. Normalizes source back to its intended choices ('eventify', 'community', 'partner')
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_contributed_by(apps, schema_editor):
|
||||||
|
"""Move user identifiers from source to contributed_by."""
|
||||||
|
Event = apps.get_model('events', 'Event')
|
||||||
|
|
||||||
|
STANDARD_SOURCES = {'eventify', 'community', 'partner', 'eventify_team', 'official', ''}
|
||||||
|
|
||||||
|
for event in Event.objects.all().iterator():
|
||||||
|
source_val = (event.source or '').strip()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
# User identifier: contains @ (email) or starts with EVT- (eventifyId)
|
||||||
|
if source_val and source_val not in STANDARD_SOURCES and not source_val.startswith('partner:'):
|
||||||
|
event.contributed_by = source_val
|
||||||
|
event.source = 'community'
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Normalize eventify_team → eventify
|
||||||
|
elif source_val == 'eventify_team':
|
||||||
|
event.source = 'eventify'
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Normalize official → eventify
|
||||||
|
elif source_val == 'official':
|
||||||
|
event.source = 'eventify'
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
event.save(update_fields=['source', 'contributed_by'])
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_backfill(apps, schema_editor):
|
||||||
|
"""Reverse: move contributed_by back to source."""
|
||||||
|
Event = apps.get_model('events', 'Event')
|
||||||
|
for event in Event.objects.exclude(contributed_by__isnull=True).exclude(contributed_by='').iterator():
|
||||||
|
event.source = event.contributed_by
|
||||||
|
event.contributed_by = None
|
||||||
|
event.save(update_fields=['source', 'contributed_by'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0010_merge_20260324_1443'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Step 1: Add the field
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='contributed_by',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text='Eventify ID (EVT-XXXXXXXX) or email of the community contributor',
|
||||||
|
max_length=100,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Step 2: Backfill data
|
||||||
|
migrations.RunPython(backfill_contributed_by, reverse_backfill),
|
||||||
|
]
|
||||||
38
events/migrations/0012_eventlike.py
Normal file
38
events/migrations/0012_eventlike.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0011_event_contributed_by'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventLike',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('event', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='likes',
|
||||||
|
to='events.event',
|
||||||
|
)),
|
||||||
|
('user', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='event_likes',
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('user', 'event')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='eventlike',
|
||||||
|
index=models.Index(fields=['user', '-created_at'], name='events_even_user_id_created_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -58,6 +58,11 @@ class Event(models.Model):
|
|||||||
is_featured = models.BooleanField(default=False, help_text='Show this event in the featured section')
|
is_featured = models.BooleanField(default=False, help_text='Show this event in the featured section')
|
||||||
is_top_event = models.BooleanField(default=False, help_text='Show this event in the Top Events section')
|
is_top_event = models.BooleanField(default=False, help_text='Show this event in the Top Events section')
|
||||||
|
|
||||||
|
contributed_by = models.CharField(
|
||||||
|
max_length=100, blank=True, null=True,
|
||||||
|
help_text='Eventify ID (EVT-XXXXXXXX) or email of the community contributor',
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.start_date})"
|
return f"{self.name} ({self.start_date})"
|
||||||
|
|
||||||
@@ -71,3 +76,26 @@ class EventImages(models.Model):
|
|||||||
return f"{self.event_image}"
|
return f"{self.event_image}"
|
||||||
|
|
||||||
|
|
||||||
|
class EventLike(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
'accounts.User',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='event_likes'
|
||||||
|
)
|
||||||
|
event = models.ForeignKey(
|
||||||
|
Event,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='likes'
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('user', 'event')
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['user', '-created_at']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.email} likes {self.event.name}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.urls import path
|
|||||||
from .views import *
|
from .views import *
|
||||||
from mobile_api.views.user import ScheduleCallView
|
from mobile_api.views.user import ScheduleCallView
|
||||||
from mobile_api.views.reviews import ReviewSubmitView, MobileReviewListView, ReviewHelpfulView, ReviewFlagView
|
from mobile_api.views.reviews import ReviewSubmitView, MobileReviewListView, ReviewHelpfulView, ReviewFlagView
|
||||||
|
from mobile_api.views.favorites import ToggleLikeView, MyLikedIdsView, MyLikedEventsView
|
||||||
from ad_control.views import ConsumerFeaturedEventsView, ConsumerTopEventsView
|
from ad_control.views import ConsumerFeaturedEventsView, ConsumerTopEventsView
|
||||||
|
|
||||||
|
|
||||||
@@ -39,3 +40,10 @@ urlpatterns += [
|
|||||||
path('reviews/helpful', ReviewHelpfulView.as_view()),
|
path('reviews/helpful', ReviewHelpfulView.as_view()),
|
||||||
path('reviews/flag', ReviewFlagView.as_view()),
|
path('reviews/flag', ReviewFlagView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Favorites URLs
|
||||||
|
urlpatterns += [
|
||||||
|
path('events/like/', ToggleLikeView.as_view()),
|
||||||
|
path('events/my-likes/', MyLikedIdsView.as_view()),
|
||||||
|
path('events/my-liked-events/', MyLikedEventsView.as_view()),
|
||||||
|
]
|
||||||
|
|||||||
146
mobile_api/views/favorites.py
Normal file
146
mobile_api/views/favorites.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from django.views import View
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
|
||||||
|
from events.models import Event, EventLike, EventImages
|
||||||
|
from mobile_api.utils import validate_token_and_get_user
|
||||||
|
from eventify_logger.services import log
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_liked_event(event):
|
||||||
|
"""Serialize an Event for the liked-events list."""
|
||||||
|
primary_img = EventImages.objects.filter(
|
||||||
|
event=event, is_primary=True
|
||||||
|
).first()
|
||||||
|
if not primary_img:
|
||||||
|
primary_img = EventImages.objects.filter(event=event).first()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': event.id,
|
||||||
|
'title': event.title or event.name,
|
||||||
|
'image': primary_img.event_image.url if primary_img else '',
|
||||||
|
'date': str(event.start_date) if event.start_date else None,
|
||||||
|
'location': event.place or '',
|
||||||
|
'venue': event.venue_name or '',
|
||||||
|
'event_type': event.event_type.event_type if event.event_type else '',
|
||||||
|
'event_status': event.event_status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class ToggleLikeView(View):
|
||||||
|
"""POST /api/events/like/ — toggle like on/off for an event."""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
event_id = data.get('event_id')
|
||||||
|
if not event_id:
|
||||||
|
return JsonResponse(
|
||||||
|
{'status': 'error', 'message': 'event_id is required'},
|
||||||
|
status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = Event.objects.get(pk=event_id)
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
return JsonResponse(
|
||||||
|
{'status': 'error', 'message': 'Event not found'},
|
||||||
|
status=404
|
||||||
|
)
|
||||||
|
|
||||||
|
like, created = EventLike.objects.get_or_create(user=user, event=event)
|
||||||
|
if not created:
|
||||||
|
like.delete()
|
||||||
|
return JsonResponse({'status': 'success', 'liked': False})
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'success', 'liked': True})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("error", "ToggleLikeView exception", request=request,
|
||||||
|
logger_data={"error": str(e)})
|
||||||
|
return JsonResponse(
|
||||||
|
{'status': 'error', 'message': 'An unexpected server error occurred.'},
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class MyLikedIdsView(View):
|
||||||
|
"""POST /api/events/my-likes/ — return all liked event IDs for the user."""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
liked_ids = list(
|
||||||
|
EventLike.objects.filter(user=user)
|
||||||
|
.values_list('event_id', flat=True)
|
||||||
|
)
|
||||||
|
return JsonResponse({'status': 'success', 'liked_event_ids': liked_ids})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("error", "MyLikedIdsView exception", request=request,
|
||||||
|
logger_data={"error": str(e)})
|
||||||
|
return JsonResponse(
|
||||||
|
{'status': 'error', 'message': 'An unexpected server error occurred.'},
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class MyLikedEventsView(View):
|
||||||
|
"""POST /api/events/my-liked-events/ — paginated liked events with full data."""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
page = int(data.get('page', 1))
|
||||||
|
page_size = min(int(data.get('page_size', 20)), 50)
|
||||||
|
|
||||||
|
# Event IDs liked by this user, newest first
|
||||||
|
liked_event_ids = list(
|
||||||
|
EventLike.objects.filter(user=user)
|
||||||
|
.order_by('-created_at')
|
||||||
|
.values_list('event_id', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Preserve ordering from liked_event_ids
|
||||||
|
from django.db.models import Case, When, IntegerField
|
||||||
|
ordering = Case(
|
||||||
|
*[When(pk=pk, then=pos) for pos, pk in enumerate(liked_event_ids)],
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
|
events_qs = Event.objects.filter(id__in=liked_event_ids).order_by(ordering)
|
||||||
|
|
||||||
|
paginator = Paginator(events_qs, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
events_data = [_serialize_liked_event(e) for e in page_obj]
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'status': 'success',
|
||||||
|
'events': events_data,
|
||||||
|
'total': paginator.count,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'has_next': page_obj.has_next(),
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("error", "MyLikedEventsView exception", request=request,
|
||||||
|
logger_data={"error": str(e)})
|
||||||
|
return JsonResponse(
|
||||||
|
{'status': 'error', 'message': 'An unexpected server error occurred.'},
|
||||||
|
status=500
|
||||||
|
)
|
||||||
0
notifications/__init__.py
Normal file
0
notifications/__init__.py
Normal file
10
notifications/admin.py
Normal file
10
notifications/admin.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Notification
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Notification)
|
||||||
|
class NotificationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('title', 'user', 'notification_type', 'is_read', 'created_at')
|
||||||
|
list_filter = ('notification_type', 'is_read', 'created_at')
|
||||||
|
search_fields = ('title', 'message', 'user__email')
|
||||||
|
readonly_fields = ('created_at',)
|
||||||
6
notifications/apps.py
Normal file
6
notifications/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'notifications'
|
||||||
25
notifications/models.py
Normal file
25
notifications/models.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.db import models
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(models.Model):
|
||||||
|
NOTIFICATION_TYPES = [
|
||||||
|
('event', 'Event'),
|
||||||
|
('promo', 'Promotion'),
|
||||||
|
('system', 'System'),
|
||||||
|
('booking', 'Booking'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
message = models.TextField()
|
||||||
|
notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES, default='system')
|
||||||
|
is_read = models.BooleanField(default=False)
|
||||||
|
action_url = models.URLField(blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.notification_type}: {self.title} → {self.user.email}"
|
||||||
8
notifications/urls.py
Normal file
8
notifications/urls.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import NotificationListView, NotificationMarkReadView, NotificationCountView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('list/', NotificationListView.as_view(), name='notification_list'),
|
||||||
|
path('mark-read/', NotificationMarkReadView.as_view(), name='notification_mark_read'),
|
||||||
|
path('count/', NotificationCountView.as_view(), name='notification_count'),
|
||||||
|
]
|
||||||
85
notifications/views.py
Normal file
85
notifications/views.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import json
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from mobile_api.utils import validate_token_and_get_user
|
||||||
|
from eventify_logger.services import log
|
||||||
|
from .models import Notification
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class NotificationListView(View):
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
page = int(data.get('page', 1))
|
||||||
|
page_size = int(data.get('page_size', 20))
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
notifications = Notification.objects.filter(user=user)[offset:offset + page_size]
|
||||||
|
total = Notification.objects.filter(user=user).count()
|
||||||
|
|
||||||
|
items = [{
|
||||||
|
'id': n.id,
|
||||||
|
'title': n.title,
|
||||||
|
'message': n.message,
|
||||||
|
'notification_type': n.notification_type,
|
||||||
|
'is_read': n.is_read,
|
||||||
|
'action_url': n.action_url or '',
|
||||||
|
'created_at': n.created_at.isoformat(),
|
||||||
|
} for n in notifications]
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'status': 'success',
|
||||||
|
'notifications': items,
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
log("error", "NotificationListView error", request=request, logger_data={"error": str(e)})
|
||||||
|
return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class NotificationMarkReadView(View):
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
mark_all = data.get('mark_all', False)
|
||||||
|
notification_id = data.get('notification_id')
|
||||||
|
|
||||||
|
if mark_all:
|
||||||
|
Notification.objects.filter(user=user, is_read=False).update(is_read=True)
|
||||||
|
return JsonResponse({'status': 'success', 'message': 'All notifications marked as read'})
|
||||||
|
|
||||||
|
if notification_id:
|
||||||
|
Notification.objects.filter(id=notification_id, user=user).update(is_read=True)
|
||||||
|
return JsonResponse({'status': 'success', 'message': 'Notification marked as read'})
|
||||||
|
|
||||||
|
return JsonResponse({'error': 'Provide notification_id or mark_all=true'}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
log("error", "NotificationMarkReadView error", request=request, logger_data={"error": str(e)})
|
||||||
|
return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class NotificationCountView(View):
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
count = Notification.objects.filter(user=user, is_read=False).count()
|
||||||
|
return JsonResponse({'status': 'success', 'unread_count': count})
|
||||||
|
except Exception as e:
|
||||||
|
log("error", "NotificationCountView error", request=request, logger_data={"error": str(e)})
|
||||||
|
return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
Django>=4.2
|
Django>=4.2
|
||||||
Pillow
|
Pillow
|
||||||
django-summernote
|
django-summernote
|
||||||
|
google-auth>=2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user