Skip to content
Snippets Groups Projects
Commit 1263e0a0 authored by Neda Moeini's avatar Neda Moeini
Browse files

Add export file functionality

parent 1f9bc769
No related branches found
No related tags found
1 merge request!1Add export file functionality
...@@ -7,8 +7,13 @@ from typing import ClassVar ...@@ -7,8 +7,13 @@ from typing import ClassVar
from django import forms from django import forms
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from sage_validation.file_validator.models import MeoCostCentres, MeoValidSageAccounts, XxData, MeoValidSuppliers, \ from sage_validation.file_validator.models import (
MeoNominal MeoCostCentres,
MeoNominal,
MeoValidSageAccounts,
MeoValidSuppliers,
XxData,
)
class CSVUploadForm(forms.Form): class CSVUploadForm(forms.Form):
...@@ -176,14 +181,17 @@ 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]: def _validate_nc_cc_dep_combination_against_meo_sage_account(self, data: Iterable[dict]) -> list[str]:
"""Validate that all occurrences of 'NominalAnalysisNominalCostCentre/{N}', """Validate that all nominal analysis fields exist in MEO.
'NominalAnalysisNominalDepartment/{N}', and 'NominalAnalysisNominalAccountNumber/{N}' exist in MEO.
This includes 'NominalAnalysisNominalCostCentre/{N}', 'NominalAnalysisNominalDepartment/{N}',
and 'NominalAnalysisNominalAccountNumber/{N}'.
Args: Args:
data (Iterable[dict]): The rows of data to validate. data (Iterable[dict]): The rows of data to validate.
Returns: Returns:
List[str]: A list of error messages, if any. List[str]: A list of error messages, if any.
""" """
errors = [] errors = []
......
...@@ -26,12 +26,21 @@ ...@@ -26,12 +26,21 @@
</div> </div>
<!-- Success Message --> <!-- Success Message -->
<div id="successSection" class="hidden mt-4 bg-green-100 border border-green-400 text-green-700 p-4 rounded-lg"> <div id="successSection"
<strong class="font-bold">Success:</strong> class="hidden mt-4 mb-8 bg-green-100 border border-green-400 text-green-700 p-4 rounded-lg text-center">
<span id="successMessage" class="block mt-2"></span> <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>
</div> </div>
<script> <script>
const form = document.getElementById('uploadForm'); const form = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
...@@ -39,6 +48,8 @@ ...@@ -39,6 +48,8 @@
const errorList = document.getElementById('errorList'); const errorList = document.getElementById('errorList');
const successSection = document.getElementById('successSection'); const successSection = document.getElementById('successSection');
const successMessage = document.getElementById('successMessage'); const successMessage = document.getElementById('successMessage');
const downloadSection = document.getElementById('downloadSection');
const downloadLink = document.getElementById('downloadLink');
form.addEventListener('submit', async function (e) { form.addEventListener('submit', async function (e) {
e.preventDefault(); e.preventDefault();
...@@ -48,6 +59,7 @@ ...@@ -48,6 +59,7 @@
successMessage.innerHTML = ''; successMessage.innerHTML = '';
errorSection.classList.add('hidden'); errorSection.classList.add('hidden');
successSection.classList.add('hidden'); successSection.classList.add('hidden');
downloadSection.classList.add('hidden');
const formData = new FormData(); const formData = new FormData();
formData.append('file', fileInput.files[0]); formData.append('file', fileInput.files[0]);
...@@ -66,18 +78,35 @@ ...@@ -66,18 +78,35 @@
if (response.ok && result.status === 'success') { if (response.ok && result.status === 'success') {
successMessage.innerText = result.message; successMessage.innerText = result.message;
successSection.classList.remove('hidden'); successSection.classList.remove('hidden');
downloadLink.href = result.download_url;
downloadSection.classList.remove('hidden');
} else if (response.status === 400 && result.status === 'error') { } else if (response.status === 400 && result.status === 'error') {
// Handle form errors from the backend errorList.innerHTML = '';
if (Array.isArray(result.errors)) { if (Array.isArray(result.errors)) {
result.errors.forEach(errorObj => { result.errors.forEach(errorObj => {
for (const [field, messages] of Object.entries(errorObj)) { if (typeof errorObj === 'string') {
messages.forEach(message => { const li = document.createElement('li');
const li = document.createElement('li'); li.textContent = errorObj;
li.textContent = `${field}: ${message}`; errorList.appendChild(li);
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 { } else {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = result.errors; li.textContent = result.errors;
...@@ -87,7 +116,6 @@ ...@@ -87,7 +116,6 @@
errorSection.classList.remove('hidden'); errorSection.classList.remove('hidden');
} }
} catch (error) { } catch (error) {
// Handle unexpected errors
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = 'An unexpected error occurred. Please try again.'; li.textContent = 'An unexpected error occurred. Please try again.';
errorList.appendChild(li); errorList.appendChild(li);
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
from django.urls import path from django.urls import path
from sage_validation.file_validator.views import CSVUploadView from sage_validation.file_validator.views import CSVExportView, CSVUploadView
urlpatterns = [ urlpatterns = [
path("upload/", CSVUploadView.as_view(), name="upload-file"), path("upload/", CSVUploadView.as_view(), name="upload-file"),
path("export/", CSVExportView.as_view(), name="export-file"),
] ]
"""Views for the file_validator app.""" """Views for the file_validator app."""
import csv
import io
from typing import Any
from django.http import HttpRequest, HttpResponse, JsonResponse from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse_lazy 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 django.views.generic.edit import FormView
from sage_validation.file_validator.forms import CSVUploadForm from sage_validation.file_validator.forms import CSVUploadForm
from sage_validation.file_validator.models import MeoCostCentres, XxData
def index_view(request: HttpRequest) -> HttpResponse: def index_view(request: HttpRequest) -> HttpResponse:
...@@ -20,17 +26,92 @@ class CSVUploadView(FormView): ...@@ -20,17 +26,92 @@ class CSVUploadView(FormView):
form_class = CSVUploadForm form_class = CSVUploadForm
success_url = reverse_lazy("upload-file") 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.""" """Render the form with no error message on GET request."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["error"] = None context["error"] = None
context["message"] = None context["message"] = None
return context return context
def form_valid(self, form: CSVUploadForm) -> JsonResponse: # noqa: ARG002 def form_valid(self, form: CSVUploadForm) -> JsonResponse:
"""Handle the CSV validation and passes appropriate success messages to the template.""" """Handle the CSV validation, store valid data, and prepare for export."""
return JsonResponse({"status": "success", "message": "File is valid"}) 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: def form_invalid(self, form: CSVUploadForm) -> JsonResponse:
"""Handle the form when it is invalid (e.g., wrong file type or validation errors).""" """Handle the form when it is invalid."""
return JsonResponse({"status": "error", "errors": [form.errors]}, status=400) 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment