from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _ from django.utils import timezone from django.conf import settings from django.core.mail import send_mail from django.urls import reverse from django.http import FileResponse, Http404, JsonResponse import threading import os import logging from common.models import UserConfig from .models import GeocodingTask from .forms import GeocodingUploadForm from .geocoding import geocode_excel_file logger = logging.getLogger(__name__) @login_required def index(request): """ Schools view - main landing page """ user_config = get_object_or_404(UserConfig, user=request.user) # Check if user has access to schools view if not user_config.can_access_view('schools'): raise PermissionDenied(_("Vous n'avez pas accès à cette vue.")) context = { 'user_config': user_config, } return render(request, "schools/index.html", context) @login_required def geolocation(request): """ Geolocation view - Upload and process Excel files for geocoding """ user_config = get_object_or_404(UserConfig, user=request.user) # Check if user has access to schools view if not user_config.can_access_view('schools'): raise PermissionDenied(_("Vous n'avez pas accès à cette vue.")) if request.method == 'POST': form = GeocodingUploadForm(request.POST, request.FILES) if form.is_valid(): # Create the task task = form.save(commit=False) task.user = request.user task.status = 'pending' task.save() # Start geocoding in background thread # Note: Using non-daemon thread to prevent data loss on server shutdown thread = threading.Thread( target=process_geocoding_task, args=(task.id,), daemon=False ) thread.start() messages.success( request, _("Votre fichier a été uploadé avec succès. " "La géolocalisation est en cours. " "Vous recevrez un email lorsque le traitement sera terminé.") ) return redirect('schools:geolocation') else: form = GeocodingUploadForm() # Get user's tasks user_tasks = GeocodingTask.objects.filter(user=request.user).order_by('-created_at')[:10] context = { 'user_config': user_config, 'form': form, 'user_tasks': user_tasks, } return render(request, "schools/geolocation/index.html", context) def process_geocoding_task(task_id): """ Background task to process geocoding This runs in a separate thread """ task = None try: task = GeocodingTask.objects.get(id=task_id) # Update status to processing task.status = 'processing' task.started_at = timezone.now() task.save() # Get file paths - use PRIVATE_MEDIA_ROOT for secure storage input_path = task.uploaded_file.path output_filename = f"geocoded_{os.path.basename(task.uploaded_file.name)}" output_relative_path = os.path.join( 'schools/geocoding/outputs', timezone.now().strftime('%Y/%m/%d'), output_filename ) output_path = os.path.join( settings.PRIVATE_MEDIA_ROOT, output_relative_path ) # Ensure output directory exists os.makedirs(os.path.dirname(output_path), exist_ok=True) # Run geocoding logger.info(f"Starting geocoding task {task_id}") chunk_size = getattr(settings, 'SCHOOLS_GEOCODING_CHUNK_SIZE', 3) api_limit = getattr(settings, 'SCHOOLS_GEOCODING_API_LIMIT', 1) internal_threshold = getattr(settings, 'SCHOOLS_GEOCODING_INTERNAL_THRESHOLD', 80) use_internal = getattr(settings, 'SCHOOLS_GEOCODING_USE_INTERNAL', True) stats = geocode_excel_file( input_path=input_path, output_path=output_path, chunk_size=chunk_size, limit=api_limit, output_sheet="geocoded", internal_threshold=internal_threshold, use_internal=use_internal ) # Update task with results task.status = 'completed' task.completed_at = timezone.now() task.total_rows = stats['total'] task.successful_geocodes = stats['success'] task.failed_geocodes = stats['failed'] # Save the output file path relative to PRIVATE_MEDIA_ROOT task.output_file.name = output_relative_path task.save() logger.info(f"Completed geocoding task {task_id}") # Send email notification send_completion_email(task) except Exception as e: logger.error(f"Error processing geocoding task {task_id}: {str(e)}") try: # Retrieve task if not already loaded if task is None: task = GeocodingTask.objects.get(id=task_id) task.status = 'failed' task.completed_at = timezone.now() task.error_message = str(e) task.save() # Send error email send_error_email(task) except Exception as inner_e: logger.error(f"Error updating task status for task {task_id}: {str(inner_e)}") def send_completion_email(task): """Send email notification when geocoding is complete""" try: user_email = task.user.email if not user_email: logger.warning(f"No email address for user {task.user.username}") return subject = "Géolocalisation terminée - StreetUp" # Build download URL # Use ROOT_URL from settings root_url = getattr(settings, 'ROOT_URL', 'http://localhost:8000') download_url = f"{root_url.rstrip('/')}{reverse('schools:download_result', args=[task.id])}" message = f""" Bonjour {task.user.first_name or task.user.username}, Votre tâche de géolocalisation est terminée. Statistiques : - Total d'adresses : {task.total_rows} - Géolocalisations réussies : {task.successful_geocodes} - Échecs : {task.failed_geocodes} Vous pouvez télécharger le fichier résultat ici : {download_url} Cordialement, L'équipe StreetUp """ try: send_mail( subject=subject, message=message, from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@streetup.be'), recipient_list=[user_email], fail_silently=False, ) logger.info(f"Sent completion email to {user_email} for task {task.id}") except Exception as mail_error: logger.error(f"Failed to send completion email to {user_email} for task {task.id}: {str(mail_error)}") except Exception as e: logger.error(f"Error sending completion email for task {task.id}: {str(e)}") def send_error_email(task): """Send email notification when geocoding fails""" try: user_email = task.user.email if not user_email: logger.warning(f"No email address for user {task.user.username}") return subject = "Erreur lors de la géolocalisation - StreetUp" message = f""" Bonjour {task.user.first_name or task.user.username}, Une erreur est survenue lors du traitement de votre tâche de géolocalisation. Détails de l'erreur : {task.error_message} Veuillez réessayer ou contacter l'administrateur si le problème persiste. Cordialement, L'équipe StreetUp """ try: send_mail( subject=subject, message=message, from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@streetup.be'), recipient_list=[user_email], fail_silently=False, ) logger.info(f"Sent error email to {user_email} for task {task.id}") except Exception as mail_error: logger.error(f"Failed to send error email to {user_email} for task {task.id}: {str(mail_error)}") except Exception as e: logger.error(f"Error sending error email for task {task.id}: {str(e)}") @login_required def download_result(request, task_id): """ Download the result file for a completed geocoding task """ task = get_object_or_404(GeocodingTask, id=task_id) # Check permission - user must own the task if task.user != request.user: raise PermissionDenied(_("Vous n'avez pas accès à ce fichier.")) # Check if task is completed and has output file if task.status != 'completed' or not task.output_file: raise Http404(_("Le fichier résultat n'est pas disponible.")) # Serve the file if not os.path.exists(task.output_file.path): raise Http404(_("Le fichier résultat n'a pas été trouvé.")) # FileResponse will handle closing the file when done response = FileResponse( open(task.output_file.path, 'rb'), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, filename=os.path.basename(task.output_file.name) ) return response @login_required def geolocation_tasks_status(request): """ API endpoint to get the status of user's geocoding tasks. Used for auto-refresh functionality. Only returns tasks from the last 24 hours to limit payload size. """ # Only get tasks from the last 24 hours last_24h = timezone.now() - timezone.timedelta(hours=24) user_tasks = GeocodingTask.objects.filter( user=request.user, created_at__gte=last_24h ).order_by('-created_at')[:10] tasks_data = [] for task in user_tasks: # Get filename from uploaded_file filename = os.path.basename(task.uploaded_file.name) if task.uploaded_file else "-" # Build download URL if task is completed download_url = None if task.status == 'completed' and task.output_file: download_url = reverse('schools:download_result', args=[task.id]) tasks_data.append({ 'id': task.id, 'filename': filename, 'created_at': task.created_at.strftime('%d/%m/%Y %H:%M') if task.created_at else '-', 'status': task.status, 'total_rows': task.total_rows if task.total_rows else '-', 'successful_geocodes': task.successful_geocodes if task.successful_geocodes else '-', 'error_message': task.error_message if task.error_message else 'Erreur inconnue', 'download_url': download_url, }) # Check if any task is still pending or processing has_active_tasks = any(t.status in ['pending', 'processing'] for t in user_tasks) return JsonResponse({ 'tasks': tasks_data, 'has_active_tasks': has_active_tasks, })