Skip to content

Commit

Permalink
Add alert for pdf download (#1825)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiraGeowerkstatt authored Jan 28, 2025
2 parents 6acd8ce + 737d7c6 commit dcbf93e
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Changed

- Exporting boreholes as CSV and JSON (without attachments) is now also available in anonymous mode.
- Display an error message to the user when attachment files could not be fetched from cloud storage during export.
- Removed all asterisks from required form fields.
- Updated the UI design of the tabs component.
- Reincluded download link for codelist references in the import panel.
Expand Down
8 changes: 7 additions & 1 deletion src/api/Controllers/ExportController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using BDMS.Authentication;
using Amazon.S3;
using BDMS.Authentication;
using BDMS.Models;
using CsvHelper;
using MaxRev.Gdal.Core;
Expand Down Expand Up @@ -337,6 +338,11 @@ public async Task<ActionResult> ExportJsonWithAttachmentsAsync([FromQuery][MaxLe

return File(memoryStream.ToArray(), "application/zip", $"{fileName}");
}
catch (AmazonS3Exception ex)
{
logger.LogError(ex, "Amazon S3 Store threw an exception.");
return Problem("An error occurred while fetching a file from the cloud storage.");
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to prepare ZIP file.");
Expand Down
5 changes: 5 additions & 0 deletions src/client/cypress/e2e/helpers/dataGridHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const unCheckRowWithText = text => {
cy.contains(".MuiDataGrid-row", text).find('.MuiCheckbox-root input[type="checkbox"]').uncheck({ force: true });
};

export const checkTwoFirstRows = () => {
cy.get(".MuiDataGrid-row").eq(0).find('.MuiCheckbox-root input[type="checkbox"]').check({ force: true });
cy.get(".MuiDataGrid-row").eq(1).find('.MuiCheckbox-root input[type="checkbox"]').check({ force: true });
};

export const clickOnRowWithText = text => {
cy.contains(".MuiDataGrid-row", text).click();
};
20 changes: 20 additions & 0 deletions src/client/cypress/e2e/mainPage/export.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {
checkAllVisibleRows,
checkRowWithText,
checkTwoFirstRows,
clickOnRowWithText,
showTableAndWaitForData,
} from "../helpers/dataGridHelpers.js";
Expand Down Expand Up @@ -283,6 +284,25 @@ describe("Test for exporting boreholes.", () => {
readDownloadedFile(`${boreholeName}.zip`);
});

it("displays an error message when file was not found on S3 store", () => {
showTableAndWaitForData();
checkTwoFirstRows();
exportItem();

// Fake Api error as returned from API
cy.intercept("GET", "/api/v2/export/zip?**", {
statusCode: 500,
body: {
title: "NoSuchKey",
status: 500,
detail: "An error occurred while fetching a file from the cloud storage.",
},
}).as("exportZipError");

exportZipItem();
cy.get(".MuiAlert-message").contains("An error occurred while fetching a file from the cloud storage.");
});

it("exports and reimports a borehole using csv", () => {
const boreholeName = "AAA_WALRUS";
createBorehole({
Expand Down
2 changes: 2 additions & 0 deletions src/client/public/locale/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"errorDataExtractionFileLoading": "Beim Laden der Datenextraktionsdatei ist ein Fehler aufgetreten.",
"errorDuplicatedUploadPerBorehole": "Diese Datei wurde bereits für diese Bohrung hochgeladen und kann nicht ein zweites Mal hochgeladen werden.",
"errorDuringBoreholeFileUpload": "Beim Hochladen ist ein Fehler aufgetreten.",
"errorDuringExport": "Beim Exportieren ist ein Fehler aufgetreten.",
"errorEndPoint": "Sie sollten den Endpunkt hinzufügen",
"errorFromDepthGreaterThanToDepth": "von Tiefe ist grösser als bis Tiefe",
"errorFromDepthTooLow": "von Tiefe ist zu klein",
Expand All @@ -184,6 +185,7 @@
"errorLocked": "Bohrung durch den {{user}} gesperrt",
"errorMissingBedrock": "Tiefe Top Fels fehlt",
"errorMissingBedrockSolution": "Tiefe Top Fels automatisch aus der Bohrdaten übernehmen",
"errorOccurredWhileFetchingFileFromCloudStorage": "Beim Laden einer Datei aus dem Cloud Speicher ist ein Fehler aufgetreten.",
"errorOverlap": "Überlappende Schichten",
"errorStartEditing": "Sie sollten die Schaltfläche 'Bearbeitung beginnen' drücken",
"errorStartWrong": "Erste Schicht beginnt nicht auf dem Referenzniveau",
Expand Down
2 changes: 2 additions & 0 deletions src/client/public/locale/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"errorDataExtractionFileLoading": "An error occurred while loading the data extraction file.",
"errorDuplicatedUploadPerBorehole": "This file has already been uploaded for this borehole and cannot be uploaded a second time.",
"errorDuringBoreholeFileUpload": "An error occurred during the upload.",
"errorDuringExport": "An error occurred during the export.",
"errorEndPoint": "You should add an end point",
"errorFromDepthGreaterThanToDepth": "from depth is greater than to depth",
"errorFromDepthTooLow": "from depth is too low",
Expand All @@ -184,6 +185,7 @@
"errorLocked": "Borehole locked by {{user}}",
"errorMissingBedrock": "Missing top bedrock depth",
"errorMissingBedrockSolution": "Take the top bedrock depth automatically from the borehole data",
"errorOccurredWhileFetchingFileFromCloudStorage": "An error occurred while fetching a file from the cloud storage.",
"errorOverlap": "Overlapping layers",
"errorStartEditing": "You should press the 'start editing' button",
"errorStartWrong": "First layer does not start at the reference level",
Expand Down
2 changes: 2 additions & 0 deletions src/client/public/locale/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"errorDataExtractionFileLoading": "Une erreur s'est produite lors du chargement du fichier d'extraction des données.",
"errorDuplicatedUploadPerBorehole": "Ce fichier a déjà été téléchargé pour ce forage et ne peut pas être téléchargé une deuxième fois.",
"errorDuringBoreholeFileUpload": "Une erreur s'est produite lors du téléchargement.",
"errorDuringExport": "Une erreur s'est produite lors de l'exportation.",
"errorEndPoint": "Veuillez ajouter la terminaison du forage",
"errorFromDepthGreaterThanToDepth": "de la profondeur est supérieure à la profondeur",
"errorFromDepthTooLow": "de la profondeur est trop faible",
Expand All @@ -184,6 +185,7 @@
"errorLocked": "Modification en cours par l'utilisateur {{user}}",
"errorMissingBedrock": "La profondeur du toit du rocher manque",
"errorMissingBedrockSolution": "Reprendre automatiquement la profondeur du toit du rocher à partir de la page des données de forage",
"errorOccurredWhileFetchingFileFromCloudStorage": "Une erreur s'est produite lors du chargement d'un fichier à partir du stockage en nuage.",
"errorOverlap": "Des couches se recouvrent",
"errorStartEditing": "Veuillez appuyer sur le bouton 'Commencer l'édition'",
"errorStartWrong": "La première couche ne débute pas au niveau de la référence altitudinale",
Expand Down
2 changes: 2 additions & 0 deletions src/client/public/locale/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"errorDataExtractionFileLoading": "Si è verificato un errore durante il caricamento del file di estrazione dei dati.",
"errorDuplicatedUploadPerBorehole": "Questo file è già stato caricato, per questa perforazione, e quindi non può essere caricato una seconda volta.",
"errorDuringBoreholeFileUpload": "Si è verificato un errore durante il caricamento.",
"errorDuringExport": "Si è verificato un errore durante l'esportazione.",
"errorEndPoint": "Per favore aggiungere il punto finale",
"errorFromDepthGreaterThanToDepth": "dalla profondità è maggiore della profondità",
"errorFromDepthTooLow": "dalla profondità è troppo bassa",
Expand All @@ -184,6 +185,7 @@
"errorLocked": "Modifica della perforazione in corso da parte di {{user}}",
"errorMissingBedrock": "Manca la profondità del substrato roccioso",
"errorMissingBedrockSolution": "Crea automaticamente la profondità del substrato roccioso attingendo dai dati inseriti della perforazione",
"errorOccurredWhileFetchingFileFromCloudStorage": "Si è verificato un errore durante il caricamento di un file dall'archiviazione cloud.",
"errorOverlap": "Sovrapposizione di strati",
"errorStartEditing": "Per favore premere il pulsante 'iniziare la modifica'",
"errorStartWrong": "Il primo strato non inizia dalla riferimento della quota",
Expand Down
2 changes: 1 addition & 1 deletion src/client/src/api/fetchApiV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function upload(url, method, payload) {
export async function download(url) {
const response = await fetchApiV2Base(url, "GET", null);
if (!response.ok) {
throw new ApiError(response.statusText, response.status);
throw new ApiError("errorOccurredWhileFetchingFileFromCloudStorage", response.status);
}

const fileName =
Expand Down
43 changes: 33 additions & 10 deletions src/client/src/components/export/exportDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from "@mui/material";
import { GridRowSelectionModel } from "@mui/x-data-grid";
import { ReduxRootState, User } from "../../api-lib/ReduxStateInterfaces.ts";
import { ApiError } from "../../api/apiInterfaces.ts";
import { exportCSVBorehole, exportJsonBoreholes, exportJsonWithAttachmentsBorehole } from "../../api/borehole.ts";
import { useAuth } from "../../auth/useBdmsAuth.tsx";
import { downloadData } from "../../utils.ts";
import { AlertContext } from "../alert/alertContext.tsx";
import { CancelButton, ExportButton } from "../buttons/buttons.tsx";

interface ExportDialogProps {
Expand All @@ -19,23 +22,43 @@ export const ExportDialog = ({ isExporting, setIsExporting, selectionModel, file
const auth = useAuth();
const user: User = useSelector((state: ReduxRootState) => state.core_user);
const canExportAttachments = !auth.anonymousModeEnabled && user.data.roles.includes("EDIT");
const { showAlert } = useContext(AlertContext);

const handleExport = async (exportFunction: (ids: number[] | GridRowSelectionModel) => Promise<Response | void>) => {
try {
await exportFunction(selectionModel.slice(0, 100));
} catch (error) {
if (error instanceof ApiError) {
showAlert(t(error.message), "error");
} else {
showAlert(t("errorDuringExport"), "error");
}
} finally {
setIsExporting(false);
}
};

const onExportJson = async () => {
await handleExport(exportJson);
};

const onExportCsv = async () => {
await handleExport(exportCsv);
};

const onExportJsonWithAttachments = async () => {
await handleExport(exportJsonWithAttachmentsBorehole);
};

const exportJson = async () => {
const exportJsonResponse = await exportJsonBoreholes(selectionModel);
const jsonString = JSON.stringify(exportJsonResponse);
downloadData(jsonString, `${fileName}.json`, "application/json");
setIsExporting(false);
};

const exportCsv = async () => {
const csvData = await exportCSVBorehole(selectionModel.slice(0, 100));
downloadData(csvData, `${fileName}.csv`, "text/csv");
setIsExporting(false);
};

const exportJsonWithAttachments = async () => {
await exportJsonWithAttachmentsBorehole(selectionModel.slice(0, 100));
setIsExporting(false);
};

return (
Expand All @@ -46,9 +69,9 @@ export const ExportDialog = ({ isExporting, setIsExporting, selectionModel, file
</DialogTitle>
<DialogContent>
<Stack gap={1} sx={{ mt: 3 }}>
<ExportButton label={"CSV"} onClick={exportCsv} />
<ExportButton label={"JSON"} onClick={exportJson} />
{canExportAttachments && <ExportButton label={"JSON + PDF"} onClick={exportJsonWithAttachments} />}
<ExportButton label={"CSV"} onClick={onExportCsv} />
<ExportButton label={"JSON"} onClick={onExportJson} />
{canExportAttachments && <ExportButton label={"JSON + PDF"} onClick={onExportJsonWithAttachments} />}
</Stack>
</DialogContent>
<DialogActions>
Expand Down
29 changes: 28 additions & 1 deletion tests/api/Controllers/ExportControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ public void TestInitialize()
boreholeFileControllerLoggerMock.Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

var loggerMock = new Mock<ILogger<ExportController>>();

controller = new ExportController(context, boreholeFileCloudService, loggerMock.Object) { ControllerContext = GetControllerContextAdmin() };
}

Expand Down Expand Up @@ -220,6 +219,34 @@ public async Task ExportJsonWithAttachments()
Assert.AreEqual("Project Alpha", borehole.ProjectName);
}

[TestMethod]
public async Task ExportJsonWithAttachmementsButFileDoesNotExist()
{
var newBorehole = GetBoreholeToAdd();

context.Add(newBorehole);

// Save first to get boreholeId
await context.SaveChangesAsync().ConfigureAwait(false);

var fileWithoutAttachments = new BoreholeFile
{
BoreholeId = newBorehole.Id,
File = new Models.File() { Name = "file.pdf", NameUuid = $"{Guid.NewGuid}.pdf", Type = "pdf" },
};

// Add file to context but not to S3 store
context.Add(fileWithoutAttachments);
await context.SaveChangesAsync().ConfigureAwait(false);

var result = await controller.ExportJsonWithAttachmentsAsync([newBorehole.Id]).ConfigureAwait(false);

Assert.IsInstanceOfType(result, typeof(ObjectResult));
ObjectResult objectResult = (ObjectResult)result;
ProblemDetails problemDetails = (ProblemDetails)objectResult.Value!;
StringAssert.StartsWith(problemDetails.Detail, "An error occurred while fetching a file from the cloud storage.");
}

[TestMethod]
[DataRow(new int[] { }, typeof(BadRequestObjectResult), DisplayName = "Ids is empty list.")]
[DataRow(null, typeof(BadRequestObjectResult), DisplayName = "Ids is null.")]
Expand Down

0 comments on commit dcbf93e

Please sign in to comment.