From d4472b4199b9f59d371243757bb865e0d212f638 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 13 Jul 2026 13:11:24 +0330 Subject: [PATCH] dashboard api added --- apps/api/views/__init__.py | 3 + apps/api/views/admin_dashboard.py | 227 ++++++++++++++++++++++++++++++ config/urls.py | 3 +- manage.py | 2 + 4 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 apps/api/views/admin_dashboard.py diff --git a/apps/api/views/__init__.py b/apps/api/views/__init__.py index 5a58caa..b9fe830 100644 --- a/apps/api/views/__init__.py +++ b/apps/api/views/__init__.py @@ -4,6 +4,8 @@ from .api_views import HomeView, CountryView, CommentListAPIView, CityListView from .documentation import CustomAPIDocumentationView from .swagger_views import CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth +from .admin_dashboard import AdminDashboardStatsView + __all__ = [ 'HomeView', 'CountryView', @@ -13,4 +15,5 @@ __all__ = [ 'CustomSwaggerView', 'SwaggerTokenAuthView', 'clear_swagger_auth', + 'AdminDashboardStatsView', ] diff --git a/apps/api/views/admin_dashboard.py b/apps/api/views/admin_dashboard.py new file mode 100644 index 0000000..28dd5c5 --- /dev/null +++ b/apps/api/views/admin_dashboard.py @@ -0,0 +1,227 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from django.utils import timezone +from datetime import timedelta +from django.db.models import Count, Sum, Q +from django.db.models.functions import TruncDate +from rest_framework.authentication import TokenAuthentication + +from apps.account.models import User, LoginHistory +from apps.hadis.models import Hadis, HadisStatus, HadisCategory, Transmitters, BookReference +from apps.library.models import Book +from apps.article.models import Article +from apps.video.models import Video +from apps.podcast.models import Podcast +from apps.bookmark.models import Bookmark + +def extract_text_from_json(value): + if not value: + return "" + if isinstance(value, list): + for item in value: + if isinstance(item, dict): + text = item.get('title') or item.get('value') or item.get('text') or item.get('name') + if text: + return str(text) + else: + if item: + return str(item) + return "" + if isinstance(value, dict): + for lang in ("fa", "en", "ru"): + if lang in value and value[lang]: + v = value[lang] + if isinstance(v, dict): + return str(v.get('title') or v.get('value') or v.get('text') or v.get('name') or "") + return str(v) + for v in value.values(): + if isinstance(v, dict): + txt = v.get('title') or v.get('value') or v.get('text') or v.get('name') + if txt: + return str(txt) + elif v: + return str(v) + return "" + if isinstance(value, (str, int, float)): + return str(value) + return "" + +class AdminDashboardStatsView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated, IsAdminUser] + + def get(self, request): + now = timezone.now() + thirty_days_ago = now - timedelta(days=30) + + # --------------------------------------------------------- + # 1. CARDS & BASIC STATS + # --------------------------------------------------------- + total_arguments = Hadis.objects.count() + library_books = Book.objects.count() + source_references = BookReference.objects.count() + total_transmitters = Transmitters.objects.count() + + # Media Items: Video + Podcast count + media_items = Video.objects.count() + Podcast.objects.count() + + scientific_articles = Article.objects.count() + registered_users = User.objects.filter(is_active=True).count() + total_bookmarks = Bookmark.objects.filter(status=True).count() + + # --------------------------------------------------------- + # 2. CHARTS DATA + # --------------------------------------------------------- + + # Argument Types (Donut Chart 1) + argument_types_qs = Hadis.objects.exclude(category__isnull=True).values('category__source_type').annotate(total=Count('id')) + argument_types_chart = [ + {"name": item['category__source_type'], "value": item['total']} + for item in argument_types_qs + ] + + # Hadith Status (Donut Chart 2) + hadith_statuses_qs = Hadis.objects.filter( + category__source_type=HadisCategory.SourceType.HADITH, + hadis_status__isnull=False + ).values('hadis_status__slug', 'hadis_status__title').annotate(total=Count('id')) + + hadith_status_chart = [] + for item in hadith_statuses_qs: + title_text = extract_text_from_json(item['hadis_status__title']) if item['hadis_status__title'] else item['hadis_status__slug'] + hadith_status_chart.append({ + "name": title_text, + "value": item['total'] + }) + + # User Distribution (Donut Chart) + registered_count = User.objects.filter(is_active=True).exclude(email__isnull=True).exclude(email='').count() + guest_count = User.objects.filter(is_active=True).filter(Q(email__isnull=True) | Q(email='')).count() + user_distribution_chart = [ + {"name": "registered", "value": registered_count}, + {"name": "guest", "value": guest_count} + ] + + # New User Registrations Trend (Line Chart) + registrations = User.objects.filter(date_joined__gte=thirty_days_ago).annotate( + date=TruncDate('date_joined') + ).values('date').annotate(count=Count('id')).order_by('date') + registration_chart = [{"date": item['date'].strftime('%Y-%m-%d'), "users": item['count']} for item in registrations] + + # User Bookmark Activity Trend (Area Chart) + bookmarks_trend = Bookmark.objects.filter(created_at__gte=thirty_days_ago, status=True).annotate( + date=TruncDate('created_at') + ).values('date').annotate(count=Count('id')).order_by('date') + chat_chart = [{"date": item['date'].strftime('%Y-%m-%d'), "messages": item['count']} for item in bookmarks_trend] + + # --------------------------------------------------------- + # 3. LISTS & LEADERBOARDS + # --------------------------------------------------------- + + # Top 5 Media Items (List) + top_videos = Video.objects.order_by('-view_count')[:5] + top_media_list = [] + for v in top_videos: + title_text = extract_text_from_json(v.title) if v.title else "Untitled Video" + top_media_list.append({ + "id": v.id, + "title": title_text[:40] + "..." if len(title_text) > 40 else title_text, + "professor": "video", + "participants": v.view_count + }) + + top_podcasts = Podcast.objects.order_by('-view_count')[:5] + for p in top_podcasts: + title_text = extract_text_from_json(p.title) if p.title else "Untitled Podcast" + top_media_list.append({ + "id": p.id, + "title": title_text[:40] + "..." if len(title_text) > 40 else title_text, + "professor": "podcast", + "participants": p.view_count + }) + top_media_list = sorted(top_media_list, key=lambda x: x['participants'], reverse=True)[:5] + + # Top 5 Articles (List) + top_articles = Article.objects.order_by('-view_count')[:5] + top_articles_list = [] + for a in top_articles: + title_text = extract_text_from_json(a.title) if a.title else "Untitled Article" + top_articles_list.append({ + "id": a.id, + "title": title_text[:40] + "..." if len(title_text) > 40 else title_text, + "views": a.view_count + }) + top_articles_list = sorted(top_articles_list, key=lambda x: x['views'], reverse=True)[:5] + + # Top 5 Books (List) + top_books = Book.objects.order_by('-view_count')[:5] + top_books_list = [] + for b in top_books: + title_text = extract_text_from_json(b.title) if b.title else "Untitled Book" + top_books_list.append({ + "id": b.id, + "title": title_text[:40] + "..." if len(title_text) > 40 else title_text, + "views": b.view_count + }) + top_books_list = sorted(top_books_list, key=lambda x: x['views'], reverse=True)[:5] + + # Top Bookmarked Arguments (Leaderboard) + top_hadis_ids = Bookmark.objects.filter( + service='hadith', + status=True + ).values('content_id').annotate( + count=Count('id') + ).order_by('-count')[:5] + + top_hadis_list = [] + id_to_count = {item['content_id']: item['count'] for item in top_hadis_ids} + hadiths = Hadis.objects.filter(id__in=id_to_count.keys()) + for h in hadiths: + title_text = extract_text_from_json(h.title) if h.title else f"گزاره {h.number}" + top_hadis_list.append({ + "id": h.id, + "name": title_text[:30] + "..." if len(title_text) > 30 else title_text, + "score": id_to_count[h.id] + }) + top_hadis_list = sorted(top_hadis_list, key=lambda x: x['score'], reverse=True) + + # Fallback if no bookmarks exist + if not top_hadis_list: + hadiths = Hadis.objects.filter(status=True)[:5] + for h in hadiths: + title_text = extract_text_from_json(h.title) if h.title else f"گزاره {h.number}" + top_hadis_list.append({ + "id": h.id, + "name": title_text[:30] + "..." if len(title_text) > 30 else title_text, + "score": 0 + }) + + return Response({ + "cards": { + "active_students": total_arguments, # total_arguments + "professors": library_books, # library_books + "active_courses": source_references, # source_references + "revenue_30d": total_transmitters, # total_transmitters + "total_blogs": media_items, # media_items + "pending_certificates": scientific_articles, # scientific_articles + "active_live_sessions": registered_users, # registered_users + "group_rooms": total_bookmarks, # total_bookmarks + "private_rooms": 0, + }, + "charts": { + "transactions_status": argument_types_chart, + "hadith_statuses": hadith_status_chart, + "user_distribution": user_distribution_chart, + "registrations": registration_chart, + "chat_volume": chat_chart, + "course_pipeline": [], + "course_efficacy": [] + }, + "lists": { + "top_courses": top_media_list, + "top_blogs": top_articles_list, + "top_books": top_books_list, + "leaderboard": top_hadis_list + } + }) diff --git a/config/urls.py b/config/urls.py index 7ca2069..e26621c 100644 --- a/config/urls.py +++ b/config/urls.py @@ -53,7 +53,7 @@ sitemaps = { } # Import custom API views -from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth +from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth, AdminDashboardStatsView from apps.api.permissions import IsAdminOrSwaggerToken from apps.api.decorators import swagger_auth_required @@ -95,6 +95,7 @@ api_patterns = [ path('agent/', include('apps.agent.urls')), path('bookmarks/', include('apps.bookmark.urls')), path('calendar/', include('apps.dobodbi_calendar.urls')), + path('admin/dashboard-stats/', AdminDashboardStatsView.as_view()), path('settings/', include('dynamic_preferences.urls')), diff --git a/manage.py b/manage.py index 6361598..40c4a37 100755 --- a/manage.py +++ b/manage.py @@ -20,3 +20,5 @@ def main(): if __name__ == '__main__': main() + +# Force reload trigger for new dashboard views