329 lines
11 KiB
Python
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,
|
|
})
|