Skip to content
Snippets Groups Projects
Commit 46ccbd2d authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 0.1.

parents feb2634c d0313ced
No related branches found
No related tags found
No related merge requests found
Showing
with 427 additions and 0 deletions
venv
.idea
*.pyc
sage_validation/staticfiles
sage_validation/frontend/node_modules
.env
\ No newline at end of file
# Changelog
## [0.1] - 2025-01-08
- initial release
MIT License
Copyright (c) 2023-2024 GÉANT Software Development Team
\ No newline at end of file
recursive-include sage_validation/file_validator/templates *
recursive-include sage_validation/file_validator/static *
recursive-include sage_validation/templates *
recursive-include sage_validation/static *
\ No newline at end of file
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../sage-validation'))
# -- Project information -----------------------------------------------------
project = 'Sage Validation'
copyright = '2024, GÉANT'
author = 'GÉANT'
# The full version, including alpha/beta/rc tags
release = '0.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx_autodoc_typehints',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
\ No newline at end of file
.. Sage Validation documentation master file, created by
sphinx-quickstart on Mon Oct 7 09:58:36 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Sage Validation's documentation!
===========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sage_validation.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
[tool.mypy]
mypy_path = "sage_validation"
exclude = [
"venv",
"docs",
"manage.py",
]
ignore_missing_imports = true # Ignore imports that are not typed
disallow_untyped_calls = true # Disallow calls to untyped functions
disallow_untyped_defs = true # Disallow untyped function definitions
[tool.ruff]
extend-exclude = [
"*/migrations",
"sage_validation/wsgi.py",
"sage_validation/asgi.py",
"sage_validation/settings.py",
"manage.py",
"docs",
"theme",
]
select = [
"A", "ARG", "B", "BLE", "C", "COM", "C4", "C90", "D", "DTZ",
"E", "EM", "ERA", "F", "FA", "FBT", "FLY", "FURB", "G", "I",
"ICN", "INP", "ISC", "LOG", "N", "PERF", "PGH", "PIE", "PL",
"PT", "PTH", "PYI", "Q", "RET", "R", "RET", "RSE", "RUF",
"S", "SIM", "SLF", "T", "T20", "TID", "TRY", "UP", "W", "YTT"
]
ignore = [
"COM812", "D203", "D213", "ISC001", "N805", "PLR0913", "PLR0904", "PLW1514", "D104"
]
target-version = "py312"
line-length = 120
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
"""ASGI config for sage_validation project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sage_validation.settings")
application = get_asgi_application()
"""Admin configuration for the file_validator app."""
"""App configuration for file_validator app."""
from django.apps import AppConfig
class FileValidatorConfig(AppConfig):
"""App configuration for file_validator."""
default_auto_field = "django.db.models.BigAutoField"
name = "sage_validation.file_validator"
"""Forms for the file_validator app."""
import csv
from collections.abc import Sequence
from typing import ClassVar
from django import forms
class CSVUploadForm(forms.Form):
"""Form for uploading CSV files only."""
file = forms.FileField(label="Select a CSV file")
required_columns: ClassVar[list] = [
"AccountNumber", "CBAccountNumber", "DaysDiscountValid", "DiscountValue",
"DiscountPercentage", "DueDate", "GoodsValueInAccountCurrency",
"PurControlValueInBaseCurrency", "DocumentToBaseCurrencyRate",
"DocumentToAccountCurrencyRate", "PostedDate", "QueryCode",
"TransactionReference", "SecondReference", "Source",
"SYSTraderTranType", "TransactionDate", "UniqueReferenceNumber",
"UserNumber", "TaxValue", "SYSTraderGenerationReasonType",
"GoodsValueInBaseCurrency"
]
repeating_columns: ClassVar[dict] = {
"NominalAnalysis": [
"NominalAnalysisTransactionValue", "NominalAnalysisNominalAccountNumber",
"NominalAnalysisNominalCostCentre", "NominalAnalysisNominalDepartment",
"NominalAnalysisNominalAnalysisNarrative", "NominalAnalysisTransactionAnalysisCode"
],
"TaxAnalysis": [
"TaxAnalysisTaxRate", "TaxAnalysisGoodsValueBeforeDiscount",
"TaxAnalysisDiscountValue", "TaxAnalysisDiscountPercentage",
"TaxAnalysisTaxOnGoodsValue"
]
}
def clean_file(self) -> str:
"""Validate the uploaded file format and contents."""
file = self.cleaned_data["file"]
if not file.name.endswith(".csv"):
err_msg = "File must be in CSV format"
raise forms.ValidationError(err_msg)
try:
csv_file = file.read().decode("utf-8").splitlines()
reader = csv.DictReader(csv_file)
fieldnames = reader.fieldnames if reader.fieldnames is not None else []
missing_columns = [col for col in self.required_columns if col not in fieldnames]
for section_name, column_list in self.repeating_columns.items():
max_repeat = self.get_max_repeat(fieldnames, section_name)
if max_repeat == 0:
missing_columns.extend([f"{base_col}/1" for base_col in column_list])
else:
for repeat in range(1, max_repeat + 1):
missing_columns.extend(
[f"{base_col}/{repeat}" for base_col in column_list if
f"{base_col}/{repeat}" not in fieldnames]
)
if missing_columns:
err_msg = f"Missing required columns: {', '.join(missing_columns)}"
raise forms.ValidationError(err_msg)
except (UnicodeDecodeError, csv.Error) as e:
err_msg = f"File could not be processed: {e!s}"
raise forms.ValidationError(err_msg) from e
return file
@staticmethod
def get_max_repeat(fieldnames: Sequence[str], section_prefix: str) -> int:
"""Identify the maximum number of repeats for a section."""
max_repeat = 0
for field in fieldnames:
if field.startswith(section_prefix):
try:
repeat_number = int(field.split("/")[-1])
max_repeat = max(max_repeat, repeat_number)
except ValueError:
continue
return max_repeat
"""Models for the file_validator app."""
{% extends 'base.html' %}
{% block title %}File Upload{% endblock %}
{% block content %}
<div class="bg-white p-10 rounded-lg shadow-lg w-11/12 md:w-1/2 lg:w-1/3 mx-auto">
<h2 class="text-2xl font-bold mb-6 text-gray-800 text-center">Upload CSV File</h2>
<form id="uploadForm" enctype="multipart/form-data" class="space-y-6">
<div>
<label for="fileInput" class="block text-sm font-medium text-gray-700 mb-2">Choose CSV File</label>
<input type="file" name="file" id="fileInput" accept=".csv"
class="w-full border border-gray-300 p-3 rounded-lg focus:outline-none focus:ring focus:ring-blue-400">
</div>
<button type="submit"
class="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring focus:ring-blue-400">
Upload
</button>
</form>
<!-- Error Display -->
<div id="errorSection" class="hidden mt-4 bg-red-100 border border-red-400 text-red-700 p-4 rounded-lg">
<strong class="font-bold">Error:</strong>
<ul id="errorList" class="mt-2 list-disc list-inside"></ul>
</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>
</div>
<script>
const form = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput');
const errorSection = document.getElementById('errorSection');
const errorList = document.getElementById('errorList');
const successSection = document.getElementById('successSection');
const successMessage = document.getElementById('successMessage');
form.addEventListener('submit', async function (e) {
e.preventDefault();
// Clear previous messages
errorList.innerHTML = '';
successMessage.innerHTML = '';
errorSection.classList.add('hidden');
successSection.classList.add('hidden');
const formData = new FormData();
formData.append('file', fileInput.files[0]);
try {
const response = await fetch('', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}',
}
});
const result = await response.json();
if (response.ok && result.status === 'success') {
successMessage.innerText = result.message;
successSection.classList.remove('hidden');
} else if (response.status === 400 && result.status === 'error') {
// Handle form errors from the backend
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);
});
}
});
} else {
const li = document.createElement('li');
li.textContent = result.errors;
errorList.appendChild(li);
}
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);
errorSection.classList.remove('hidden');
}
});
</script>
{% endblock %}
\ No newline at end of file
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