from django_unicorn.components import UnicornView from django.contrib.contenttypes.models import ContentType from django.db.models import Q, Count from django.core.paginator import Paginator from typing import Any, Dict, List, Optional import logging # Note: We'll need to check if these imports exist and create them if needed try: from apps.moderation.models import Submission except ImportError: # Fallback - we'll create a basic structure Submission = None try: from apps.moderation.services import ModerationService except ImportError: # We'll create this service ModerationService = None logger = logging.getLogger(__name__) class ModerationDashboardView(UnicornView): """ Main moderation dashboard component handling submission management, filtering, bulk actions, and real-time updates. Replaces complex HTMX template with reactive Django Unicorn component. """ # Status management current_status: str = "PENDING" status_counts: Dict[str, int] = {} # Submissions data (converted to list for caching compatibility) submissions: List[Dict] = [] total_submissions: int = 0 # Filtering state submission_type: str = "" content_type: str = "" type_filter: str = "" search_query: str = "" # Bulk actions selected_submissions: List[int] = [] bulk_action: str = "" # UI state show_mobile_filters: bool = False loading: bool = False error_message: str = "" # Toast notifications toast_message: str = "" toast_type: str = "success" # success, error, warning, info show_toast_notification: bool = False # Pagination current_page: int = 1 items_per_page: int = 20 total_pages: int = 1 def mount(self): """Initialize component on mount""" try: self.load_status_counts() self.load_submissions() except Exception as e: logger.error(f"Error mounting moderation dashboard: {e}") self.show_error("Failed to load dashboard data") def hydrate(self): """Recalculate data after state changes""" try: self.calculate_pagination() except Exception as e: logger.error(f"Error hydrating moderation dashboard: {e}") def load_status_counts(self): """Load submission counts for each status""" try: if Submission is not None: counts = Submission.objects.values('status').annotate(count=Count('id')) self.status_counts = { 'PENDING': 0, 'APPROVED': 0, 'REJECTED': 0, 'ESCALATED': 0 } for item in counts: if item['status'] in self.status_counts: self.status_counts[item['status']] = item['count'] else: # Fallback for development self.status_counts = {'PENDING': 5, 'APPROVED': 10, 'REJECTED': 2, 'ESCALATED': 1} except Exception as e: logger.error(f"Error loading status counts: {e}") self.status_counts = {'PENDING': 0, 'APPROVED': 0, 'REJECTED': 0, 'ESCALATED': 0} def get_filtered_queryset(self): """Get filtered queryset based on current filters""" if Submission is None: # Return empty queryset for development from django.db import models return models.QuerySet(model=None).none() queryset = Submission.objects.select_related( 'user', 'user__profile', 'content_type', 'handled_by' ).prefetch_related('content_object') # Status filter if self.current_status: queryset = queryset.filter(status=self.current_status) # Submission type filter if self.submission_type: queryset = queryset.filter(submission_type=self.submission_type) # Content type filter if self.content_type: try: ct = ContentType.objects.get(model=self.content_type) queryset = queryset.filter(content_type=ct) except ContentType.DoesNotExist: pass # Type filter (CREATE/EDIT) if self.type_filter: queryset = queryset.filter(submission_type=self.type_filter) # Search query if self.search_query: queryset = queryset.filter( Q(reason__icontains=self.search_query) | Q(user__username__icontains=self.search_query) | Q(notes__icontains=self.search_query) ) return queryset.order_by('-created_at') def load_submissions(self): """Load submissions with current filters and pagination""" try: self.loading = True queryset = self.get_filtered_queryset() # Pagination paginator = Paginator(queryset, self.items_per_page) self.total_pages = paginator.num_pages self.total_submissions = paginator.count # Ensure current page is valid if self.current_page > self.total_pages and self.total_pages > 0: self.current_page = self.total_pages elif self.current_page < 1: self.current_page = 1 # Get page data if self.total_pages > 0: page = paginator.page(self.current_page) # Convert to list for caching compatibility if Submission is not None: self.submissions = list(page.object_list.values( 'id', 'status', 'submission_type', 'content_type__model', 'user__username', 'created_at', 'reason', 'source', 'notes', 'handled_by__username', 'changes' )) else: # Fallback data for development self.submissions = [ { 'id': 1, 'status': 'PENDING', 'submission_type': 'CREATE', 'content_type__model': 'park', 'user__username': 'testuser', 'created_at': '2025-01-31', 'reason': 'New park submission', 'source': 'https://example.com', 'notes': '', 'handled_by__username': None, 'changes': {} } ] else: self.submissions = [] self.calculate_pagination() self.loading = False except Exception as e: logger.error(f"Error loading submissions: {e}") self.show_error("Failed to load submissions") self.loading = False def calculate_pagination(self): """Calculate pagination display values""" if self.total_submissions == 0: self.total_pages = 1 return import math self.total_pages = math.ceil(self.total_submissions / self.items_per_page) # Status navigation methods def on_status_change(self, status: str): """Handle status tab change""" if status != self.current_status: self.current_status = status self.current_page = 1 self.selected_submissions = [] self.load_submissions() # Filter methods def on_filter_change(self): """Handle filter changes""" self.current_page = 1 self.selected_submissions = [] self.load_submissions() def toggle_mobile_filters(self): """Toggle mobile filter visibility""" self.show_mobile_filters = not self.show_mobile_filters def clear_filters(self): """Clear all filters""" self.submission_type = "" self.content_type = "" self.type_filter = "" self.search_query = "" self.on_filter_change() # Search methods def on_search(self, query: str): """Handle search query change (debounced)""" self.search_query = query.strip() self.current_page = 1 self.load_submissions() # Pagination methods def on_page_changed(self, page: int): """Handle page change""" if 1 <= page <= self.total_pages: self.current_page = page self.load_submissions() def go_to_page(self, page: int): """Navigate to specific page""" self.on_page_changed(page) def go_to_previous(self): """Navigate to previous page""" if self.current_page > 1: self.on_page_changed(self.current_page - 1) def go_to_next(self): """Navigate to next page""" if self.current_page < self.total_pages: self.on_page_changed(self.current_page + 1) # Selection methods def toggle_submission_selection(self, submission_id: int): """Toggle submission selection for bulk actions""" if submission_id in self.selected_submissions: self.selected_submissions.remove(submission_id) else: self.selected_submissions.append(submission_id) def select_all_submissions(self): """Select all submissions on current page""" self.selected_submissions = [s['id'] for s in self.submissions] def clear_selection(self): """Clear all selected submissions""" self.selected_submissions = [] # Bulk action methods def bulk_approve(self): """Bulk approve selected submissions""" if not self.selected_submissions: self.show_toast("No submissions selected", "warning") return try: if ModerationService is not None: count = ModerationService.bulk_approve_submissions( self.selected_submissions, self.request.user ) else: count = len(self.selected_submissions) self.selected_submissions = [] self.load_submissions() self.load_status_counts() self.show_toast(f"Successfully approved {count} submissions", "success") except Exception as e: logger.error(f"Error bulk approving submissions: {e}") self.show_toast("Failed to approve submissions", "error") def bulk_reject(self): """Bulk reject selected submissions""" if not self.selected_submissions: self.show_toast("No submissions selected", "warning") return try: if ModerationService is not None: count = ModerationService.bulk_reject_submissions( self.selected_submissions, self.request.user ) else: count = len(self.selected_submissions) self.selected_submissions = [] self.load_submissions() self.load_status_counts() self.show_toast(f"Successfully rejected {count} submissions", "success") except Exception as e: logger.error(f"Error bulk rejecting submissions: {e}") self.show_toast("Failed to reject submissions", "error") def bulk_escalate(self): """Bulk escalate selected submissions""" if not self.selected_submissions: self.show_toast("No submissions selected", "warning") return try: if ModerationService is not None: count = ModerationService.bulk_escalate_submissions( self.selected_submissions, self.request.user ) else: count = len(self.selected_submissions) self.selected_submissions = [] self.load_submissions() self.load_status_counts() self.show_toast(f"Successfully escalated {count} submissions", "success") except Exception as e: logger.error(f"Error bulk escalating submissions: {e}") self.show_toast("Failed to escalate submissions", "error") # Individual submission actions def approve_submission(self, submission_id: int, notes: str = ""): """Approve individual submission""" try: if ModerationService is not None: ModerationService.approve_submission( submission_id, self.request.user, notes) self.load_submissions() self.load_status_counts() self.show_toast("Submission approved successfully", "success") except Exception as e: logger.error(f"Error approving submission {submission_id}: {e}") self.show_toast("Failed to approve submission", "error") def reject_submission(self, submission_id: int, notes: str = ""): """Reject individual submission""" try: if ModerationService is not None: ModerationService.reject_submission( submission_id, self.request.user, notes) self.load_submissions() self.load_status_counts() self.show_toast("Submission rejected", "success") except Exception as e: logger.error(f"Error rejecting submission {submission_id}: {e}") self.show_toast("Failed to reject submission", "error") def escalate_submission(self, submission_id: int, notes: str = ""): """Escalate individual submission""" try: if ModerationService is not None: ModerationService.escalate_submission( submission_id, self.request.user, notes) self.load_submissions() self.load_status_counts() self.show_toast("Submission escalated", "success") except Exception as e: logger.error(f"Error escalating submission {submission_id}: {e}") self.show_toast("Failed to escalate submission", "error") # Utility methods def refresh_data(self): """Refresh all dashboard data""" self.load_status_counts() self.load_submissions() self.show_toast("Dashboard refreshed", "info") def show_toast(self, message: str, toast_type: str = "success"): """Show toast notification""" self.toast_message = message self.toast_type = toast_type self.show_toast_notification = True def hide_toast(self): """Hide toast notification""" self.show_toast_notification = False self.toast_message = "" def show_error(self, message: str): """Show error message""" self.error_message = message self.loading = False # Properties for template @property def has_submissions(self) -> bool: """Check if there are any submissions""" return len(self.submissions) > 0 @property def has_selected_submissions(self) -> bool: """Check if any submissions are selected""" return len(self.selected_submissions) > 0 @property def selected_count(self) -> int: """Get count of selected submissions""" return len(self.selected_submissions) @property def active_filter_count(self) -> int: """Get count of active filters""" count = 0 if self.submission_type: count += 1 if self.content_type: count += 1 if self.type_filter: count += 1 if self.search_query: count += 1 return count @property def has_previous_page(self) -> bool: """Check if there's a previous page""" return self.current_page > 1 @property def has_next_page(self) -> bool: """Check if there's a next page""" return self.current_page < self.total_pages @property def showing_text(self) -> str: """Get text showing current range""" if self.total_submissions == 0: return "No submissions found" start = (self.current_page - 1) * self.items_per_page + 1 end = min(self.current_page * self.items_per_page, self.total_submissions) return f"Showing {start:,} to {end:,} of {self.total_submissions:,} submissions"