Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Json import with existing UI #1779

Merged
merged 19 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/api/Controllers/ImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,29 @@ public ImportController(BdmsContext context, ILogger<ImportController> logger, L
/// Receives an uploaded JSON file to import one or several <see cref="Borehole"/>(s).
/// </summary>
/// <param name="workgroupId">The <see cref="Workgroup.Id"/> of the new <see cref="Borehole"/>(s).</param>
/// <param name="file">The <see cref="IFormFile"/> containing the borehole JSON records that were uploaded.</param>
/// <param name="boreholesFile">The <see cref="IFormFile"/> containing the borehole JSON records that were uploaded.</param>
/// <returns>The number of the newly created <see cref="Borehole"/>s.</returns>
[HttpPost("json")]
[Authorize(Policy = PolicyNames.Viewer)]
[RequestSizeLimit(int.MaxValue)]
[RequestFormLimits(MultipartBodyLengthLimit = MaxFileSize)]
public async Task<ActionResult<int>> UploadJsonFileAsync(int workgroupId, IFormFile file)
public async Task<ActionResult<int>> UploadJsonFileAsync(int workgroupId, IFormFile boreholesFile)
{
// Increase max allowed errors to be able to return more validation errors at once.
ModelState.MaxAllowedErrors = 1000;

logger.LogInformation("Import boreholes json to workgroup with id <{WorkgroupId}>", workgroupId);

if (file == null || file.Length == 0) return BadRequest("No file uploaded.");
if (boreholesFile == null || boreholesFile.Length == 0) return BadRequest("No file uploaded.");

if (!FileTypeChecker.IsJson(file)) return BadRequest("Invalid file type for borehole JSON.");
if (!FileTypeChecker.IsJson(boreholesFile)) return BadRequest("Invalid file type for borehole JSON.");

try
{
List<BoreholeImport>? boreholes;
try
{
using var stream = file.OpenReadStream();
using var stream = boreholesFile.OpenReadStream();
boreholes = await JsonSerializer.DeserializeAsync<List<BoreholeImport>>(stream, jsonImportOptions).ConfigureAwait(false);
}
catch (JsonException ex)
Expand Down
2 changes: 2 additions & 0 deletions src/api/Models/Observation.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;

namespace BDMS.Models;

Expand Down Expand Up @@ -107,6 +108,7 @@ public class Observation : IChangeTracking, IIdentifyable
/// <summary>
/// Gets or sets the <see cref="Observation"/>'s borehole.
/// </summary>
[JsonIgnore]
public Borehole? Borehole { get; set; }

/// <inheritdoc />
Expand Down
52 changes: 47 additions & 5 deletions src/client/docs/import.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Bohrdaten importieren

Mit der Import-Funktion können geologische Bohrdaten aus einer CSV-Datei importiert werden.
Mit der Import-Funktion können geologische Bohrdaten via CSV oder JSON Dateien importiert werden.

## Anleitung
## Anleitung CSV-Import

### Schritt 1: CSV-Datei vorbereiten

Expand Down Expand Up @@ -34,7 +34,7 @@ Die CSV-Datei muss den folgenden Anforderungen und dem Format entsprechen, damit
- Die Werte in den Spalten müssen den erwarteten Datentypen entsprechen (z.B. numerisch für Tiefe, Text für Namen, etc.).


## Bohrloch Datei Format
## Bohrloch Datei CSV Format

Die zu importierenden Daten müssen gemäss obigen Anforderungen im CSV-Format vorliegen. Die erste Zeile wird als Spaltentitel/Spaltenname interpretiert, die restlichen Zeilen als Daten.

Expand Down Expand Up @@ -84,7 +84,7 @@ Koordinaten können in LV95 oder LV03 importiert werden, das räumliche Bezugssy

## Validierung

### Fehlende Werte
### CSV-Import: Fehlende Werte

Für jeden bereitgestellten Header CSV-Datei muss für jede Zeile ein entsprechender Wert angegeben werden, oder leer gelassen werden.

Expand All @@ -94,6 +94,48 @@ Beim Importprozess der Bohrdaten wird eine Duplikatsvalidierung durchgeführt, u
Duplikate werden nur innerhalb einer Arbeitsgruppe erkannt. Die Duplikaterkennung erfolgt anhand der Koordinaten mit einer Toleranz von +/- 2 Metern und der Gesamttiefe des Bohrlochs.


## Generelles
## Anmerkungen

Es ist wichtig zu beachten, dass der Import beim ersten Fehler abgebrochen wird und keine teilweisen Importe stattfinden. Entweder werden alle Daten importiert, oder es findet kein Import statt. Der Import unterstützt keine Updates von bestehenden Daten.


## Anleitung JSON-Import

### Schritt 1: JSON-Datei vorbereiten

Zunächst sollte die JSON-Datei den Anforderungen und dem Format entsprechen, wie im Abschnitt [Format und Anforderungen an die JSON-Datei](#format-und-anforderungen-an-die-json-datei) beschrieben.

### Schritt 2: Navigieren zum Import-Bereich

1. In der Webapplikation anmelden.
2. Unten links auf die Schaltfläche _Importieren_ klicken.

### Schritt 3: Bohrloch JSON-Datei selektieren

1. Schaltfläche _Datei auswählen_ anklicken und die vorbereitete JSON-Datei auswählen.
2. Unter _Arbeitsgruppe_ die Arbeitsgruppe auswählen, in welche die Bohrdaten importiert werden sollen (neue Arbeitsgruppen können nur als "Admin-User" erstellt werden).

### Schritt 4: Dateien hochladen

1. Import-Prozess mit einem Klick auf _Importieren_ starten.
2. Warten, bis der Upload abgeschlossen ist und die Daten in der Anwendung verfügbar sind.

## Format und Anforderungen an die JSON-Datei

Die JSON-Datei muss den folgenden Anforderungen entsprechen, damit sie erfolgreich in die Webapplikation importiert werden kann:

- Die Datei muss im JSON-Format vorliegen.
- Die Datei muss im UTF-8-Format gespeichert sein.
- Die JSON-Datei muss ein Array von Objekten enthalten. Jedes Objekt entspricht einem Bohrloch. Auch ein einzelnes Bohrloch muss als Array von einem Objekt definiert werden.
- Die JSON-Datei eines Bohrlochexports kann als valide Vorlage für den Import betrachtet werden.

## Validierung

### Duplikate

Beim Importprozess der Bohrdaten wird eine Duplikatsvalidierung durchgeführt, um sicherzustellen, dass kein Bohrloch mehrmals in der Datei vorhanden ist oder bereits in der Datenbank existiert.
Duplikate werden nur innerhalb einer Arbeitsgruppe erkannt. Die Duplikaterkennung erfolgt anhand der Koordinaten mit einer Toleranz von +/- 2 Metern und der Gesamttiefe des Bohrlochs.

## Anmerkungen

Es ist wichtig zu beachten, dass der Import beim ersten Fehler abgebrochen wird und keine teilweisen Importe stattfinden. Entweder werden alle Daten importiert, oder es findet kein Import statt. Der Import unterstützt keine Updates von bestehenden Daten.
4 changes: 3 additions & 1 deletion src/client/public/locale/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
"drilling_mud_type": "Bohrspülung Typ",
"drilling_start_date": "Datum Bohrbeginn",
"dropZoneAttachmentsText": "Datei(en) mit Anhängen hier ablegen oder klicken, um sie hochzuladen",
"dropZoneBoreholesText": "CSV Datei mit Bohrungen hier ablegen oder klicken, um sie hochzuladen",
"dropZoneBoreholeCsvText": "Wählen Sie eine CSV Datei aus",
"dropZoneBoreholeJsonText": "Wählen Sie eine JSON Datei aus",
"dropZoneChooseBoreholeFilesFirst": "Wählen Sie zuerst eine CSV Datei mit Bohrungen aus",
"dropZoneDefaultErrorMsg": "Beim Auswählen ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"dropZoneFileToLarge": "Eine oder mehrere Dateien sind zu gross. Max. 200 MB.",
Expand Down Expand Up @@ -254,6 +255,7 @@
"identifier_value": "ID Code",
"import": "Importieren",
"importBoreholeAttachment": "Wählen Sie eine oder mehrere Datei(en) aus:",
"importBoreholes": "Bohrungen importieren",
"importDate": "Importdatum",
"importedBy": "Importiert von",
"importedData": "Importierte Daten",
Expand Down
4 changes: 3 additions & 1 deletion src/client/public/locale/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
"drilling_mud_type": "Drilling mud type",
"drilling_start_date": "Drilling date: start",
"dropZoneAttachmentsText": "Drag and drop file(s) with the appendixes here or click to upload",
"dropZoneBoreholesText": "Drag and drop a CSV file with the boreholes here or click to upload",
"dropZoneBoreholeCsvText": "Select a CSV file",
"dropZoneBoreholeJsonText": "Select a JSON file",
"dropZoneChooseBoreholeFilesFirst": "First, select a CSV file with boreholes",
"dropZoneDefaultErrorMsg": "An error occurred during the selection. Please try again.",
"dropZoneFileToLarge": "One ore more files are too large. Max. 200 MB.",
Expand Down Expand Up @@ -254,6 +255,7 @@
"identifier_value": "ID Code",
"import": "Import",
"importBoreholeAttachment": "Select one or multiple file(s) to upload:",
"importBoreholes": "Import boreholes",
"importDate": "Import date",
"importedBy": "Imported by",
"importedData": "Imported data",
Expand Down
4 changes: 3 additions & 1 deletion src/client/public/locale/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
"drilling_mud_type": "Type de boue de forage",
"drilling_start_date": "Date de début du forage",
"dropZoneAttachmentsText": "Déposez les fichiers avec les annexes ici ou cliquez pour les télécharger",
"dropZoneBoreholesText": "Déposez le fichier CSV avec les forages ici ou cliquez pour le télécharger",
"dropZoneBoreholeCsvText": "Sélectionnez un fichier CSV",
"dropZoneBoreholeJsonText": "Sélectionnez un fichier JSON",
"dropZoneChooseBoreholeFilesFirst": "D'abord, sélectionnez un fichier CSV avec les forages",
"dropZoneDefaultErrorMsg": "Une erreur s'est produite lors de la sélection. Veuillez réessayer.",
"dropZoneFileToLarge": "Un ou plusieurs fichiers sont trop volumineux. Max. 200 Mo.",
Expand Down Expand Up @@ -254,6 +255,7 @@
"identifier_value": "Code d'ID",
"import": "Importer",
"importBoreholeAttachment": "Sélectionnez un ou plusieurs fichiers à télécharger:",
"importBoreholes": "Importer des forages",
"importDate": "Date d'importation",
"importedBy": "Importé par",
"importedData": "Données importées",
Expand Down
4 changes: 3 additions & 1 deletion src/client/public/locale/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
"drilling_mud_type": "Tipo di fango di perforazione",
"drilling_start_date": "Data di inizio della perforazione",
"dropZoneAttachmentsText": "Trascina qui i file con i appendici o clicca per caricarli",
"dropZoneBoreholesText": "Trascina qui il file CSV con i perforazioni o clicca per caricarlo CSV",
"dropZoneBoreholeCsvText": "Seleziona un file CSV",
"dropZoneBoreholeJsonText": "Seleziona un file JSON",
"dropZoneChooseBoreholeFilesFirst": "Innanzitutto, seleziona un file CSV con i perforazioni",
"dropZoneDefaultErrorMsg": "Si è verificato un errore durante la selezione. Riprova.",
"dropZoneFileToLarge": "Uno o più file sono troppo grandi. Max. 200 MB.",
Expand Down Expand Up @@ -254,6 +255,7 @@
"identifier_value": "Codice di ID",
"import": "Importare",
"importBoreholeAttachment": "Seleziona uno o più file da scaricare:",
"importBoreholes": "Importare perforazioni",
"importDate": "Data di importazione",
"importedBy": "Importato da",
"importedData": "Dati importati",
Expand Down
8 changes: 6 additions & 2 deletions src/client/src/api/borehole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,22 @@ export const getBoreholeById = async (id: number) => await fetchApiV2(`borehole/

export const exportJsonBoreholes = async (ids: number[] | GridRowSelectionModel) => {
const idsQuery = ids.map(id => `ids=${id}`).join("&");
return await fetchApiV2(`borehole/json?${idsQuery}`, "GET");
return await fetchApiV2(`export/json?${idsQuery}`, "GET");
};

export const updateBorehole = async (borehole: BoreholeV2) => {
return await fetchApiV2("borehole", "PUT", borehole);
};

/* eslint-disable @typescript-eslint/no-explicit-any */
export const importBoreholes = async (workgroupId: string, combinedFormData: any) => {
export const importBoreholesCsv = async (workgroupId: string, combinedFormData: any) => {
return await upload(`import?workgroupId=${workgroupId}`, "POST", combinedFormData);
};

export const importBoreholesJson = async (workgroupId: string, combinedFormData: any) => {
return await upload(`import/json?workgroupId=${workgroupId}`, "POST", combinedFormData);
};

export const copyBorehole = async (boreholeId: GridRowSelectionModel, workgroupId: string | null) => {
return await fetchApiV2(`borehole/copy?id=${boreholeId}&workgroupId=${workgroupId}`, "POST");
};
Expand Down
6 changes: 3 additions & 3 deletions src/client/src/components/export/exportDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from "@mui/material";
import { GridRowSelectionModel } from "@mui/x-data-grid";
import { exportCSVBorehole, getAllBoreholes } from "../../api/borehole.ts";
import { exportCSVBorehole, exportJsonBoreholes } from "../../api/borehole.ts";
import { downloadData } from "../../utils.ts";
import { CancelButton, ExportButton } from "../buttons/buttons.tsx";

Expand All @@ -15,8 +15,8 @@ export const ExportDialog = ({ isExporting, setIsExporting, selectionModel, file
const { t } = useTranslation();

const exportJson = async () => {
const paginatedResponse = await getAllBoreholes(selectionModel, 1, selectionModel.length);
const jsonString = JSON.stringify(paginatedResponse.boreholes, null, 2);
const exportJsonResponse = await exportJsonBoreholes(selectionModel);
const jsonString = JSON.stringify(exportJsonResponse);
downloadData(jsonString, `${fileName}.json`, "application/json");
setIsExporting(false);
};
Expand Down
47 changes: 37 additions & 10 deletions src/client/src/pages/detail/attachments/fileDropzone.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { theme } from "../../../AppTheme.ts";
* @param {string} props.defaultText - The default text to display in the dropzone.
* @param {number} props.maxFilesToSelectAtOnce - The maximum number of files that can be selected at once.
* @param {number} props.maxFilesToUpload - The maximum number of files that can be uploaded.
* @param {boolean} props.restrictAcceptedFileTypeToCsv - Whether to restrict accepted file types to CSV.
* @param {Array<string>} props.acceptedFileTypes - The list of accepted file types.
* @param {boolean} props.isDisabled - Whether the dropzone is disabled.
* @param {string} props.dataCy - The data-cy attribute for testing.
* @param {Function} props.setFileType - A react SetStateAction to set the file type.
* @returns {JSX.Element} The rendered FileDropzone component.
*/
export const FileDropzone = props => {
Expand All @@ -24,21 +25,20 @@ export const FileDropzone = props => {
defaultText,
maxFilesToSelectAtOnce,
maxFilesToUpload,
restrictAcceptedFileTypeToCsv,
acceptedFileTypes,
isDisabled,
dataCy,
setFileType,
} = props;
const { t } = useTranslation();
const [files, setFiles] = useState([]);
const [dropZoneText, setDropZoneText] = useState(null);
const [dropZoneText, setDropZoneText] = useState(t(defaultText));
const [dropZoneTextColor, setDropZoneTextColor] = useState(null);
const defaultDropzoneTextColor = isDisabled ? "#9f9f9f" : "#2185d0";
const initialDropzoneText = isDisabled ? t("dropZoneChooseBoreholeFilesFirst") : t(defaultText);

useEffect(() => {
setDropZoneText(initialDropzoneText);
setDropZoneTextColor(defaultDropzoneTextColor);
}, [defaultDropzoneTextColor, initialDropzoneText]);
}, [defaultDropzoneTextColor]);

// Set the color of the dropzone text to red and display an error message
const showErrorMsg = useCallback(
Expand Down Expand Up @@ -88,17 +88,38 @@ export const FileDropzone = props => {
setDropZoneTextColor(defaultDropzoneTextColor);
setDropZoneText(t(defaultText));
setFiles(prevFiles => [...prevFiles, ...acceptedFiles]);

if (setFileType) {
if (acceptedFileTypes.includes("text/csv")) {
setFileType("csv");
}
if (acceptedFileTypes.includes("application/json")) {
setFileType("json");
}
}
}
},
[defaultDropzoneTextColor, defaultText, files.length, maxFilesToUpload, showErrorMsg, t],
[
defaultDropzoneTextColor,
defaultText,
files.length,
maxFilesToUpload,
showErrorMsg,
t,
acceptedFileTypes,
setFileType,
],
);

// Is called when a accepted file is removed.
// Is called when an accepted file is removed.
const removeFile = fileToRemove => {
setFiles(prevFiles => prevFiles.filter(file => file !== fileToRemove));
if (setFileType) {
setFileType("");
}
};

// IS called when the selected/dropped files are rejected
// Is called when the selected/dropped files are rejected
const onDropRejected = useCallback(
fileRejections => {
const errorCode = fileRejections[0].errors[0].code;
Expand All @@ -113,7 +134,13 @@ export const FileDropzone = props => {
onDropAccepted,
maxFiles: maxFilesToSelectAtOnce || Infinity,
maxSize: 209715200,
accept: restrictAcceptedFileTypeToCsv ? { "text/csv": [".csv"] } : "*",
accept:
acceptedFileTypes.length > 0
? acceptedFileTypes.reduce((acc, type) => {
acc[type] = [];
return acc;
}, {})
: "*",
noClick: isDisabled,
noKeyboard: isDisabled,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const GeometryImport = ({ boreholeId }) => {
<FileDropzone
onHandleFileChange={field.onChange}
defaultText={"dropZoneGeometryText"}
restrictAcceptedFileTypeToCsv={true}
acceptedFileTypes={["text/csv"]}
maxFilesToSelectAtOnce={1}
maxFilesToUpload={1}
isDisabled={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ export interface NewBoreholeProps extends WorkgroupSelectProps {
toggleDrawer: (open: boolean) => void;
}

export interface ImportContentProps {
setSelectedFile: React.Dispatch<React.SetStateAction<Blob[] | null>>;
}

export interface ImportModalProps extends ImportContentProps {
export interface ImportModalProps {
modal: boolean;
creating: boolean;
selectedFile: Blob[] | null;
Expand Down
Loading
Loading