loko/streetup/schools/views.py
2026-07-22 14:48:40 +02:00

329 lines
11 KiB
Python

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,
})