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
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 = []
......
......@@ -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);
......
......@@ -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"),
]
"""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
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