From 1263e0a0c78e37828285ff640045349205028ef9 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Thu, 13 Feb 2025 10:26:33 +0100
Subject: [PATCH] Add export file functionality

---
 sage_validation/file_validator/forms.py       | 16 +++-
 .../file_validator/templates/upload.html      | 50 +++++++---
 sage_validation/file_validator/urls.py        |  3 +-
 sage_validation/file_validator/views.py       | 93 +++++++++++++++++--
 4 files changed, 140 insertions(+), 22 deletions(-)

diff --git a/sage_validation/file_validator/forms.py b/sage_validation/file_validator/forms.py
index 0e22efe..4234342 100644
--- a/sage_validation/file_validator/forms.py
+++ b/sage_validation/file_validator/forms.py
@@ -7,8 +7,13 @@ from typing import ClassVar
 from django import forms
 from django.core.files.uploadedfile import UploadedFile
 
-from sage_validation.file_validator.models import MeoCostCentres, MeoValidSageAccounts, XxData, MeoValidSuppliers, \
-    MeoNominal
+from sage_validation.file_validator.models import (
+    MeoCostCentres,
+    MeoNominal,
+    MeoValidSageAccounts,
+    MeoValidSuppliers,
+    XxData,
+)
 
 
 class CSVUploadForm(forms.Form):
@@ -176,14 +181,17 @@ class CSVUploadForm(forms.Form):
 
 
     def _validate_nc_cc_dep_combination_against_meo_sage_account(self, data: Iterable[dict]) -> list[str]:
-        """Validate that all occurrences of 'NominalAnalysisNominalCostCentre/{N}',
-        'NominalAnalysisNominalDepartment/{N}', and 'NominalAnalysisNominalAccountNumber/{N}' exist in MEO.
+        """Validate that all nominal analysis fields exist in MEO.
+
+        This includes 'NominalAnalysisNominalCostCentre/{N}', 'NominalAnalysisNominalDepartment/{N}',
+        and 'NominalAnalysisNominalAccountNumber/{N}'.
 
         Args:
             data (Iterable[dict]): The rows of data to validate.
 
         Returns:
             List[str]: A list of error messages, if any.
+
         """
         errors = []
 
diff --git a/sage_validation/file_validator/templates/upload.html b/sage_validation/file_validator/templates/upload.html
index 129121e..4a595da 100644
--- a/sage_validation/file_validator/templates/upload.html
+++ b/sage_validation/file_validator/templates/upload.html
@@ -26,12 +26,21 @@
         </div>
 
         <!-- Success Message -->
-        <div id="successSection" class="hidden mt-4 bg-green-100 border border-green-400 text-green-700 p-4 rounded-lg">
-            <strong class="font-bold">Success:</strong>
-            <span id="successMessage" class="block mt-2"></span>
+        <div id="successSection"
+             class="hidden mt-4 mb-8 bg-green-100 border border-green-400 text-green-700 p-4 rounded-lg text-center">
+            <strong class="font-bold text-lg">Success!</strong>
+            <p id="successMessage" class="block mt-2 text-md"></p>
+        </div>
+        <div id="downloadSection" class="hidden mt-16 flex justify-center">
+            <a id="downloadLink" href="#"
+               class="w-full max-w-xs text-center bg-blue-600 text-white py-4 px-12 rounded-lg shadow-lg hover:bg-blue-700
+          focus:outline-none focus:ring focus:ring-blue-400 transition text-lg font-semibold block">
+                📥 Download Updated CSV
+            </a>
         </div>
     </div>
 
+
     <script>
         const form = document.getElementById('uploadForm');
         const fileInput = document.getElementById('fileInput');
@@ -39,6 +48,8 @@
         const errorList = document.getElementById('errorList');
         const successSection = document.getElementById('successSection');
         const successMessage = document.getElementById('successMessage');
+        const downloadSection = document.getElementById('downloadSection');
+        const downloadLink = document.getElementById('downloadLink');
 
         form.addEventListener('submit', async function (e) {
             e.preventDefault();
@@ -48,6 +59,7 @@
             successMessage.innerHTML = '';
             errorSection.classList.add('hidden');
             successSection.classList.add('hidden');
+            downloadSection.classList.add('hidden');
 
             const formData = new FormData();
             formData.append('file', fileInput.files[0]);
@@ -66,18 +78,35 @@
                 if (response.ok && result.status === 'success') {
                     successMessage.innerText = result.message;
                     successSection.classList.remove('hidden');
+                    downloadLink.href = result.download_url;
+                    downloadSection.classList.remove('hidden');
                 } else if (response.status === 400 && result.status === 'error') {
-                    // Handle form errors from the backend
+                    errorList.innerHTML = '';
+
                     if (Array.isArray(result.errors)) {
                         result.errors.forEach(errorObj => {
-                            for (const [field, messages] of Object.entries(errorObj)) {
-                                messages.forEach(message => {
-                                    const li = document.createElement('li');
-                                    li.textContent = `${field}: ${message}`;
-                                    errorList.appendChild(li);
-                                });
+                            if (typeof errorObj === 'string') {
+                                const li = document.createElement('li');
+                                li.textContent = errorObj;
+                                errorList.appendChild(li);
+                            } else {
+                                for (const [field, messages] of Object.entries(errorObj)) {
+                                    messages.forEach(message => {
+                                        const li = document.createElement('li');
+                                        li.textContent = `${field}: ${message}`;
+                                        errorList.appendChild(li);
+                                    });
+                                }
                             }
                         });
+                    } else if (typeof result.errors === 'object') {
+                        for (const [field, messages] of Object.entries(result.errors)) {
+                            messages.forEach(message => {
+                                const li = document.createElement('li');
+                                li.textContent = `${field}: ${message}`;
+                                errorList.appendChild(li);
+                            });
+                        }
                     } else {
                         const li = document.createElement('li');
                         li.textContent = result.errors;
@@ -87,7 +116,6 @@
                     errorSection.classList.remove('hidden');
                 }
             } catch (error) {
-                // Handle unexpected errors
                 const li = document.createElement('li');
                 li.textContent = 'An unexpected error occurred. Please try again.';
                 errorList.appendChild(li);
diff --git a/sage_validation/file_validator/urls.py b/sage_validation/file_validator/urls.py
index 12e1520..a5c0d9f 100644
--- a/sage_validation/file_validator/urls.py
+++ b/sage_validation/file_validator/urls.py
@@ -2,8 +2,9 @@
 
 from django.urls import path
 
-from sage_validation.file_validator.views import CSVUploadView
+from sage_validation.file_validator.views import CSVExportView, CSVUploadView
 
 urlpatterns = [
     path("upload/", CSVUploadView.as_view(), name="upload-file"),
+    path("export/", CSVExportView.as_view(), name="export-file"),
 ]
diff --git a/sage_validation/file_validator/views.py b/sage_validation/file_validator/views.py
index 33ec72f..99efd17 100644
--- a/sage_validation/file_validator/views.py
+++ b/sage_validation/file_validator/views.py
@@ -1,11 +1,17 @@
 """Views for the file_validator app."""
+import csv
+import io
+from typing import Any
 
 from django.http import HttpRequest, HttpResponse, JsonResponse
 from django.shortcuts import render
 from django.urls import reverse_lazy
+from django.utils import timezone
+from django.views.generic.base import View
 from django.views.generic.edit import FormView
 
 from sage_validation.file_validator.forms import CSVUploadForm
+from sage_validation.file_validator.models import MeoCostCentres, XxData
 
 
 def index_view(request: HttpRequest) -> HttpResponse:
@@ -20,17 +26,92 @@ class CSVUploadView(FormView):
     form_class = CSVUploadForm
     success_url = reverse_lazy("upload-file")
 
-    def get_context_data(self, **kwargs: dict) -> dict:
+    def get_context_data(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
         """Render the form with no error message on GET request."""
         context = super().get_context_data(**kwargs)
         context["error"] = None
         context["message"] = None
         return context
 
-    def form_valid(self, form: CSVUploadForm) -> JsonResponse:  # noqa: ARG002
-        """Handle the CSV validation and passes appropriate success messages to the template."""
-        return JsonResponse({"status": "success", "message": "File is valid"})
+    def form_valid(self, form: CSVUploadForm) -> JsonResponse:
+        """Handle the CSV validation, store valid data, and prepare for export."""
+        csv_file = form.cleaned_data["file"]
+        csv_file.seek(0)
+        decoded_file = csv_file.read().decode("utf-8").strip()
+
+        if not decoded_file:
+            return JsonResponse({"status": "error", "message": "Uploaded file is empty."}, status=400)
+
+        reader = csv.DictReader(io.StringIO(decoded_file))
+        csv_data: list[dict[str, str]] = list(reader)
+
+        updated_data = self.update_fields(csv_data)
+
+        self.request.session["validated_csv"] = updated_data
+        self.request.session.modified = True
+
+        return JsonResponse({
+            "status": "success",
+            "message": "File successfully uploaded and processed.",
+            "download_url": reverse_lazy("export-file")
+        })
 
     def form_invalid(self, form: CSVUploadForm) -> JsonResponse:
-        """Handle the form when it is invalid (e.g., wrong file type or validation errors)."""
-        return JsonResponse({"status": "error", "errors": [form.errors]}, status=400)
+        """Handle the form when it is invalid."""
+        return JsonResponse({"status": "error", "errors": form.errors}, status=400)
+
+    @staticmethod
+    def update_fields(csv_data: list[dict[str, str]]) -> list[dict[str, str]]:
+        """Automatically update specific fields before export."""
+        current_date: str = timezone.now().strftime("%d/%m/%Y")
+
+        xx_data_map: dict[str, tuple] = {
+            obj.xx_value: (obj.project, obj.overhead) for obj in XxData.objects.using("meo").all()
+        }
+        cost_centre_map: dict[str, str] = {
+            obj.cc: obj.cc_type for obj in MeoCostCentres.objects.using("meo").all()
+        }
+
+        for row in csv_data:
+            row["TransactionDate"] = current_date
+
+            repeat = 1
+            while f"NominalAnalysisNominalCostCentre/{repeat}" in row:
+                cc = row.get(f"NominalAnalysisNominalCostCentre/{repeat}", "")
+                nominal_account_name = row.get(f"NominalAnalysisNominalAccountNumber/{repeat}", "")
+
+                if not cc or not nominal_account_name:
+                    repeat += 1
+                    continue
+
+                cc_type = cost_centre_map.get(cc, "")
+                xx_data = xx_data_map.get(nominal_account_name)
+
+                if xx_data:
+                    row[f"NominalAnalysisNominalAccountNumber/{repeat}"] = (
+                        xx_data[0] if cc_type == "Project" else xx_data[1]
+                    )
+
+                repeat += 1
+
+        return csv_data
+
+
+class CSVExportView(View):
+    """View for exporting the updated CSV file."""
+
+    def get(self, request: HttpRequest) -> HttpResponse:
+        """Generate a downloadable CSV file with updated values."""
+        csv_data: list[dict[str, str]] = request.session.get("validated_csv", [])
+
+        if not csv_data:
+            return HttpResponse("No data available for export.", status=400)
+
+        response = HttpResponse(content_type="text/csv")
+        response["Content-Disposition"] = 'attachment; filename="updated_file.csv"'
+
+        writer = csv.DictWriter(response, fieldnames=csv_data[0].keys())
+        writer.writeheader()
+        writer.writerows(csv_data)
+
+        return response
-- 
GitLab