From 55e2b4a6854d758497285ac42f3904aebca241f1 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Mon, 16 Dec 2024 14:57:20 +0100 Subject: [PATCH 01/38] Use export controller with csvConfig --- src/api/BdmsContextExtensions.cs | 3 + src/api/BoreholeGeometry/AzIncFormat.cs | 2 +- src/api/BoreholeGeometry/PitchRollFormat.cs | 2 +- src/api/BoreholeGeometry/XYZFormat.cs | 2 +- src/api/Controllers/BoreholeController.cs | 57 ----- src/api/Controllers/ExportController.cs | 163 +++++++++++++ src/api/Controllers/ImportController.cs | 12 +- .../{BoreholeGeometry => }/CsvConfigHelper.cs | 13 +- src/api/Models/Borehole.cs | 19 ++ src/client/cypress/e2e/helpers/testHelpers.js | 20 +- src/client/cypress/e2e/mainPage/export.cy.js | 171 ++++++++++++- .../api/Controllers/BoreholeControllerTest.cs | 55 ----- tests/api/Controllers/ExportControllerTest.cs | 230 ++++++++++++++++++ 13 files changed, 598 insertions(+), 151 deletions(-) create mode 100644 src/api/Controllers/ExportController.cs rename src/api/{BoreholeGeometry => }/CsvConfigHelper.cs (74%) create mode 100644 tests/api/Controllers/ExportControllerTest.cs diff --git a/src/api/BdmsContextExtensions.cs b/src/api/BdmsContextExtensions.cs index 8afbf51d7..587d27d3f 100644 --- a/src/api/BdmsContextExtensions.cs +++ b/src/api/BdmsContextExtensions.cs @@ -202,6 +202,9 @@ public static void SeedData(this BdmsContext context) .RuleFor(o => o.PrecisionLocationY, f => f.PickRandom(Enumerable.Range(0, 10))) .RuleFor(o => o.PrecisionLocationXLV03, f => f.PickRandom(Enumerable.Range(0, 10))) .RuleFor(o => o.PrecisionLocationYLV03, f => f.PickRandom(Enumerable.Range(0, 10))) + .RuleFor(o => o.TotalDepthTvd, _ => null) + .RuleFor(o => o.TopBedrockFreshTvd, _ => null) + .RuleFor(o => o.TopBedrockWeatheredTvd, _ => null) .RuleFor(o => o.Observations, _ => new Collection()) .FinishWith((f, o) => { o.Name = o.OriginalName; }); diff --git a/src/api/BoreholeGeometry/AzIncFormat.cs b/src/api/BoreholeGeometry/AzIncFormat.cs index 3f6e3d754..0e91c4575 100644 --- a/src/api/BoreholeGeometry/AzIncFormat.cs +++ b/src/api/BoreholeGeometry/AzIncFormat.cs @@ -21,7 +21,7 @@ internal sealed class AzIncFormat : IBoreholeGeometryFormat public IList ReadCsv(IFormFile file, int boreholeId) { using var reader = new StreamReader(file.OpenReadStream()); - using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig); + using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig); var data = csv.GetRecords().ToList(); diff --git a/src/api/BoreholeGeometry/PitchRollFormat.cs b/src/api/BoreholeGeometry/PitchRollFormat.cs index 05054b975..aadc4f0fe 100644 --- a/src/api/BoreholeGeometry/PitchRollFormat.cs +++ b/src/api/BoreholeGeometry/PitchRollFormat.cs @@ -21,7 +21,7 @@ internal sealed class PitchRollFormat : IBoreholeGeometryFormat public IList ReadCsv(IFormFile file, int boreholeId) { using var reader = new StreamReader(file.OpenReadStream()); - using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig); + using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig); var data = csv.GetRecords().ToList(); diff --git a/src/api/BoreholeGeometry/XYZFormat.cs b/src/api/BoreholeGeometry/XYZFormat.cs index f7787b395..1f142d0e6 100644 --- a/src/api/BoreholeGeometry/XYZFormat.cs +++ b/src/api/BoreholeGeometry/XYZFormat.cs @@ -20,7 +20,7 @@ internal sealed class XYZFormat : IBoreholeGeometryFormat public IList ReadCsv(IFormFile file, int boreholeId) { using var reader = new StreamReader(file.OpenReadStream()); - using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig); + using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig); var data = csv.GetRecords(); return ToBoreholeGeometry(data, boreholeId); diff --git a/src/api/Controllers/BoreholeController.cs b/src/api/Controllers/BoreholeController.cs index d25d2ab0c..9c283a541 100644 --- a/src/api/Controllers/BoreholeController.cs +++ b/src/api/Controllers/BoreholeController.cs @@ -120,63 +120,6 @@ public async Task> GetByIdAsync(int id) return Ok(borehole); } - /// - /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. - /// - /// The list of IDs for the boreholes to be exported. - /// A CSV file containing the details specified boreholes. - [HttpGet("export-csv")] - [Authorize(Policy = PolicyNames.Viewer)] - public async Task DownloadCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) - { - ids = ids.Take(MaxPageSize).ToList(); - if (!ids.Any()) return BadRequest("The list of IDs must not be empty."); - - var boreholes = await Context.Boreholes - .Where(borehole => ids.Contains(borehole.Id)) - .Select(b => new - { - b.Id, - b.OriginalName, - b.ProjectName, - b.Name, - b.RestrictionId, - b.RestrictionUntil, - b.NationalInterest, - b.LocationX, - b.LocationY, - b.LocationPrecisionId, - b.ElevationZ, - b.ElevationPrecisionId, - b.ReferenceElevation, - b.ReferenceElevationTypeId, - b.ReferenceElevationPrecisionId, - b.HrsId, - b.TypeId, - b.PurposeId, - b.StatusId, - b.Remarks, - b.TotalDepth, - b.DepthPrecisionId, - b.TopBedrockFreshMd, - b.TopBedrockWeatheredMd, - b.HasGroundwater, - b.LithologyTopBedrockId, - b.ChronostratigraphyTopBedrockId, - b.LithostratigraphyTopBedrockId, - }) - .ToListAsync() - .ConfigureAwait(false); - - if (boreholes.Count == 0) return NotFound("No borehole(s) found for the provided id(s)."); - - using var stringWriter = new StringWriter(); - using var csvWriter = new CsvWriter(stringWriter, CultureInfo.InvariantCulture); - await csvWriter.WriteRecordsAsync(boreholes).ConfigureAwait(false); - - return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv"); - } - /// /// Asynchronously copies a . /// diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs new file mode 100644 index 000000000..ee8988e58 --- /dev/null +++ b/src/api/Controllers/ExportController.cs @@ -0,0 +1,163 @@ +using BDMS.Authentication; +using BDMS.Models; +using CsvHelper; +using CsvHelper.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Text; + +namespace BDMS.Controllers; + +[ApiController] +[Route("api/v{version:apiVersion}/[controller]")] +public class ExportController : ControllerBase +{ + // Limit the maximum number of items per request to 100. + // This also applies to the number of filtered ids to ensure the URL length does not exceed the maximum allowed length. + private const int MaxPageSize = 100; + private readonly BdmsContext context; + private readonly ILogger logger; + + public ExportController(BdmsContext context, ILogger logger) + { + this.context = context; + this.logger = logger; + } + + /// + /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. + /// + /// The list of IDs for the boreholes to be exported. + /// A CSV file containing the details of the specified boreholes. + [HttpGet("export-csv")] + [Authorize(Policy = PolicyNames.Viewer)] + public async Task DownloadCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) + { + List idList = ids.Take(MaxPageSize).ToList(); + if (idList.Count < 1) return BadRequest("The list of IDs must not be empty."); + + var boreholes = await context.Boreholes + .Include(b => b.BoreholeCodelists) + .ThenInclude(bc => bc.Codelist) + .Where(borehole => idList.Contains(borehole.Id)) + .OrderBy(b => idList.IndexOf(b.Id)) + .ToListAsync() + .ConfigureAwait(false); + + if (boreholes.Count == 0) return NotFound("No borehole(s) found for the provided id(s)."); + + using var stringWriter = new StringWriter(); + using var csvWriter = new CsvWriter(stringWriter, CsvConfigHelper.CsvWriteConfig); + + // Write headers for standard fields + csvWriter.WriteField(nameof(Borehole.Id)); + csvWriter.WriteField(nameof(Borehole.OriginalName)); + csvWriter.WriteField(nameof(Borehole.ProjectName)); + csvWriter.WriteField(nameof(Borehole.Name)); + csvWriter.WriteField(nameof(Borehole.RestrictionId)); + csvWriter.WriteField(nameof(Borehole.RestrictionUntil)); + csvWriter.WriteField(nameof(Borehole.NationalInterest)); + csvWriter.WriteField(nameof(Borehole.LocationX)); + csvWriter.WriteField(nameof(Borehole.LocationY)); + csvWriter.WriteField(nameof(Borehole.LocationPrecisionId)); + csvWriter.WriteField(nameof(Borehole.ElevationZ)); + csvWriter.WriteField(nameof(Borehole.ElevationPrecisionId)); + csvWriter.WriteField(nameof(Borehole.ReferenceElevation)); + csvWriter.WriteField(nameof(Borehole.ReferenceElevationTypeId)); + csvWriter.WriteField(nameof(Borehole.ReferenceElevationPrecisionId)); + csvWriter.WriteField(nameof(Borehole.HrsId)); + csvWriter.WriteField(nameof(Borehole.TypeId)); + csvWriter.WriteField(nameof(Borehole.PurposeId)); + csvWriter.WriteField(nameof(Borehole.StatusId)); + csvWriter.WriteField(nameof(Borehole.Remarks)); + csvWriter.WriteField(nameof(Borehole.TotalDepth)); + csvWriter.WriteField(nameof(Borehole.DepthPrecisionId)); + csvWriter.WriteField(nameof(Borehole.TopBedrockFreshMd)); + csvWriter.WriteField(nameof(Borehole.TopBedrockWeatheredMd)); + csvWriter.WriteField(nameof(Borehole.TotalDepthTvd)); + csvWriter.WriteField(nameof(Borehole.TopBedrockFreshTvd)); + csvWriter.WriteField(nameof(Borehole.TopBedrockWeatheredTvd)); + csvWriter.WriteField(nameof(Borehole.HasGroundwater)); + csvWriter.WriteField(nameof(Borehole.LithologyTopBedrockId)); + csvWriter.WriteField(nameof(Borehole.ChronostratigraphyTopBedrockId)); + csvWriter.WriteField(nameof(Borehole.LithostratigraphyTopBedrockId)); + + // Write dynamic headers for each distinct custom Id + var customIdHeaders = boreholes + .SelectMany(b => b.BoreholeCodelists ?? Enumerable.Empty()) + .Select(bc => new { bc.CodelistId, bc.Codelist?.En }) + .Distinct() + .OrderBy(x => x.CodelistId) + .ToList(); + + foreach (var header in customIdHeaders) + { + csvWriter.WriteField(header.En.Replace(" ", "", StringComparison.OrdinalIgnoreCase)); + } + + // Move to the next line + await csvWriter.NextRecordAsync().ConfigureAwait(false); + + // Write data for standard fields + foreach (var b in boreholes) + { + var boreholeGeometry = await context.BoreholeGeometry + .AsNoTracking() + .Where(g => g.BoreholeId == b.Id) + .ToListAsync() + .ConfigureAwait(false); + + b.TotalDepthTvd = boreholeGeometry.GetTVDIfGeometryExists(b.TotalDepth); + b.TopBedrockFreshTvd = boreholeGeometry.GetTVDIfGeometryExists(b.TopBedrockFreshMd); + b.TopBedrockWeatheredTvd = boreholeGeometry.GetTVDIfGeometryExists(b.TopBedrockWeatheredMd); + + csvWriter.WriteField(b.Id); + csvWriter.WriteField(b.OriginalName); + csvWriter.WriteField(b.ProjectName); + csvWriter.WriteField(b.Name); + csvWriter.WriteField(b.RestrictionId); + csvWriter.WriteField(b.RestrictionUntil); + csvWriter.WriteField(b.NationalInterest); + csvWriter.WriteField(b.LocationX); + csvWriter.WriteField(b.LocationY); + csvWriter.WriteField(b.LocationPrecisionId); + csvWriter.WriteField(b.ElevationZ); + csvWriter.WriteField(b.ElevationPrecisionId); + csvWriter.WriteField(b.ReferenceElevation); + csvWriter.WriteField(b.ReferenceElevationTypeId); + csvWriter.WriteField(b.ReferenceElevationPrecisionId); + csvWriter.WriteField(b.HrsId); + csvWriter.WriteField(b.TypeId); + csvWriter.WriteField(b.PurposeId); + csvWriter.WriteField(b.StatusId); + csvWriter.WriteField(b.Remarks); + csvWriter.WriteField(b.TotalDepth); + csvWriter.WriteField(b.DepthPrecisionId); + csvWriter.WriteField(b.TopBedrockFreshMd); + csvWriter.WriteField(b.TopBedrockWeatheredMd); + csvWriter.WriteField(b.TotalDepthTvd); + csvWriter.WriteField(b.TopBedrockFreshTvd); + csvWriter.WriteField(b.TopBedrockWeatheredTvd); + csvWriter.WriteField(b.HasGroundwater); + csvWriter.WriteField(b.LithologyTopBedrockId); + csvWriter.WriteField(b.ChronostratigraphyTopBedrockId); + csvWriter.WriteField(b.LithostratigraphyTopBedrockId); + + // Write dynamic fields for custom Ids + foreach (var header in customIdHeaders) + { + var codelistValue = (b.BoreholeCodelists ?? Enumerable.Empty()).FirstOrDefault(bc => bc.CodelistId == header.CodelistId)?.Value; + csvWriter.WriteField(codelistValue ?? ""); + } + + // Move to the next line + await csvWriter.NextRecordAsync().ConfigureAwait(false); + } + + await csvWriter.FlushAsync().ConfigureAwait(false); + return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv"); + } +} diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index 091e85cf0..657ac00d2 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -25,13 +25,6 @@ public class ImportController : ControllerBase private readonly int sridLv95 = 2056; private readonly int sridLv03 = 21781; private readonly string nullOrEmptyMsg = "Field '{0}' is required."; - private readonly CsvConfiguration csvConfig = new(new CultureInfo("de-CH")) - { - Delimiter = ";", - IgnoreReferences = true, - PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title), - MissingFieldFound = null, - }; private static readonly JsonSerializerOptions jsonImportOptions = new() { PropertyNameCaseInsensitive = true }; @@ -350,7 +343,7 @@ internal static bool CompareValuesWithTolerance(double? firstValue, double? seco private List ReadBoreholesFromCsv(IFormFile file) { using var reader = new StreamReader(file.OpenReadStream()); - using var csv = new CsvReader(reader, csvConfig); + using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig); csv.Context.RegisterClassMap(new CsvImportBoreholeMap()); @@ -445,6 +438,9 @@ public CsvImportBoreholeMap() Map(b => b.Canton).Ignore(); Map(b => b.Country).Ignore(); Map(m => m.Id).Ignore(); + Map(m => m.TotalDepthTvd).Ignore(); + Map(m => m.TopBedrockFreshTvd).Ignore(); + Map(m => m.TopBedrockWeatheredTvd).Ignore(); // Define additional mapping logic Map(m => m.BoreholeCodelists).Convert(args => diff --git a/src/api/BoreholeGeometry/CsvConfigHelper.cs b/src/api/CsvConfigHelper.cs similarity index 74% rename from src/api/BoreholeGeometry/CsvConfigHelper.cs rename to src/api/CsvConfigHelper.cs index 211f2a71d..8c523a3b2 100644 --- a/src/api/BoreholeGeometry/CsvConfigHelper.cs +++ b/src/api/CsvConfigHelper.cs @@ -6,11 +6,11 @@ using NetTopologySuite.Utilities; using System.Globalization; -namespace BDMS.BoreholeGeometry; +namespace BDMS; public static class CsvConfigHelper { - internal static readonly CsvConfiguration CsvConfig = new(new CultureInfo("de-CH")) + internal static readonly CsvConfiguration CsvReadConfig = new(new CultureInfo("de-CH")) { Delimiter = ";", IgnoreReferences = true, @@ -18,15 +18,20 @@ public static class CsvConfigHelper MissingFieldFound = null, }; + internal static readonly CsvConfiguration CsvWriteConfig = new(new CultureInfo("de-CH")) + { + Delimiter = ";", + }; + /// /// Get the CSV header for a class of type . - /// Uses the map generated by . + /// Uses the map generated by . /// If a property has multiple possible column names only the first is considered. /// /// The class to get the header for. internal static string GetCsvHeader() { - var context = new CsvContext(CsvConfig); + var context = new CsvContext(CsvReadConfig); var map = context.AutoMap(); return string.Join("; ", map.MemberMaps .Select(m => diff --git a/src/api/Models/Borehole.cs b/src/api/Models/Borehole.cs index bc7017a41..56b9d79d4 100644 --- a/src/api/Models/Borehole.cs +++ b/src/api/Models/Borehole.cs @@ -1,5 +1,6 @@ using NetTopologySuite.Geometries; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics; using System.Text.Json.Serialization; namespace BDMS.Models; @@ -350,6 +351,24 @@ public class Borehole : IChangeTracking, IIdentifyable [Column("reference_elevation_type_id_cli")] public int? ReferenceElevationTypeId { get; set; } + /// + /// Gets or sets the 's true vertical total depth. + /// + [NotMapped] + public double? TotalDepthTvd { get; set; } + + /// + /// Gets or sets the 's true vertical top bedrock fresh depth. + /// + [NotMapped] + public double? TopBedrockFreshTvd { get; set; } + + /// + /// Gets or sets the 's true vertical top bedrock weathered depth. + /// + [NotMapped] + public double? TopBedrockWeatheredTvd { get; set; } + /// /// Gets or sets the 's reference elevation type. /// diff --git a/src/client/cypress/e2e/helpers/testHelpers.js b/src/client/cypress/e2e/helpers/testHelpers.js index 8b125d7f1..f6c5afe60 100644 --- a/src/client/cypress/e2e/helpers/testHelpers.js +++ b/src/client/cypress/e2e/helpers/testHelpers.js @@ -263,6 +263,10 @@ export const returnToOverview = () => { cy.wait(["@edit_list", "@borehole"]); }; +export const getElementByDataCy = attribute => { + return cy.get(`[data-cy=${attribute}]`); +}; + export const deleteBorehole = id => { cy.get("@id_token").then(token => { cy.request({ @@ -332,26 +336,20 @@ export const setValueOfInputElement = function (inputElement, inputValue) { // Deletes a downloaded file in Cypress' downloads folder export const deleteDownloadedFile = fileName => { // Get the path to the downloaded file you want to delete - let filePath = "cypress/downloads/" + fileName; + const filePath = Cypress.platform === "win32" ? `cypress\\downloads\\${fileName}` : `cypress/downloads/${fileName}`; // If file exists in download folder, delete it. - cy.task("fileExistsInDownloadFolder", "languages/en.yml").then(exists => { + cy.task("fileExistsInDownloadFolder", filePath).then(exists => { if (exists) { - // Set the command in case of linux os - let command = "rm -f"; - - // Override the command and path in case of windows os - if (Cypress.platform === "win32") { - command = "del"; - filePath = "cypress\\downloads\\" + fileName; - } + // Set the command to delete the file based on the OS + const command = Cypress.platform === "win32" ? "del" : "rm -f"; cy.exec(`${command} ${filePath}`).then(result => { // Check if the command executed successfully expect(result.code).to.equal(0); // Check that the file has been deleted - cy.readFile(filePath, { log: false }).should("not.exist"); + cy.readFile(filePath, { log: false, timeout: 10000 }).should("not.exist"); }); } }); diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index 5be5dcddb..06b9bec38 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -1,45 +1,192 @@ -import { exportCSVItem, exportJsonItem } from "../helpers/buttonHelpers"; +import { addItem, deleteItem, exportCSVItem, exportJsonItem, saveWithSaveBar } from "../helpers/buttonHelpers"; import { checkAllVisibleRows, checkRowWithText, showTableAndWaitForData } from "../helpers/dataGridHelpers.js"; +import { evaluateInput, setInput, setSelect } from "../helpers/formHelpers"; import { createBorehole, deleteDownloadedFile, + getElementByDataCy, + getImportFileFromFixtures, + goToRouteAndAcceptTerms, handlePrompt, - loginAsAdmin, + newEditableBorehole, prepareDownloadPath, readDownloadedFile, + returnToOverview, + startBoreholeEditing, + stopBoreholeEditing, } from "../helpers/testHelpers"; const jsonFileName = `bulkexport_${new Date().toISOString().split("T")[0]}.json`; const csvFileName = `bulkexport_${new Date().toISOString().split("T")[0]}.csv`; +const splitFileContent = fileContent => { + const lines = fileContent.split("\n"); + const rows = lines.map(row => row.split(";")); + return { lines, rows }; +}; + +const verifyTVDContentInCSVFile = ( + fileName, + expectedTotalDepthVD, + expectedTopBedrockFreshTVD, + expectedTopBedrockWeatheredTVD, +) => { + cy.readFile(prepareDownloadPath(fileName)).then(fileContent => { + const { lines, rows } = splitFileContent(fileContent); + expect(lines.length).to.equal(3); + expect(rows[0][24]).to.equal("TotalDepthTvd"); + expect(rows[1][24]).to.equal(expectedTotalDepthVD); + expect(rows[0][25]).to.equal("TopBedrockFreshTvd"); + expect(rows[1][25]).to.equal(expectedTopBedrockFreshTVD); + expect(rows[0][26]).to.equal("TopBedrockWeatheredTvd"); + expect(rows[1][26]).to.equal(expectedTopBedrockWeatheredTVD); + }); +}; + describe("Test for exporting boreholes.", () => { it("bulk exports boreholes to json and csv", () => { + deleteDownloadedFile(jsonFileName); + deleteDownloadedFile(csvFileName); createBorehole({ "extended.original_name": "AAA_NINTIC", "custom.alternate_name": "AAA_NINTIC" }).as( "borehole_id_1", ); createBorehole({ "extended.original_name": "AAA_LOMONE", "custom.alternate_name": "AAA_LOMONE" }).as( "borehole_id_2", ); - loginAsAdmin(); + goToRouteAndAcceptTerms("/"); showTableAndWaitForData(); - cy.get('[data-cy="borehole-table"]').within(() => { - checkRowWithText("AAA_NINTIC"); - checkRowWithText("AAA_LOMONE"); - }); + checkRowWithText("AAA_NINTIC"); + checkRowWithText("AAA_LOMONE"); - deleteDownloadedFile(jsonFileName); - deleteDownloadedFile(csvFileName); exportJsonItem(); exportCSVItem(); readDownloadedFile(jsonFileName); readDownloadedFile(csvFileName); + deleteItem(); // bulk delete all added boreholes + handlePrompt("Do you really want to delete these 2 boreholes? This cannot be undone.", "Delete"); + }); + + it("exports TVD for a borehole with and without geometry", () => { + const boreholeName = "AAA_FROGGY"; + const secondBoreholeName = "AAA_FISHY"; + const fileName = `${boreholeName}.csv`; + const secondFileName = `${secondBoreholeName}.csv`; + + deleteDownloadedFile(fileName); + deleteDownloadedFile(secondFileName); + + createBorehole({ "extended.original_name": boreholeName, "custom.alternate_name": boreholeName }).as("borehole_id"); + + cy.get("@borehole_id").then(id => { + goToRouteAndAcceptTerms(`/${id}`); + }); + + // add geometry to borehole and verify export tvd changed + getElementByDataCy("borehole-menu-item").click(); + startBoreholeEditing(); + setInput("totalDepth", 700); + setInput("topBedrockFreshMd", 800); + setInput("topBedrockWeatheredMd", 900); + evaluateInput("totalDepth", "700"); + evaluateInput("total_depth_tvd", "700"); + evaluateInput("topBedrockFreshMd", "800"); + evaluateInput("top_bedrock_fresh_tvd", "800"); + evaluateInput("topBedrockWeatheredMd", "900"); + evaluateInput("top_bedrock_weathered_tvd", "900"); + + saveWithSaveBar(); + + stopBoreholeEditing(); + exportCSVItem(); + + verifyTVDContentInCSVFile(fileName, "700", "800", "900"); + startBoreholeEditing(); + + getElementByDataCy("geometry-tab").click(); + getElementByDataCy("boreholegeometryimport-button").should("be.disabled"); + + // upload geometry csv file + let geometryFile = new DataTransfer(); + getImportFileFromFixtures("geometry_azimuth_inclination.csv", null).then(fileContent => { + const file = new File([fileContent], "geometry_azimuth_inclination.csv", { + type: "text/csv", + }); + geometryFile.items.add(file); + }); + getElementByDataCy("import-geometry-input").within(() => { + cy.get("input[type=file]", { force: true }).then(input => { + input[0].files = geometryFile.files; + input[0].dispatchEvent(new Event("change", { bubbles: true })); + }); + }); + + setSelect("geometryFormat", 1); + getElementByDataCy("boreholegeometryimport-button").click(); + cy.wait(["@boreholegeometry_POST", "@boreholegeometry_GET"]); + cy.get(".MuiTableBody-root").should("exist"); + + getElementByDataCy("general-tab").click(); + evaluateInput("totalDepth", "700"); + evaluateInput("total_depth_tvd", "674.87"); + getElementByDataCy("location-menu-item").click(); + setInput("originalName", secondBoreholeName); // change name to avoid potential CSV filename conflict + saveWithSaveBar(); + stopBoreholeEditing(); + exportCSVItem(); + verifyTVDContentInCSVFile(secondFileName, "674.8678208299723", "762.6098263945338", "846.9637100889873"); + startBoreholeEditing(); + getElementByDataCy("deleteborehole-button").click(); + handlePrompt("Do you really want to delete this borehole? This cannot be undone.", "Delete"); + }); + + it("exports custom Ids form borehole", () => { + const firstBoreholeName = "AAA_DUCKY"; + const secondBoreholeName = "AAA_SNAKEY"; + deleteDownloadedFile(csvFileName); + newEditableBorehole().as("borehole_id"); + setInput("name", firstBoreholeName); + addItem("addIdentifier"); + setSelect("boreholeCodelists.0.codelistId", 3); + setInput("boreholeCodelists.0.value", 13); + saveWithSaveBar(); + returnToOverview(); + + newEditableBorehole().as("borehole_id_2"); + setInput("name", secondBoreholeName); + addItem("addIdentifier"); + setSelect("boreholeCodelists.0.codelistId", 4); + setInput("boreholeCodelists.0.value", 14); + saveWithSaveBar(); + returnToOverview(); + showTableAndWaitForData(); + checkRowWithText(firstBoreholeName); + checkRowWithText(secondBoreholeName); + exportCSVItem(); + cy.readFile(prepareDownloadPath(csvFileName)).then(fileContent => { + const { lines, rows } = splitFileContent(fileContent); + expect(lines.length).to.equal(4); + + expect(rows[0][3]).to.equal("Name"); + expect(rows[1][3]).to.equal(firstBoreholeName); + expect(rows[2][3]).to.equal(secondBoreholeName); + + expect(rows[0][31]).to.equal("IDInfoGeol"); + expect(rows[1][31]).to.equal(""); + expect(rows[2][31]).to.equal("14"); + + expect(rows[0][32]).to.equal("IDGeODin\r"); + expect(rows[1][32]).to.equal("13\r"); + expect(rows[2][32]).to.equal("\r"); + }); + deleteItem(); + handlePrompt("Do you really want to delete these 2 boreholes? This cannot be undone.", "Delete"); }); it("downloads a maximum of 100 boreholes", () => { - loginAsAdmin(); + deleteDownloadedFile(csvFileName); + deleteDownloadedFile(jsonFileName); showTableAndWaitForData(); checkAllVisibleRows(); - deleteDownloadedFile(csvFileName); exportCSVItem(); const moreThan100SelectedPrompt = @@ -56,8 +203,6 @@ describe("Test for exporting boreholes.", () => { const lines = fileContent.split("\n"); expect(lines.length).to.equal(102); }); - - deleteDownloadedFile(jsonFileName); exportJsonItem(); handlePrompt(moreThan100SelectedPrompt, "Cancel"); }); diff --git a/tests/api/Controllers/BoreholeControllerTest.cs b/tests/api/Controllers/BoreholeControllerTest.cs index 68708fe28..22c17b6a5 100644 --- a/tests/api/Controllers/BoreholeControllerTest.cs +++ b/tests/api/Controllers/BoreholeControllerTest.cs @@ -687,59 +687,4 @@ public async Task CopyWithNonAdminUser() Assert.IsNotNull(copiedBoreholeId); Assert.IsInstanceOfType(copiedBoreholeId, typeof(int)); } - - [TestMethod] - public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes() - { - var ids = Enumerable.Range(testBoreholeId, 120).ToList(); - - var result = await controller.DownloadCsvAsync(ids) as FileContentResult; - - Assert.IsNotNull(result); - Assert.AreEqual("text/csv", result.ContentType); - Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); - var csvData = Encoding.UTF8.GetString(result.FileContents); - var fileLength = csvData.Split('\n').Length; - var recordCount = fileLength - 2; // Remove header and last line break - Assert.AreEqual(100, recordCount); - } - - [TestMethod] - public async Task DownloadCsvWithInvalidIdsReturnsNotFound() - { - var ids = new List { 8, 2, 11, 87 }; - - var result = await controller.DownloadCsvAsync(ids) as NotFoundObjectResult; - - Assert.IsNotNull(result); - Assert.AreEqual("No borehole(s) found for the provided id(s).", result.Value); - } - - [TestMethod] - public async Task DownloadCsvWithPartiallyValidIdsReturnsFileForPartillyValidIds() - { - var ids = new List { 9, 8, 0, testBoreholeId }; - - var result = await controller.DownloadCsvAsync(ids) as FileContentResult; - - Assert.IsNotNull(result); - Assert.IsNotNull(result); - Assert.AreEqual("text/csv", result.ContentType); - Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); - var csvData = Encoding.UTF8.GetString(result.FileContents); - var fileLength = csvData.Split('\n').Length; - var recordCount = fileLength - 2; - Assert.AreEqual(recordCount, 1); - } - - [TestMethod] - public async Task DownloadCsvEmptyIdsReturnsBadRequest() - { - var ids = new List(); - - var result = await controller.DownloadCsvAsync(ids) as BadRequestObjectResult; - - Assert.IsNotNull(result); - Assert.AreEqual("The list of IDs must not be empty.", result.Value); - } } diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs new file mode 100644 index 000000000..2d0e4d806 --- /dev/null +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -0,0 +1,230 @@ +using BDMS.Models; +using CsvHelper; +using CsvHelper.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Globalization; +using System.Text; +using static BDMS.Helpers; + +namespace BDMS.Controllers; + +[DeploymentItem("TestData")] +[TestClass] +public class ExportControllerTest +{ + private BdmsContext context; + private ExportController controller; + private Mock> loggerMock; + private static int testBoreholeId = 1000068; + + [TestInitialize] + public void TestInitialize() + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.Development.json").Build(); + + context = ContextFactory.CreateContext(); + loggerMock = new Mock>(); + + controller = new ExportController(context, loggerMock.Object) { ControllerContext = GetControllerContextAdmin() }; + } + + [TestMethod] + public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes() + { + var ids = Enumerable.Range(testBoreholeId, 120).ToList(); + + var result = await controller.DownloadCsvAsync(ids) as FileContentResult; + + Assert.IsNotNull(result); + Assert.AreEqual("text/csv", result.ContentType); + Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + var csvData = Encoding.UTF8.GetString(result.FileContents); + var fileLength = csvData.Split('\n').Length; + var recordCount = fileLength - 2; // Remove header and last line break + Assert.AreEqual(100, recordCount); + } + + [TestMethod] + public async Task DownloadCsvReturnsTVD() + { + var boreholeQuery = context.Boreholes + .Include(b => b.BoreholeGeometry); + + var boreholeIdsWithoutGeometry = boreholeQuery + .Where(b => b.BoreholeGeometry.Count < 2) + .Take(3).Select(b => b.Id); + + var boreholeIdsWithGeometry = boreholeQuery + .Where(b => b.BoreholeGeometry.Count > 1) + .Take(3).Select(b => b.Id); + + var boreholeIds = await boreholeIdsWithoutGeometry.Concat(boreholeIdsWithGeometry).ToListAsync(); + var result = await controller.DownloadCsvAsync(boreholeIds) as FileContentResult; + + Assert.IsNotNull(result); + Assert.AreEqual("text/csv", result.ContentType); + Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + var records = GetRecordsFromFileContent(result); + + foreach (var record in records) + { + var totalDepthTvd = record.TotalDepthTvd; + var totalDepthMd = record.TotalDepth; + var topBedrockFreshTvd = record.TopBedrockFreshTvd; + var topBedrockFreshMd = record.TopBedrockFreshMd; + var topBedrockWeatheredTvd = record.TopBedrockWeatheredTvd; + var topBedrockWeatheredMd = record.TopBedrockWeatheredMd; + + if (boreholeIdsWithoutGeometry.Select(b => b.ToString()).ToList().Contains(record.Id)) + { + Assert.AreEqual(totalDepthMd, totalDepthTvd); + Assert.AreEqual(topBedrockFreshMd, topBedrockFreshTvd); + Assert.AreEqual(topBedrockWeatheredMd, topBedrockWeatheredTvd); + } + + if (boreholeIdsWithGeometry.Select(b => b.ToString()).ToList().Contains(record.Id)) + { + Assert.AreNotEqual(totalDepthMd, totalDepthTvd); + Assert.AreNotEqual(topBedrockFreshMd, topBedrockFreshTvd); + if (topBedrockWeatheredMd != "") + { + Assert.AreNotEqual(topBedrockWeatheredMd, topBedrockWeatheredTvd); + } + else + { + Assert.AreEqual("", topBedrockWeatheredTvd); + } + } + } + + // Assert values for single borehole with geometry + var singleRecord = records.Single(r => r.Id == "1000002"); + Assert.AreEqual("680.5358560199551", singleRecord.TotalDepth); + Assert.AreEqual("601.9441138962023", singleRecord.TopBedrockFreshMd); + Assert.AreEqual("", singleRecord.TopBedrockWeatheredMd); + Assert.AreEqual("216.25173394135473", singleRecord.TotalDepthTvd); + Assert.AreEqual("191.34988682963814", singleRecord.TopBedrockFreshTvd); + Assert.AreEqual("", singleRecord.TopBedrockWeatheredTvd); + } + + [TestMethod] + public async Task DownloadCsvWithCustomIds() + { + var firstBoreholeId = 1_009_068; + var boreholeWithCustomIds = new Borehole + { + Id = firstBoreholeId, + BoreholeCodelists = new List + { + new BoreholeCodelist + { + BoreholeId = firstBoreholeId, + CodelistId = 100000010, + Value = "ID GeoDIN value", + }, + new BoreholeCodelist + { + BoreholeId = firstBoreholeId, + CodelistId = 100000011, + Value = "ID Kernlager value", + }, + }, + }; + + var secondBoreholeId = 1_009_069; + var boreholeWithOtherCustomIds = new Borehole + { + Id = secondBoreholeId, + BoreholeCodelists = new List + { + new BoreholeCodelist + { + BoreholeId = secondBoreholeId, + CodelistId = 100000010, + Value = "ID GeoDIN value", + }, + new BoreholeCodelist + { + BoreholeId = secondBoreholeId, + CodelistId = 100000009, + Value = "ID TopFels value", + }, + }, + }; + + context.AddRange(boreholeWithCustomIds, boreholeWithOtherCustomIds); + + var ids = new List { firstBoreholeId, secondBoreholeId }; + + var result = await controller.DownloadCsvAsync(ids) as FileContentResult; + Assert.IsNotNull(result); + Assert.AreEqual("text/csv", result.ContentType); + Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + + var records = GetRecordsFromFileContent(result); + + var firstBorehole = records.Find(r => r.Id == firstBoreholeId.ToString()); + Assert.IsNotNull(firstBorehole); + Assert.AreEqual("ID GeoDIN value", firstBorehole.IDGeODin); + Assert.AreEqual("ID Kernlager value", firstBorehole.IDKernlager); + Assert.AreEqual("", firstBorehole.IDTopFels); + + var secondBorehole = records.Find(r => r.Id == secondBoreholeId.ToString()); + Assert.IsNotNull(secondBorehole); + Assert.AreEqual("ID GeoDIN value", secondBorehole.IDGeODin); + Assert.AreEqual("", secondBorehole.IDKernlager); + Assert.AreEqual("ID TopFels value", secondBorehole.IDTopFels); + } + + [TestMethod] + public async Task DownloadCsvWithInvalidIdsReturnsNotFound() + { + var ids = new List { 8, 2, 11, 87 }; + + var result = await controller.DownloadCsvAsync(ids) as NotFoundObjectResult; + + Assert.IsNotNull(result); + Assert.AreEqual("No borehole(s) found for the provided id(s).", result.Value); + } + + [TestMethod] + public async Task DownloadCsvWithPartiallyValidIdsReturnsFileForPartillyValidIds() + { + var ids = new List { 9, 8, 0, testBoreholeId }; + + var result = await controller.DownloadCsvAsync(ids) as FileContentResult; + + Assert.IsNotNull(result); + Assert.IsNotNull(result); + Assert.AreEqual("text/csv", result.ContentType); + Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + var csvData = Encoding.UTF8.GetString(result.FileContents); + var fileLength = csvData.Split('\n').Length; + var recordCount = fileLength - 2; + Assert.AreEqual(recordCount, 1); + } + + [TestMethod] + public async Task DownloadCsvEmptyIdsReturnsBadRequest() + { + var ids = new List(); + + var result = await controller.DownloadCsvAsync(ids) as BadRequestObjectResult; + + Assert.IsNotNull(result); + Assert.AreEqual("The list of IDs must not be empty.", result.Value); + } + + private static List GetRecordsFromFileContent(FileContentResult result) + { + var memoryStream = new MemoryStream(result.FileContents); + var reader = new StreamReader(memoryStream); + var csv = new CsvReader(reader, CsvConfigHelper.CsvWriteConfig); + return csv.GetRecords().ToList(); + } +} From 51170e3320abcf9bed164a3a65992cf7a0569ad5 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 12:13:01 +0100 Subject: [PATCH 02/38] Fix endpoint path --- src/api/Controllers/ExportController.cs | 4 +- src/client/cypress/e2e/helpers/testHelpers.js | 2 +- src/client/src/api/borehole.ts | 2 +- tests/api/Controllers/ExportControllerTest.cs | 81 +++++++++++-------- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index ee8988e58..86eaa9d22 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -32,9 +32,9 @@ public ExportController(BdmsContext context, ILogger logger) /// /// The list of IDs for the boreholes to be exported. /// A CSV file containing the details of the specified boreholes. - [HttpGet("export-csv")] + [HttpGet("csv")] [Authorize(Policy = PolicyNames.Viewer)] - public async Task DownloadCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) + public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) { List idList = ids.Take(MaxPageSize).ToList(); if (idList.Count < 1) return BadRequest("The list of IDs must not be empty."); diff --git a/src/client/cypress/e2e/helpers/testHelpers.js b/src/client/cypress/e2e/helpers/testHelpers.js index f6c5afe60..9811eb999 100644 --- a/src/client/cypress/e2e/helpers/testHelpers.js +++ b/src/client/cypress/e2e/helpers/testHelpers.js @@ -33,7 +33,7 @@ export const interceptApiCalls = () => { cy.intercept("PUT", "/api/v2/layer").as("update-layer"); cy.intercept("/api/v2/location/identify**").as("location"); cy.intercept("/api/v2/borehole/copy*").as("borehole_copy"); - cy.intercept("/api/v2/borehole/export-csv**").as("borehole_export_csv"); + cy.intercept("/api/v2/export/csv**").as("borehole_export_csv"); cy.intercept("/api/v2/borehole/**").as("borehole_by_id"); cy.intercept("PUT", "/api/v2/borehole").as("update-borehole"); diff --git a/src/client/src/api/borehole.ts b/src/client/src/api/borehole.ts index 2f4ff93dc..3d922cd2e 100644 --- a/src/client/src/api/borehole.ts +++ b/src/client/src/api/borehole.ts @@ -87,5 +87,5 @@ export const getAllBoreholes = async (ids: number[] | GridRowSelectionModel, pag export const exportCSVBorehole = async (boreholeIds: GridRowSelectionModel) => { const idsQuery = boreholeIds.map(id => `ids=${id}`).join("&"); - return await fetchApiV2(`borehole/export-csv?${idsQuery}`, "GET"); + return await fetchApiV2(`export/csv?${idsQuery}`, "GET"); }; diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index 2d0e4d806..18bd9b507 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -19,6 +19,7 @@ public class ExportControllerTest { private BdmsContext context; private ExportController controller; + private BoreholeController boreholeController; private Mock> loggerMock; private static int testBoreholeId = 1000068; @@ -27,9 +28,13 @@ public void TestInitialize() { var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.Development.json").Build(); - context = ContextFactory.CreateContext(); + context = ContextFactory.GetTestContext(); loggerMock = new Mock>(); - + var boreholeLockServiceMock = new Mock(MockBehavior.Strict); + boreholeLockServiceMock + .Setup(x => x.IsBoreholeLockedAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + boreholeController = new BoreholeController(context, new Mock>().Object, boreholeLockServiceMock.Object) { ControllerContext = GetControllerContextAdmin() }; controller = new ExportController(context, loggerMock.Object) { ControllerContext = GetControllerContextAdmin() }; } @@ -38,7 +43,7 @@ public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes() { var ids = Enumerable.Range(testBoreholeId, 120).ToList(); - var result = await controller.DownloadCsvAsync(ids) as FileContentResult; + var result = await controller.ExportCsvAsync(ids) as FileContentResult; Assert.IsNotNull(result); Assert.AreEqual("text/csv", result.ContentType); @@ -64,7 +69,7 @@ public async Task DownloadCsvReturnsTVD() .Take(3).Select(b => b.Id); var boreholeIds = await boreholeIdsWithoutGeometry.Concat(boreholeIdsWithGeometry).ToListAsync(); - var result = await controller.DownloadCsvAsync(boreholeIds) as FileContentResult; + var result = await controller.ExportCsvAsync(boreholeIds) as FileContentResult; Assert.IsNotNull(result); Assert.AreEqual("text/csv", result.ContentType); @@ -115,25 +120,32 @@ public async Task DownloadCsvReturnsTVD() [TestMethod] public async Task DownloadCsvWithCustomIds() { + // These codelists are used to make the TestContext aware of the Codelists, so that they can be included in the download controller. + var codelistGeoDIN = new Codelist { Id = 100000010, En = "ID GeODin" }; + var codelistKernlager = new Codelist { Id = 100000011, En = "ID Kernlager" }; + var codelistTopFels = new Codelist { Id = 100000009, En = "ID TopFels" }; + var firstBoreholeId = 1_009_068; var boreholeWithCustomIds = new Borehole { Id = firstBoreholeId, BoreholeCodelists = new List + { + new BoreholeCodelist { - new BoreholeCodelist - { - BoreholeId = firstBoreholeId, - CodelistId = 100000010, - Value = "ID GeoDIN value", - }, - new BoreholeCodelist - { - BoreholeId = firstBoreholeId, - CodelistId = 100000011, - Value = "ID Kernlager value", - }, + BoreholeId = firstBoreholeId, + CodelistId = codelistGeoDIN.Id, + Codelist = codelistGeoDIN, + Value = "ID GeoDIN value", + }, + new BoreholeCodelist + { + BoreholeId = firstBoreholeId, + CodelistId = codelistKernlager.Id, + Codelist = codelistKernlager, + Value = "ID Kernlager value", }, + }, }; var secondBoreholeId = 1_009_069; @@ -141,27 +153,30 @@ public async Task DownloadCsvWithCustomIds() { Id = secondBoreholeId, BoreholeCodelists = new List + { + new BoreholeCodelist { - new BoreholeCodelist - { - BoreholeId = secondBoreholeId, - CodelistId = 100000010, - Value = "ID GeoDIN value", - }, - new BoreholeCodelist - { - BoreholeId = secondBoreholeId, - CodelistId = 100000009, - Value = "ID TopFels value", - }, + BoreholeId = secondBoreholeId, + CodelistId = codelistGeoDIN.Id, + Codelist = codelistGeoDIN, + Value = "ID GeoDIN value", + }, + new BoreholeCodelist + { + BoreholeId = secondBoreholeId, + CodelistId = codelistTopFels.Id, + Codelist = codelistTopFels, + Value = "ID TopFels value", }, + }, }; - context.AddRange(boreholeWithCustomIds, boreholeWithOtherCustomIds); + await boreholeController.CreateAsync(boreholeWithCustomIds).ConfigureAwait(false); + await boreholeController.CreateAsync(boreholeWithOtherCustomIds).ConfigureAwait(false); var ids = new List { firstBoreholeId, secondBoreholeId }; - var result = await controller.DownloadCsvAsync(ids) as FileContentResult; + var result = await controller.ExportCsvAsync(ids) as FileContentResult; Assert.IsNotNull(result); Assert.AreEqual("text/csv", result.ContentType); Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); @@ -186,7 +201,7 @@ public async Task DownloadCsvWithInvalidIdsReturnsNotFound() { var ids = new List { 8, 2, 11, 87 }; - var result = await controller.DownloadCsvAsync(ids) as NotFoundObjectResult; + var result = await controller.ExportCsvAsync(ids) as NotFoundObjectResult; Assert.IsNotNull(result); Assert.AreEqual("No borehole(s) found for the provided id(s).", result.Value); @@ -197,7 +212,7 @@ public async Task DownloadCsvWithPartiallyValidIdsReturnsFileForPartillyValidIds { var ids = new List { 9, 8, 0, testBoreholeId }; - var result = await controller.DownloadCsvAsync(ids) as FileContentResult; + var result = await controller.ExportCsvAsync(ids) as FileContentResult; Assert.IsNotNull(result); Assert.IsNotNull(result); @@ -214,7 +229,7 @@ public async Task DownloadCsvEmptyIdsReturnsBadRequest() { var ids = new List(); - var result = await controller.DownloadCsvAsync(ids) as BadRequestObjectResult; + var result = await controller.ExportCsvAsync(ids) as BadRequestObjectResult; Assert.IsNotNull(result); Assert.AreEqual("The list of IDs must not be empty.", result.Value); From f1816e88835ded759976bd2bfdb7d2272408f8fb Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 12:19:13 +0100 Subject: [PATCH 03/38] Add LV03 to export --- src/api/Controllers/ExportController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 86eaa9d22..999eb3a51 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -28,7 +28,7 @@ public ExportController(BdmsContext context, ILogger logger) } /// - /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. + /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs.export /// /// The list of IDs for the boreholes to be exported. /// A CSV file containing the details of the specified boreholes. @@ -62,6 +62,8 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz csvWriter.WriteField(nameof(Borehole.NationalInterest)); csvWriter.WriteField(nameof(Borehole.LocationX)); csvWriter.WriteField(nameof(Borehole.LocationY)); + csvWriter.WriteField(nameof(Borehole.LocationXLV03)); + csvWriter.WriteField(nameof(Borehole.LocationYLV03)); csvWriter.WriteField(nameof(Borehole.LocationPrecisionId)); csvWriter.WriteField(nameof(Borehole.ElevationZ)); csvWriter.WriteField(nameof(Borehole.ElevationPrecisionId)); @@ -123,6 +125,8 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz csvWriter.WriteField(b.NationalInterest); csvWriter.WriteField(b.LocationX); csvWriter.WriteField(b.LocationY); + csvWriter.WriteField(b.LocationXLV03); + csvWriter.WriteField(b.LocationYLV03); csvWriter.WriteField(b.LocationPrecisionId); csvWriter.WriteField(b.ElevationZ); csvWriter.WriteField(b.ElevationPrecisionId); From 21bcb31fcfadcb0b2f7449441f419e6470240ef2 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 12:27:32 +0100 Subject: [PATCH 04/38] Fix warning --- src/api/Controllers/ExportController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 999eb3a51..5a7046bb3 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -28,7 +28,7 @@ public ExportController(BdmsContext context, ILogger logger) } /// - /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs.export + /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. /// /// The list of IDs for the boreholes to be exported. /// A CSV file containing the details of the specified boreholes. From 797f16188972c4b3e2944fcb45473234e5cb9848 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 12:57:26 +0100 Subject: [PATCH 05/38] Fix export controller test --- tests/api/Controllers/ExportControllerTest.cs | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index 18bd9b507..7cb2323c0 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -19,7 +19,6 @@ public class ExportControllerTest { private BdmsContext context; private ExportController controller; - private BoreholeController boreholeController; private Mock> loggerMock; private static int testBoreholeId = 1000068; @@ -30,11 +29,6 @@ public void TestInitialize() context = ContextFactory.GetTestContext(); loggerMock = new Mock>(); - var boreholeLockServiceMock = new Mock(MockBehavior.Strict); - boreholeLockServiceMock - .Setup(x => x.IsBoreholeLockedAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(false); - boreholeController = new BoreholeController(context, new Mock>().Object, boreholeLockServiceMock.Object) { ControllerContext = GetControllerContextAdmin() }; controller = new ExportController(context, loggerMock.Object) { ControllerContext = GetControllerContextAdmin() }; } @@ -120,11 +114,6 @@ public async Task DownloadCsvReturnsTVD() [TestMethod] public async Task DownloadCsvWithCustomIds() { - // These codelists are used to make the TestContext aware of the Codelists, so that they can be included in the download controller. - var codelistGeoDIN = new Codelist { Id = 100000010, En = "ID GeODin" }; - var codelistKernlager = new Codelist { Id = 100000011, En = "ID Kernlager" }; - var codelistTopFels = new Codelist { Id = 100000009, En = "ID TopFels" }; - var firstBoreholeId = 1_009_068; var boreholeWithCustomIds = new Borehole { @@ -134,15 +123,13 @@ public async Task DownloadCsvWithCustomIds() new BoreholeCodelist { BoreholeId = firstBoreholeId, - CodelistId = codelistGeoDIN.Id, - Codelist = codelistGeoDIN, + CodelistId = 100000010, Value = "ID GeoDIN value", }, new BoreholeCodelist { BoreholeId = firstBoreholeId, - CodelistId = codelistKernlager.Id, - Codelist = codelistKernlager, + CodelistId = 100000011, Value = "ID Kernlager value", }, }, @@ -157,22 +144,20 @@ public async Task DownloadCsvWithCustomIds() new BoreholeCodelist { BoreholeId = secondBoreholeId, - CodelistId = codelistGeoDIN.Id, - Codelist = codelistGeoDIN, + CodelistId = 100000010, Value = "ID GeoDIN value", }, new BoreholeCodelist { BoreholeId = secondBoreholeId, - CodelistId = codelistTopFels.Id, - Codelist = codelistTopFels, + CodelistId = 100000009, Value = "ID TopFels value", }, }, }; - await boreholeController.CreateAsync(boreholeWithCustomIds).ConfigureAwait(false); - await boreholeController.CreateAsync(boreholeWithOtherCustomIds).ConfigureAwait(false); + context.Boreholes.AddRange(boreholeWithCustomIds, boreholeWithOtherCustomIds); + await context.SaveChangesAsync(); var ids = new List { firstBoreholeId, secondBoreholeId }; From 93044984cff91d365f4bcd89cc6e28dddd6747e1 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 12:58:59 +0100 Subject: [PATCH 06/38] Fix csv indexes --- src/client/cypress/e2e/mainPage/export.cy.js | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index 06b9bec38..693da6e56 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -34,12 +34,12 @@ const verifyTVDContentInCSVFile = ( cy.readFile(prepareDownloadPath(fileName)).then(fileContent => { const { lines, rows } = splitFileContent(fileContent); expect(lines.length).to.equal(3); - expect(rows[0][24]).to.equal("TotalDepthTvd"); - expect(rows[1][24]).to.equal(expectedTotalDepthVD); - expect(rows[0][25]).to.equal("TopBedrockFreshTvd"); - expect(rows[1][25]).to.equal(expectedTopBedrockFreshTVD); - expect(rows[0][26]).to.equal("TopBedrockWeatheredTvd"); - expect(rows[1][26]).to.equal(expectedTopBedrockWeatheredTVD); + expect(rows[0][26]).to.equal("TotalDepthTvd"); + expect(rows[1][26]).to.equal(expectedTotalDepthVD); + expect(rows[0][27]).to.equal("TopBedrockFreshTvd"); + expect(rows[1][27]).to.equal(expectedTopBedrockFreshTVD); + expect(rows[0][28]).to.equal("TopBedrockWeatheredTvd"); + expect(rows[1][28]).to.equal(expectedTopBedrockWeatheredTVD); }); }; @@ -170,13 +170,13 @@ describe("Test for exporting boreholes.", () => { expect(rows[1][3]).to.equal(firstBoreholeName); expect(rows[2][3]).to.equal(secondBoreholeName); - expect(rows[0][31]).to.equal("IDInfoGeol"); - expect(rows[1][31]).to.equal(""); - expect(rows[2][31]).to.equal("14"); + expect(rows[0][33]).to.equal("IDInfoGeol"); + expect(rows[1][33]).to.equal(""); + expect(rows[2][33]).to.equal("14"); - expect(rows[0][32]).to.equal("IDGeODin\r"); - expect(rows[1][32]).to.equal("13\r"); - expect(rows[2][32]).to.equal("\r"); + expect(rows[0][34]).to.equal("IDGeODin\r"); + expect(rows[1][34]).to.equal("13\r"); + expect(rows[2][34]).to.equal("\r"); }); deleteItem(); handlePrompt("Do you really want to delete these 2 boreholes? This cannot be undone.", "Delete"); From 452b9390aaa3b77556e5a2671180c1d9d3f4b281 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 13:32:22 +0100 Subject: [PATCH 07/38] Fix some sonar cloud issues --- src/api/Controllers/ExportController.cs | 6 +- src/api/Controllers/ImportController.cs | 2 +- tests/api/Controllers/ExportControllerTest.cs | 59 +++++++++---------- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 5a7046bb3..34588804e 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -1,12 +1,10 @@ using BDMS.Authentication; using BDMS.Models; using CsvHelper; -using CsvHelper.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Text; namespace BDMS.Controllers; @@ -19,12 +17,10 @@ public class ExportController : ControllerBase // This also applies to the number of filtered ids to ensure the URL length does not exceed the maximum allowed length. private const int MaxPageSize = 100; private readonly BdmsContext context; - private readonly ILogger logger; - public ExportController(BdmsContext context, ILogger logger) + public ExportController(BdmsContext context) { this.context = context; - this.logger = logger; } /// diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index 657ac00d2..ad234e7c3 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -340,7 +340,7 @@ internal static bool CompareValuesWithTolerance(double? firstValue, double? seco return Math.Abs(firstValue.Value - secondValue.Value) <= tolerance; } - private List ReadBoreholesFromCsv(IFormFile file) + private static List ReadBoreholesFromCsv(IFormFile file) { using var reader = new StreamReader(file.OpenReadStream()); using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig); diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index 7cb2323c0..e65093225 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -1,13 +1,9 @@ using BDMS.Models; using CsvHelper; -using CsvHelper.Configuration; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using System.Globalization; using System.Text; using static BDMS.Helpers; @@ -17,9 +13,10 @@ namespace BDMS.Controllers; [TestClass] public class ExportControllerTest { + private const string TestCsvString = "text/csv"; + private const string ExportFileName = "boreholes_export.csv"; private BdmsContext context; private ExportController controller; - private Mock> loggerMock; private static int testBoreholeId = 1000068; [TestInitialize] @@ -28,8 +25,7 @@ public void TestInitialize() var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.Development.json").Build(); context = ContextFactory.GetTestContext(); - loggerMock = new Mock>(); - controller = new ExportController(context, loggerMock.Object) { ControllerContext = GetControllerContextAdmin() }; + controller = new ExportController(context) { ControllerContext = GetControllerContextAdmin() }; } [TestMethod] @@ -40,8 +36,8 @@ public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes() var result = await controller.ExportCsvAsync(ids) as FileContentResult; Assert.IsNotNull(result); - Assert.AreEqual("text/csv", result.ContentType); - Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + Assert.AreEqual(TestCsvString, result.ContentType); + Assert.AreEqual(ExportFileName, result.FileDownloadName); var csvData = Encoding.UTF8.GetString(result.FileContents); var fileLength = csvData.Split('\n').Length; var recordCount = fileLength - 2; // Remove header and last line break @@ -54,20 +50,20 @@ public async Task DownloadCsvReturnsTVD() var boreholeQuery = context.Boreholes .Include(b => b.BoreholeGeometry); - var boreholeIdsWithoutGeometry = boreholeQuery + var boreholeIdsWithoutGeometry = await boreholeQuery .Where(b => b.BoreholeGeometry.Count < 2) - .Take(3).Select(b => b.Id); + .Take(3).Select(b => b.Id).ToListAsync(); - var boreholeIdsWithGeometry = boreholeQuery + var boreholeIdsWithGeometry = await boreholeQuery .Where(b => b.BoreholeGeometry.Count > 1) - .Take(3).Select(b => b.Id); + .Take(3).Select(b => b.Id).ToListAsync(); - var boreholeIds = await boreholeIdsWithoutGeometry.Concat(boreholeIdsWithGeometry).ToListAsync(); + var boreholeIds = boreholeIdsWithoutGeometry.Concat(boreholeIdsWithGeometry); var result = await controller.ExportCsvAsync(boreholeIds) as FileContentResult; Assert.IsNotNull(result); - Assert.AreEqual("text/csv", result.ContentType); - Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + Assert.AreEqual(TestCsvString, result.ContentType); + Assert.AreEqual(ExportFileName, result.FileDownloadName); var records = GetRecordsFromFileContent(result); foreach (var record in records) @@ -79,14 +75,14 @@ public async Task DownloadCsvReturnsTVD() var topBedrockWeatheredTvd = record.TopBedrockWeatheredTvd; var topBedrockWeatheredMd = record.TopBedrockWeatheredMd; - if (boreholeIdsWithoutGeometry.Select(b => b.ToString()).ToList().Contains(record.Id)) + if (boreholeIdsWithoutGeometry.Contains(int.Parse(record.Id))) { Assert.AreEqual(totalDepthMd, totalDepthTvd); Assert.AreEqual(topBedrockFreshMd, topBedrockFreshTvd); Assert.AreEqual(topBedrockWeatheredMd, topBedrockWeatheredTvd); } - if (boreholeIdsWithGeometry.Select(b => b.ToString()).ToList().Contains(record.Id)) + if (boreholeIdsWithGeometry.Contains(int.Parse(record.Id))) { Assert.AreNotEqual(totalDepthMd, totalDepthTvd); Assert.AreNotEqual(topBedrockFreshMd, topBedrockFreshTvd); @@ -114,6 +110,9 @@ public async Task DownloadCsvReturnsTVD() [TestMethod] public async Task DownloadCsvWithCustomIds() { + string idGeoDinValue = "ID GeoDIN value"; + string idTopFelsValue = "ID TopFels value"; + string idKernlagerValue = "ID Kernlager value"; var firstBoreholeId = 1_009_068; var boreholeWithCustomIds = new Borehole { @@ -124,13 +123,13 @@ public async Task DownloadCsvWithCustomIds() { BoreholeId = firstBoreholeId, CodelistId = 100000010, - Value = "ID GeoDIN value", + Value = idGeoDinValue, }, new BoreholeCodelist { BoreholeId = firstBoreholeId, CodelistId = 100000011, - Value = "ID Kernlager value", + Value = idKernlagerValue, }, }, }; @@ -145,13 +144,13 @@ public async Task DownloadCsvWithCustomIds() { BoreholeId = secondBoreholeId, CodelistId = 100000010, - Value = "ID GeoDIN value", + Value = idGeoDinValue, }, new BoreholeCodelist { BoreholeId = secondBoreholeId, CodelistId = 100000009, - Value = "ID TopFels value", + Value = idTopFelsValue, }, }, }; @@ -163,22 +162,22 @@ public async Task DownloadCsvWithCustomIds() var result = await controller.ExportCsvAsync(ids) as FileContentResult; Assert.IsNotNull(result); - Assert.AreEqual("text/csv", result.ContentType); - Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + Assert.AreEqual(TestCsvString, result.ContentType); + Assert.AreEqual(ExportFileName, result.FileDownloadName); var records = GetRecordsFromFileContent(result); var firstBorehole = records.Find(r => r.Id == firstBoreholeId.ToString()); Assert.IsNotNull(firstBorehole); - Assert.AreEqual("ID GeoDIN value", firstBorehole.IDGeODin); - Assert.AreEqual("ID Kernlager value", firstBorehole.IDKernlager); + Assert.AreEqual(idGeoDinValue, firstBorehole.IDGeODin); + Assert.AreEqual(idKernlagerValue, firstBorehole.IDKernlager); Assert.AreEqual("", firstBorehole.IDTopFels); var secondBorehole = records.Find(r => r.Id == secondBoreholeId.ToString()); Assert.IsNotNull(secondBorehole); - Assert.AreEqual("ID GeoDIN value", secondBorehole.IDGeODin); + Assert.AreEqual(idGeoDinValue, secondBorehole.IDGeODin); Assert.AreEqual("", secondBorehole.IDKernlager); - Assert.AreEqual("ID TopFels value", secondBorehole.IDTopFels); + Assert.AreEqual(idTopFelsValue, secondBorehole.IDTopFels); } [TestMethod] @@ -201,8 +200,8 @@ public async Task DownloadCsvWithPartiallyValidIdsReturnsFileForPartillyValidIds Assert.IsNotNull(result); Assert.IsNotNull(result); - Assert.AreEqual("text/csv", result.ContentType); - Assert.AreEqual("boreholes_export.csv", result.FileDownloadName); + Assert.AreEqual(TestCsvString, result.ContentType); + Assert.AreEqual(ExportFileName, result.FileDownloadName); var csvData = Encoding.UTF8.GetString(result.FileContents); var fileLength = csvData.Split('\n').Length; var recordCount = fileLength - 2; From 3b770680a67a5508ce159ee8b44f7023a64baeaa Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 13:57:46 +0100 Subject: [PATCH 08/38] Reuse csv config --- src/api/Controllers/ImportController.cs | 8 +------- tests/api/Controllers/ExportControllerTest.cs | 2 -- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index ad234e7c3..1c8ebaf2c 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -385,13 +385,7 @@ private sealed class CsvImportBoreholeMap : ClassMap public CsvImportBoreholeMap() { - var config = new CsvConfiguration(swissCulture) - { - IgnoreReferences = true, - PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title), - }; - - AutoMap(config); + AutoMap(CsvConfigHelper.CsvReadConfig); // Define all optional properties of Borehole (ef navigation properties do not need to be defined as optional). Map(m => m.CreatedById).Optional(); diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index e65093225..a18d32d41 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -22,8 +22,6 @@ public class ExportControllerTest [TestInitialize] public void TestInitialize() { - var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.Development.json").Build(); - context = ContextFactory.GetTestContext(); controller = new ExportController(context) { ControllerContext = GetControllerContextAdmin() }; } From f2f9c74d1c595978b9d5615b7477919c02d89657 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 14:02:38 +0100 Subject: [PATCH 09/38] Remove unused fields --- src/api/Controllers/ImportController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index 1c8ebaf2c..e836d828a 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -381,8 +381,6 @@ private void AddValidationErrorToModelState(int boreholeIndex, string errorMessa private sealed class CsvImportBoreholeMap : ClassMap { - private readonly CultureInfo swissCulture = new("de-CH"); - public CsvImportBoreholeMap() { AutoMap(CsvConfigHelper.CsvReadConfig); From 6850af38155ce7c586acee82a6d9257f9d4bf0c7 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 14:21:37 +0100 Subject: [PATCH 10/38] Add missing imports --- src/client/cypress/e2e/mainPage/export.cy.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index a57849b77..0657eb676 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -1,4 +1,11 @@ -import { deleteItem, exportCSVItem, exportItem, exportJsonItem } from "../helpers/buttonHelpers"; +import { + addItem, + deleteItem, + exportCSVItem, + exportItem, + exportJsonItem, + saveWithSaveBar, +} from "../helpers/buttonHelpers"; import { checkAllVisibleRows, checkRowWithText, showTableAndWaitForData } from "../helpers/dataGridHelpers.js"; import { evaluateInput, setInput, setSelect } from "../helpers/formHelpers"; import { From e295724a2d6a19c37e24102b08d58af84340472a Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 15:20:08 +0100 Subject: [PATCH 11/38] Add export statements to test --- src/client/cypress/e2e/mainPage/export.cy.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index 0657eb676..f8d014b83 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -108,6 +108,7 @@ describe("Test for exporting boreholes.", () => { saveWithSaveBar(); stopBoreholeEditing(); + exportItem(); exportCSVItem(); verifyTVDContentInCSVFile(fileName, "700", "800", "900"); @@ -143,6 +144,7 @@ describe("Test for exporting boreholes.", () => { setInput("originalName", secondBoreholeName); // change name to avoid potential CSV filename conflict saveWithSaveBar(); stopBoreholeEditing(); + exportItem(); exportCSVItem(); verifyTVDContentInCSVFile(secondFileName, "674.8678208299723", "762.6098263945338", "846.9637100889873"); startBoreholeEditing(); @@ -172,6 +174,7 @@ describe("Test for exporting boreholes.", () => { showTableAndWaitForData(); checkRowWithText(firstBoreholeName); checkRowWithText(secondBoreholeName); + exportItem(); exportCSVItem(); cy.readFile(prepareDownloadPath(csvFileName)).then(fileContent => { const { lines, rows } = splitFileContent(fileContent); @@ -206,6 +209,7 @@ describe("Test for exporting boreholes.", () => { handlePrompt(moreThan100SelectedPrompt, "Cancel"); exportItem(); handlePrompt(moreThan100SelectedPrompt, "Export 100 boreholes"); + exportItem(); exportCSVItem(); cy.wait("@borehole_export_csv").its("response.statusCode").should("eq", 200); readDownloadedFile(csvFileName); From ec5d53449e51971087429494e3869acad4f1a908 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 15:48:38 +0100 Subject: [PATCH 12/38] Fix location fetch and map update --- .../detail/form/location/locationSegment.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/client/src/pages/detail/form/location/locationSegment.tsx b/src/client/src/pages/detail/form/location/locationSegment.tsx index f9a83cea5..d3895c239 100644 --- a/src/client/src/pages/detail/form/location/locationSegment.tsx +++ b/src/client/src/pages/detail/form/location/locationSegment.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { UseFormReturn } from "react-hook-form"; import { Card, Grid, Stack } from "@mui/material"; import { fetchApiV2 } from "../../../../api/fetchApiV2"; @@ -17,6 +17,8 @@ interface LocationSegmentProps extends LocationBaseProps { } const LocationSegment = ({ borehole, editingEnabled, labelingPanelOpen, formMethods }: LocationSegmentProps) => { + const [currentLV95X, setCurrentLV95X] = useState(borehole.locationX ? Number(borehole.locationX) : null); + const [currentLV95Y, setCurrentLV95Y] = useState(borehole.locationY ? Number(borehole.locationY) : null); const transformCoordinates = useCallback(async (referenceSystem: string, x: number, y: number) => { let apiUrl; if (referenceSystem === referenceSystems.LV95.name) { @@ -70,12 +72,18 @@ const LocationSegment = ({ borehole, editingEnabled, labelingPanelOpen, formMeth if (!response) return; // Ensure response is valid const maxPrecision = Math.max(XPrecision, YPrecision); - const transformedX = parseFloat(response.easting).toFixed(maxPrecision); - const transformedY = parseFloat(response.northing).toFixed(maxPrecision); + const transformedX = parseFloat(response.easting); + const transformedY = parseFloat(response.northing); - const location = await fetchApiV2(`location/identify?east=${X}&north=${Y}`, "GET"); + const XLV95 = sourceSystem === ReferenceSystemKey.LV95 ? X : transformedX; + const YLV95 = sourceSystem === ReferenceSystemKey.LV95 ? Y : transformedY; + + setCurrentLV95X(XLV95); + setCurrentLV95Y(YLV95); + + const location = await fetchApiV2(`location/identify?east=${XLV95}&north=${YLV95}`, "GET"); setValuesForCountryCantonMunicipality(location); - setValuesForReferenceSystem(targetSystem, transformedX, transformedY); + setValuesForReferenceSystem(targetSystem, transformedX.toFixed(maxPrecision), transformedY.toFixed(maxPrecision)); }, [setValuesForCountryCantonMunicipality, setValuesForReferenceSystem, transformCoordinates], ); @@ -130,8 +138,8 @@ const LocationSegment = ({ borehole, editingEnabled, labelingPanelOpen, formMeth }} id={borehole.id} isEditable={editingEnabled} - x={borehole.locationX ? Number(borehole.locationX) : null} - y={borehole.locationY ? Number(borehole.locationY) : null} + x={currentLV95X} + y={currentLV95Y} /> From 2300938b49f3e3bcb3feb8e1b7f5b8e0f7b5bcf8 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 12:25:30 +0100 Subject: [PATCH 13/38] Remove attachments from csv import --- .../src/pages/overview/layout/mainSideNav.tsx | 3 -- .../commons/actionsInterfaces.ts | 3 -- .../sidePanelContent/importer/importModal.tsx | 14 +------ .../importer/importModalContent.tsx | 38 +------------------ 4 files changed, 3 insertions(+), 55 deletions(-) diff --git a/src/client/src/pages/overview/layout/mainSideNav.tsx b/src/client/src/pages/overview/layout/mainSideNav.tsx index c76d43f85..b616998c2 100644 --- a/src/client/src/pages/overview/layout/mainSideNav.tsx +++ b/src/client/src/pages/overview/layout/mainSideNav.tsx @@ -45,7 +45,6 @@ const MainSideNav = ({ const [upload, setUpload] = useState(false); const [validationErrorModal, setValidationErrorModal] = useState(false); const [selectedFile, setSelectedFile] = useState(null); - const [selectedBoreholeAttachments, setSelectedBoreholeAttachments] = useState(null); const [errorsResponse, setErrorsResponse] = useState(null); const filterContext = useContext(FilterContext); @@ -177,10 +176,8 @@ const MainSideNav = ({ setSelectedFile={setSelectedFile} setWorkgroup={setWorkgroupId} enabledWorkgroups={enabledWorkgroups} - setSelectedBoreholeAttachments={setSelectedBoreholeAttachments} workgroup={workgroupId} selectedFile={selectedFile} - selectedBoreholeAttachments={selectedBoreholeAttachments} modal={modal} upload={upload} /> diff --git a/src/client/src/pages/overview/sidePanelContent/commons/actionsInterfaces.ts b/src/client/src/pages/overview/sidePanelContent/commons/actionsInterfaces.ts index 128731516..98d55d4c8 100644 --- a/src/client/src/pages/overview/sidePanelContent/commons/actionsInterfaces.ts +++ b/src/client/src/pages/overview/sidePanelContent/commons/actionsInterfaces.ts @@ -21,15 +21,12 @@ export interface NewBoreholeProps extends WorkgroupSelectProps { } export interface ImportContentProps { - setSelectedBoreholeAttachments: React.Dispatch>; - selectedFile: Blob[] | null; setSelectedFile: React.Dispatch>; } export interface ImportModalProps extends ImportContentProps { modal: boolean; creating: boolean; - selectedBoreholeAttachments: Blob[] | null; selectedFile: Blob[] | null; upload: boolean; workgroup: string; diff --git a/src/client/src/pages/overview/sidePanelContent/importer/importModal.tsx b/src/client/src/pages/overview/sidePanelContent/importer/importModal.tsx index db2f49592..47e45a57a 100644 --- a/src/client/src/pages/overview/sidePanelContent/importer/importModal.tsx +++ b/src/client/src/pages/overview/sidePanelContent/importer/importModal.tsx @@ -16,8 +16,6 @@ const ImportModal = ({ setErrorsResponse, setValidationErrorModal, selectedFile, - selectedBoreholeAttachments, - setSelectedBoreholeAttachments, setSelectedFile, modal, creating, @@ -35,12 +33,6 @@ const ImportModal = ({ selectedFile.forEach((boreholeFile: string | Blob) => { combinedFormData.append("boreholesFile", boreholeFile); }); - - if (selectedBoreholeAttachments !== null) { - selectedBoreholeAttachments.forEach((attachment: string | Blob) => { - combinedFormData.append("attachments", attachment); - }); - } } importBoreholes(workgroup, combinedFormData).then(response => { setCreating(false); @@ -97,11 +89,7 @@ const ImportModal = ({ - +

{capitalizeFirstLetter(t("workgroup"))}

diff --git a/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx b/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx index fd71fcb37..cdead8998 100644 --- a/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx +++ b/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx @@ -8,18 +8,6 @@ import Downloadlink from "../../../detail/attachments/downloadlink.jsx"; import { FileDropzone } from "../../../detail/attachments/fileDropzone.jsx"; import { ImportContentProps } from "../commons/actionsInterfaces.js"; -const SeparatorLine = () => { - return ( - - ); -}; - const ExampleHeadings = (headings: string) => { return ( { ); }; -const ImportModalContent = ({ setSelectedBoreholeAttachments, setSelectedFile, selectedFile }: ImportContentProps) => { +const ImportModalContent = ({ setSelectedFile }: ImportContentProps) => { const { t } = useTranslation(); - const handleBoreholeAttachmentChange = useCallback( - (attachmentsFromDropzone: Blob[]) => { - setSelectedBoreholeAttachments(attachmentsFromDropzone); - }, - [setSelectedBoreholeAttachments], - ); - const handleBoreholeFileChange = useCallback( (boreholeFileFromDropzone: Blob[]) => { setSelectedFile(boreholeFileFromDropzone); @@ -60,7 +41,6 @@ const ImportModalContent = ({ setSelectedBoreholeAttachments, setSelectedFile, s

- {SeparatorLine()}

{capitalizeFirstLetter(t("boreholes"))}

@@ -76,7 +56,7 @@ const ImportModalContent = ({ setSelectedBoreholeAttachments, setSelectedFile, s "status_id;remarks;total_depth;qt_depth_id;top_bedrock_fresh_md;" + "top_bedrock_weathered_md;" + "has_groundwater;lithology_top_bedrock_id;" + - "chronostratigraphy_id;lithostratigraphy_id;attachments;", + "chronostratigraphy_id;lithostratigraphy_id;", )} -

{capitalizeFirstLetter(t("attachments"))}

- - {t("importBoreholeAttachment")} - - - {SeparatorLine()} ); }; From 3fe59c7e0467c913b538e9a1fea00c79cee371f7 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 13:29:39 +0100 Subject: [PATCH 14/38] Adapt example headings --- .../importer/importModalContent.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx b/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx index cdead8998..98430ad7e 100644 --- a/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx +++ b/src/client/src/pages/overview/sidePanelContent/importer/importModalContent.tsx @@ -46,17 +46,17 @@ const ImportModalContent = ({ setSelectedFile }: ImportContentProps) => { {t("csvFormatExplanation")} {ExampleHeadings( - "id_geodin_shortname;id_info_geol;id_original;" + - "id_canton;id_geo_quat;id_geo_mol;id_geo_therm;id_top_fels;" + - "id_geodin;id_kernlager;original_name;project_name;name;" + - "restriction_id;restriction_until;national_interest;location_x;location_y;" + - "location_precision;elevation_z;elevation_precision_id;" + - "reference_elevation;reference_elevation_type_id;" + - "qt_reference_elevation_id;hrs_id;type_id;purpose_id;" + - "status_id;remarks;total_depth;qt_depth_id;top_bedrock_fresh_md;" + - "top_bedrock_weathered_md;" + - "has_groundwater;lithology_top_bedrock_id;" + - "chronostratigraphy_id;lithostratigraphy_id;", + "IdOriginal;" + + "IdCanton;IdGeoQuat;IdGeoMol;IdGeoTherm;IdTopFels;" + + "IdGeodin;IdKernlager;OriginalName;ProjectName;Name;" + + "RestrictionId;RestrictionUntil;NationalInterest;LocationX;LocationY;" + + "LocationPrecision;ElevationZ;ElevationPrecisionId;" + + "ReferenceElevation;ReferenceElevationTypeId;" + + "QtReferenceElevationId;HrsId;TypeId;PurposeId;" + + "StatusId;Remarks;TotalDepth;QtDepthId;TopBedrockFreshMd;" + + "TopBedrockWeatheredMd;" + + "HasGroundwater;LithologyTopBedrockId;" + + "ChronostratigraphyId;LithostratigraphyId;", )} Date: Tue, 17 Dec 2024 14:33:39 +0100 Subject: [PATCH 15/38] Fix custom id import --- src/api/Controllers/ImportController.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index e836d828a..d6522b620 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -440,16 +440,16 @@ public CsvImportBoreholeMap() var boreholeCodeLists = new List(); new List<(string Name, int CodeListId)> { - ("id_geodin_shortname", 100000000), - ("id_info_geol", 100000003), - ("id_original", 100000004), - ("id_canton", 100000005), - ("id_geo_quat", 100000006), - ("id_geo_mol", 100000007), - ("id_geo_therm", 100000008), - ("id_top_fels", 100000009), - ("id_geodin", 100000010), - ("id_kernlager", 100000011), + ("IDGeODin-Shortname", 100000000), + ("IDInfoGeol", 100000003), + ("IDOriginal", 100000004), + ("IDCanton", 100000005), + ("IDGeoQuat", 100000006), + ("IDGeoMol", 100000007), + ("IDGeoTherm", 100000008), + ("IDTopFels", 100000009), + ("IDGeODin", 100000010), + ("IDKernlager", 100000011), }.ForEach(id => { if (args.Row.HeaderRecord != null && args.Row.HeaderRecord.Any(h => h == id.Name)) From f006af1c8096d4467fbe09b890cc86a2ba3e0676 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 14:44:18 +0100 Subject: [PATCH 16/38] Update help --- src/client/docs/import.md | 94 +++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/src/client/docs/import.md b/src/client/docs/import.md index 5db6da5bc..450a83119 100644 --- a/src/client/docs/import.md +++ b/src/client/docs/import.md @@ -18,11 +18,7 @@ Zunächst sollte die CSV-Datei den Anforderungen und dem Format entsprechen, wie 1. Schaltfläche _Datei auswählen_ anklicken und die vorbereitete CSV-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: Bohrlochanhänge selektieren (optional) - -1. Schaltfläche _Dateien hier ablegen oder klicken, um sie hochzuladen_ anklicken und die vorbereitete Datei(en) auswählen. - -### Schritt 5: Dateien hochladen +### 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. @@ -37,57 +33,50 @@ Die CSV-Datei muss den folgenden Anforderungen und dem Format entsprechen, damit - Die Spaltenüberschriften müssen den vorgegebenen Feldnamen aus dem Import-Dialog entsprechen. - Die Werte in den Spalten müssen den erwarteten Datentypen entsprechen (z.B. numerisch für Tiefe, Text für Namen, etc.). -## Format und Anforderungen an die Dateien der Bohrlochanhänge - -Die Anhangsdatei muss den folgenden Anforderungen entsprechen, damit sie erfolgreich in die Webapplikation importiert werden kann: - -- Die Datei darf maximal 200 MB gross sein. -- Der Dateiname darf keine Leerzeichen enthalten. ## Bohrloch Datei 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. -| Feldname | Datentyp | Pflichtfeld | Beschreibung | -| --------------------------- | -------------- | ----------- | ------------------------------------------------------------------------------------- | -| id_geodin_shortname | Zahl | Nein | ID GeODin-Shortname | -| id_info_geol | Zahl | Nein | ID InfoGeol | -| id_original | Zahl | Nein | ID Original | -| id_canton | Zahl | Nein | ID Kanton | -| id_geo_quat | Zahl | Nein | ID GeoQuat | -| id_geo_mol | Zahl | Nein | ID GeoMol | -| id_geo_therm | Zahl | Nein | ID GeoTherm | -| id_top_fels | Zahl | Nein | ID TopFels | -| id_geodin | Zahl | Nein | ID GeODin | -| id_kernlager | Zahl | Nein | ID Kernlager | -| original_name | Text | Ja | Originalname | -| project_name | Text | Nein | Projektname | -| name | Text | Nein | Name | -| restriction_id | ID (Codeliste) | Nein | Beschränkung | -| restriction_until | Datum | Nein | Ablaufdatum der Beschränkung | -| national_interest | True/False | Nein | Nationales Interesse | -| location_x | Dezimalzahl | Ja | Koordinate Ost LV95 | -| location_y | Dezimalzahl | Ja | Koordinate Nord LV95 | -| location_precision_id | ID (Codeliste) | Nein | +/- Koordinaten [m] | -| elevation_z | Dezimalzahl | Nein | Terrainhöhe [m ü.M.] | -| elevation_precision_id | ID (Codeliste) | Nein | +/- Terrainhöhe [m] | -| reference_elevation | Dezimalzahl | Nein | Referenz Ansatzhöhe [m ü.M.] | -| reference_elevation_type_id | ID (Codeliste) | Nein | Typ der Referenz Ansatzhöhe | -| qt_reference_elevation_id | ID (Codeliste) | Nein | +/- Referenz Ansatzhöhe [m] | -| hrs_id | ID (Codeliste) | Nein | Höhenreferenzsystem | -| type_id | ID (Codeliste) | Nein | Bohrtyp | -| purpose_id | ID (Codeliste) | Nein | Bohrzweck | -| status_id | ID (Codeliste) | Nein | Bohrungsstatus | -| remarks | Text | Nein | Bemerkungen | -| total_depth | Dezimalzahl | Nein | Bohrlochlänge [m MD] | -| qt_depth_id | ID (Codeliste) | Nein | +/- Bohrlochlänge [m MD] | -| top_bedrock_fresh_md | Dezimalzahl | Nein | Top Fels (frisch) [m MD] | -| top_bedrock_weathered_md | Dezimalzahl | Nein | Top Fels (verwittert) [m MD] | -| has_groundwater | True/False | Nein | Grundwasser | -| lithology_top_bedrock_id | ID (Codeliste) | Nein | Lithologie Top Fels | -| chronostratigraphy_id | ID (Codeliste) | Nein | Chronostratigraphie Top Fels | -| lithostratigraphy_id | ID (Codeliste) | Nein | Lithostratigraphie Top Fels | -| attachments | Text | Nein | Kommaseparierte Dateinamen der Anhänge mit Dateiendung z.B. anhang_1.pdf,anhang_2.zip | +| Feldname | Datentyp | Pflichtfeld | Beschreibung | +| --------------------------- | -------------- | ----------- | ------------------------------------------------------------------------------------- | +| IdGeodinShortname | Zahl | Nein | ID GeODin-Shortname | +| IdInfoGeol | Zahl | Nein | ID InfoGeol | +| IdOriginal | Zahl | Nein | ID Original | +| IdCanton | Zahl | Nein | ID Kanton | +| IdGeoQuat | Zahl | Nein | ID GeoQuat | +| IdGeoMol | Zahl | Nein | ID GeoMol | +| IdGeoTherm | Zahl | Nein | ID GeoTherm | +| IdTopFels | Zahl | Nein | ID TopFels | +| IdGeodin | Zahl | Nein | ID GeODin | +| IdKernlager | Zahl | Nein | ID Kernlager | +| OriginalName | Text | Ja | Originalname | +| ProjectName | Text | Nein | Projektname | +| Name | Text | Nein | Name | +| RestrictionId | ID (Codeliste) | Nein | Beschränkung | +| RestrictionUntil | Datum | Nein | Ablaufdatum der Beschränkung | +| NationalInterest | True/False | Nein | Nationales Interesse | +| LocationX | Dezimalzahl | Ja | Koordinate Ost LV95 | +| LocationY | Dezimalzahl | Ja | Koordinate Nord LV95 | +| LocationPrecisionId | ID (Codeliste) | Nein | +/- Koordinaten [m] | +| ElevationZ | Dezimalzahl | Nein | Terrainhöhe [m ü.M.] | +| ElevationPrecisionId | ID (Codeliste) | Nein | +/- Terrainhöhe [m] | +| ReferenceElevation | Dezimalzahl | Nein | Referenz Ansatzhöhe [m ü.M.] | +| ReferenceElevationTypeId | ID (Codeliste) | Nein | Typ der Referenz Ansatzhöhe | +| ReferenceElevationPrecisionId | ID (Codeliste) | Nein | +/- Referenz Ansatzhöhe [m] | +| HrsId | ID (Codeliste) | Nein | Höhenreferenzsystem | +| TypeId | ID (Codeliste) | Nein | Bohrtyp | +| PurposeId | ID (Codeliste) | Nein | Bohrzweck | +| StatusId | ID (Codeliste) | Nein | Bohrungsstatus | +| Remarks | Text | Nein | Bemerkungen | +| TotalDepth | Dezimalzahl | Nein | Bohrlochlänge [m MD] | +| DepthPrecisionId | ID (Codeliste) | Nein | +/- Bohrlochlänge [m MD] | +| TopBedrockFreshMd | Dezimalzahl | Nein | Top Fels (frisch) [m MD] | +| TopBedrockWeatheredMd | Dezimalzahl | Nein | Top Fels (verwittert) [m MD] | +| HasGroundwater | True/False | Nein | Grundwasser | +| LithologyTopBedrockId | ID (Codeliste) | Nein | Lithologie Top Fels | +| ChronostratigraphyTopBedrockId| ID (Codeliste) | Nein | Chronostratigraphie Top Fels | +| LithostratigraphyTopBedrockId | ID (Codeliste) | Nein | Lithostratigraphie Top Fels | ## Validierung @@ -100,9 +89,6 @@ Für jeden bereitgestellten Header CSV-Datei muss für jede Zeile ein entspreche 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. -### Bohrlochanhänge - -Überprüft wird, ob jeder Dateiname in der kommaseparierten Liste in dem _attachments_-Feld in der Liste der Anhänge vorhanden ist. ## Generelles From 5efda6fbb289c1fc2f21157b79e3b3287d486712 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 16:09:59 +0100 Subject: [PATCH 17/38] Improve ExportController --- src/api/Controllers/ExportController.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 34588804e..9d70f90cf 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -36,8 +36,7 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz if (idList.Count < 1) return BadRequest("The list of IDs must not be empty."); var boreholes = await context.Boreholes - .Include(b => b.BoreholeCodelists) - .ThenInclude(bc => bc.Codelist) + .Include(b => b.BoreholeCodelists).ThenInclude(bc => bc.Codelist) .Where(borehole => idList.Contains(borehole.Id)) .OrderBy(b => idList.IndexOf(b.Id)) .ToListAsync() @@ -85,7 +84,7 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz // Write dynamic headers for each distinct custom Id var customIdHeaders = boreholes - .SelectMany(b => b.BoreholeCodelists ?? Enumerable.Empty()) + .SelectMany(b => GetBoreholeCodelists(b)) .Select(bc => new { bc.CodelistId, bc.Codelist?.En }) .Distinct() .OrderBy(x => x.CodelistId) @@ -149,7 +148,7 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz // Write dynamic fields for custom Ids foreach (var header in customIdHeaders) { - var codelistValue = (b.BoreholeCodelists ?? Enumerable.Empty()).FirstOrDefault(bc => bc.CodelistId == header.CodelistId)?.Value; + var codelistValue = GetBoreholeCodelists(b).FirstOrDefault(bc => bc.CodelistId == header.CodelistId)?.Value; csvWriter.WriteField(codelistValue ?? ""); } @@ -160,4 +159,9 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz await csvWriter.FlushAsync().ConfigureAwait(false); return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv"); } + + private IEnumerable GetBoreholeCodelists(Borehole borehole) + { + return borehole.BoreholeCodelists ?? Enumerable.Empty(); + } } From ff824f27268f545507077887dc2282a104f7170f Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 16:34:31 +0100 Subject: [PATCH 18/38] Make GetBoreholeCodelists static --- src/api/Controllers/ExportController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 9d70f90cf..386eeb19a 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -160,7 +160,7 @@ public async Task ExportCsvAsync([FromQuery][MaxLength(MaxPageSiz return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv"); } - private IEnumerable GetBoreholeCodelists(Borehole borehole) + private static IEnumerable GetBoreholeCodelists(Borehole borehole) { return borehole.BoreholeCodelists ?? Enumerable.Empty(); } From ccd5dee93acec5ac8c775820b6a81c0050a0d035 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 16:50:26 +0100 Subject: [PATCH 19/38] Add cypress test --- .../cypress/e2e/detailPage/coordinates.cy.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/client/cypress/e2e/detailPage/coordinates.cy.js b/src/client/cypress/e2e/detailPage/coordinates.cy.js index dfc949d1a..0eb51e565 100644 --- a/src/client/cypress/e2e/detailPage/coordinates.cy.js +++ b/src/client/cypress/e2e/detailPage/coordinates.cy.js @@ -196,4 +196,44 @@ describe("Tests for editing coordinates of a borehole.", () => { checkDecimalPlaces("@LV03X-input", 5); checkDecimalPlaces("@LV03Y-input", 5); }); + + it("updates canton and municipality when changing coordinates", () => { + // Type coordinates for Samaden in LV95 + cy.get("@LV95X-input").type("2789000"); + cy.get("@LV95Y-input").type("1155000"); + cy.wait("@location"); + cy.wait(4000); + + cy.get("@country").should("have.value", "Schweiz"); + cy.get("@canton").should("have.value", "Graubünden"); + cy.get("@municipality").should("have.value", "Samaden"); + + // Type coordinates for Unterentfelden in LV95 + cy.get("@LV95X-input").clear().type("2646000"); + cy.get("@LV95Y-input").clear().type("1247000"); + + cy.get("@country").should("have.value", "Schweiz"); + cy.get("@canton").should("have.value", "Aargau"); + cy.get("@municipality").should("have.value", "Unterentfelden"); + + // switch reference system to LV03 + setSelect("originalReferenceSystem", 1); + handlePrompt("Changing the coordinate system will reset the coordinates. Do you want to continue?", "Confirm"); + + // Type coordinates for Samaden in LV03 + cy.get("@LV03X-input").clear().type("789000"); + cy.get("@LV03Y-input").clear().type("155000"); + + cy.get("@country").should("have.value", "Schweiz"); + cy.get("@canton").should("have.value", "Graubünden"); + cy.get("@municipality").should("have.value", "Samaden"); + + // Type coordinates for Unterentfelden in LV03 + cy.get("@LV03X-input").clear().type("646000"); + cy.get("@LV03Y-input").clear().type("247000"); + + cy.get("@country").should("have.value", "Schweiz"); + cy.get("@canton").should("have.value", "Aargau"); + cy.get("@municipality").should("have.value", "Unterentfelden"); + }); }); From 55cc71ca7144b14394975662f0f2fb595f7ca63c Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 17:35:31 +0100 Subject: [PATCH 20/38] Add cypress test --- .../cypress/e2e/detailPage/boreholeform.cy.js | 27 +---- src/client/cypress/e2e/mainPage/export.cy.js | 100 +++++++++++++++++- 2 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/client/cypress/e2e/detailPage/boreholeform.cy.js b/src/client/cypress/e2e/detailPage/boreholeform.cy.js index 3bb360f74..708d59996 100644 --- a/src/client/cypress/e2e/detailPage/boreholeform.cy.js +++ b/src/client/cypress/e2e/detailPage/boreholeform.cy.js @@ -1,13 +1,11 @@ -import { exportCSVItem, exportItem, exportJsonItem, saveWithSaveBar } from "../helpers/buttonHelpers"; +import { saveWithSaveBar } from "../helpers/buttonHelpers"; import { clickOnRowWithText, showTableAndWaitForData, sortBy } from "../helpers/dataGridHelpers"; import { evaluateInput, evaluateSelect, isDisabled, setInput, setSelect } from "../helpers/formHelpers"; import { createBorehole, - deleteDownloadedFile, goToRouteAndAcceptTerms, handlePrompt, newEditableBorehole, - readDownloadedFile, returnToOverview, startBoreholeEditing, } from "../helpers/testHelpers"; @@ -234,27 +232,4 @@ describe("Test for the borehole form.", () => { ensureEditingEnabled(); }); }); - - it("Exports a borehole as csv and json", () => { - const boreholeName = "AAA_HIPPOPOTHAMUS"; - createBorehole({ - "extended.original_name": boreholeName, - "custom.alternate_name": boreholeName, - }).as("borehole_id"); - - deleteDownloadedFile(`${boreholeName}.json`); - deleteDownloadedFile(`${boreholeName}.csv`); - - cy.get("@borehole_id").then(id => { - goToRouteAndAcceptTerms(`/${id}`); - ensureEditingDisabled(); - exportItem(); - exportJsonItem(); - exportItem(); - exportCSVItem(); - }); - - readDownloadedFile(`${boreholeName}.json`); - readDownloadedFile(`${boreholeName}.csv`); - }); }); diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index f8d014b83..16fcdd0ee 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -6,7 +6,12 @@ import { exportJsonItem, saveWithSaveBar, } from "../helpers/buttonHelpers"; -import { checkAllVisibleRows, checkRowWithText, showTableAndWaitForData } from "../helpers/dataGridHelpers.js"; +import { + checkAllVisibleRows, + checkRowWithText, + clickOnRowWithText, + showTableAndWaitForData, +} from "../helpers/dataGridHelpers.js"; import { evaluateInput, setInput, setSelect } from "../helpers/formHelpers"; import { createBorehole, @@ -222,4 +227,97 @@ describe("Test for exporting boreholes.", () => { deleteDownloadedFile(jsonFileName); }); + + it("exports a single borehole as csv and json", () => { + const boreholeName = "AAA_HIPPOPOTHAMUS"; + createBorehole({ + "extended.original_name": boreholeName, + "custom.alternate_name": boreholeName, + }).as("borehole_id"); + + deleteDownloadedFile(`${boreholeName}.json`); + deleteDownloadedFile(`${boreholeName}.csv`); + + cy.get("@borehole_id").then(id => { + goToRouteAndAcceptTerms(`/${id}`); + getElementByDataCy("edit-button").should("exist"); + getElementByDataCy("editingstop-button").should("not.exist"); + exportItem(); + exportJsonItem(); + exportItem(); + exportCSVItem(); + }); + + readDownloadedFile(`${boreholeName}.json`); + readDownloadedFile(`${boreholeName}.csv`); + }); + + it("exports and reimports a borehole using csv", () => { + const boreholeName = "AAA_WALRUS"; + createBorehole({ + "extended.original_name": boreholeName, + "custom.alternate_name": boreholeName, + }).as("borehole_id"); + + cy.get("@borehole_id").then(id => { + goToRouteAndAcceptTerms(`/${id}`); + startBoreholeEditing(); + + // set two custom identifiers + addItem("addIdentifier"); + setSelect("boreholeCodelists.0.codelistId", 1); + setInput("boreholeCodelists.0.value", "w1"); + + addItem("addIdentifier"); + setSelect("boreholeCodelists.1.codelistId", 2); + setInput("boreholeCodelists.1.value", "w2"); + + // add coordinates + cy.get('[data-cy="locationX-formCoordinate"] input').type("2646000 "); + cy.get('[data-cy="locationY-formCoordinate"] input').type("1247000 "); + cy.wait("@location"); + cy.wait(4000); + saveWithSaveBar(); + + exportItem(); + exportCSVItem(); + + const downloadedFilePath = prepareDownloadPath(`${boreholeName}.csv`); + cy.readFile(downloadedFilePath).should("exist"); + + returnToOverview(); + showTableAndWaitForData(); + checkRowWithText(boreholeName); + deleteItem(); + handlePrompt("Do you really want to delete this borehole? This cannot be undone.", "Delete"); + getElementByDataCy("import-borehole-button").click(); + cy.contains(boreholeName).should("not.exist"); + + cy.readFile(downloadedFilePath, "utf-8").then(fileContent => { + // Create a DataTransfer and a File from the downloaded content + const boreholeFile = new DataTransfer(); + const file = new File([fileContent], "AAA_WALRUS.csv", { + type: "text/csv", + }); + boreholeFile.items.add(file); + + cy.get('[data-cy="import-boreholeFile-input"]').within(() => { + cy.get("input[type=file]", { force: true }).then(input => { + input[0].files = boreholeFile.files; // Attach the file + input[0].dispatchEvent(new Event("change", { bubbles: true })); + }); + }); + + cy.get('[data-cy="import-button"]').click(); + cy.wait("@borehole-upload"); + + clickOnRowWithText(boreholeName); + evaluateInput("name", boreholeName); + evaluateInput("boreholeCodelists.1.value", "w1"); + evaluateInput("boreholeCodelists.0.value", "w2"); + cy.get('[data-cy="locationX-formCoordinate"] input').should("have.value", `2'646'000`); + cy.get('[data-cy="locationY-formCoordinate"] input').should("have.value", `1'247'000`); + }); + }); + }); }); From 9a0ffd7afadcc9a62f9664d3eea50ecfc6ec093a Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 17:46:51 +0100 Subject: [PATCH 21/38] Add release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39691454c..5bb94608d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ - When copying a borehole, attachments won't be copied. - Removed csv lithology import. - Removed import id from csv import. +- Removed attachments from csv import. +- Updated recommended csv headers for borehole import to camel case e.g.`OriginalName` (snake case e.g.`original_name` is still supported). ### Fixed From 6b5af503ba49540e42e67e5508ab426aac16aefb Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 17:50:18 +0100 Subject: [PATCH 22/38] Improve nested code --- src/client/cypress/e2e/mainPage/export.cy.js | 97 ++++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index 16fcdd0ee..8721fe943 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -261,63 +261,62 @@ describe("Test for exporting boreholes.", () => { cy.get("@borehole_id").then(id => { goToRouteAndAcceptTerms(`/${id}`); - startBoreholeEditing(); - - // set two custom identifiers - addItem("addIdentifier"); - setSelect("boreholeCodelists.0.codelistId", 1); - setInput("boreholeCodelists.0.value", "w1"); + }); + startBoreholeEditing(); - addItem("addIdentifier"); - setSelect("boreholeCodelists.1.codelistId", 2); - setInput("boreholeCodelists.1.value", "w2"); + // set two custom identifiers + addItem("addIdentifier"); + setSelect("boreholeCodelists.0.codelistId", 1); + setInput("boreholeCodelists.0.value", "w1"); - // add coordinates - cy.get('[data-cy="locationX-formCoordinate"] input').type("2646000 "); - cy.get('[data-cy="locationY-formCoordinate"] input').type("1247000 "); - cy.wait("@location"); - cy.wait(4000); - saveWithSaveBar(); + addItem("addIdentifier"); + setSelect("boreholeCodelists.1.codelistId", 2); + setInput("boreholeCodelists.1.value", "w2"); + + // add coordinates + cy.get('[data-cy="locationX-formCoordinate"] input').type("2646000 "); + cy.get('[data-cy="locationY-formCoordinate"] input').type("1247000 "); + cy.wait("@location"); + cy.wait(4000); + saveWithSaveBar(); - exportItem(); - exportCSVItem(); + exportItem(); + exportCSVItem(); - const downloadedFilePath = prepareDownloadPath(`${boreholeName}.csv`); - cy.readFile(downloadedFilePath).should("exist"); - - returnToOverview(); - showTableAndWaitForData(); - checkRowWithText(boreholeName); - deleteItem(); - handlePrompt("Do you really want to delete this borehole? This cannot be undone.", "Delete"); - getElementByDataCy("import-borehole-button").click(); - cy.contains(boreholeName).should("not.exist"); - - cy.readFile(downloadedFilePath, "utf-8").then(fileContent => { - // Create a DataTransfer and a File from the downloaded content - const boreholeFile = new DataTransfer(); - const file = new File([fileContent], "AAA_WALRUS.csv", { - type: "text/csv", - }); - boreholeFile.items.add(file); + const downloadedFilePath = prepareDownloadPath(`${boreholeName}.csv`); + cy.readFile(downloadedFilePath).should("exist"); - cy.get('[data-cy="import-boreholeFile-input"]').within(() => { - cy.get("input[type=file]", { force: true }).then(input => { - input[0].files = boreholeFile.files; // Attach the file - input[0].dispatchEvent(new Event("change", { bubbles: true })); - }); - }); + returnToOverview(); + showTableAndWaitForData(); + checkRowWithText(boreholeName); + deleteItem(); + handlePrompt("Do you really want to delete this borehole? This cannot be undone.", "Delete"); + getElementByDataCy("import-borehole-button").click(); + cy.contains(boreholeName).should("not.exist"); - cy.get('[data-cy="import-button"]').click(); - cy.wait("@borehole-upload"); + cy.readFile(downloadedFilePath, "utf-8").then(fileContent => { + // Create a DataTransfer and a File from the downloaded content + const boreholeFile = new DataTransfer(); + const file = new File([fileContent], `${boreholeName}.csv`, { + type: "text/csv", + }); + boreholeFile.items.add(file); - clickOnRowWithText(boreholeName); - evaluateInput("name", boreholeName); - evaluateInput("boreholeCodelists.1.value", "w1"); - evaluateInput("boreholeCodelists.0.value", "w2"); - cy.get('[data-cy="locationX-formCoordinate"] input').should("have.value", `2'646'000`); - cy.get('[data-cy="locationY-formCoordinate"] input').should("have.value", `1'247'000`); + cy.get('[data-cy="import-boreholeFile-input"]').within(() => { + cy.get("input[type=file]", { force: true }).then(input => { + input[0].files = boreholeFile.files; // Attach the file + input[0].dispatchEvent(new Event("change", { bubbles: true })); + }); }); + cy.get('[data-cy="import-button"]').click(); + cy.wait("@borehole-upload"); }); + + clickOnRowWithText(boreholeName); + evaluateInput("name", boreholeName); + evaluateInput("boreholeCodelists.1.value", "w1"); + evaluateInput("boreholeCodelists.0.value", "w2"); + cy.get('[data-cy="locationX-formCoordinate"] input').should("have.value", `2'646'000`); + cy.get('[data-cy="locationY-formCoordinate"] input').should("have.value", `1'247'000`); }); }); From c244dccbdc508efed8dc3366c68674dd854b21b4 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 17:57:27 +0100 Subject: [PATCH 23/38] Update Testdata --- tests/api/TestData/testdata.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/TestData/testdata.csv b/tests/api/TestData/testdata.csv index b3a6918d3..c4b601e71 100644 --- a/tests/api/TestData/testdata.csv +++ b/tests/api/TestData/testdata.csv @@ -1,4 +1,4 @@ -id_geodin_shortname;id_info_geol;id_original;id_canton;id_geo_quat;id_geo_mol;id_geo_therm;id_top_fels;id_geodin;id_kernlager;original_name;project_name;name;date;restriction_id;restriction_until;original_reference_system;location_x;location_y;location_x_lv_03;location_y_lv_03;location_precision_id;elevation_z;elevation_precision_id;reference_elevation;reference_elevation_type_id;reference_elevation_precision_id;hrs_id;type_id;purpose_id;status_id;remarks;total_depth;depth_precision_id;top_bedrock_fresh_md;top_bedrock_weathered_md;has_groundwater;lithology_top_bedrock_id;chronostratigraphy_top_bedrock_id;lithostratigraphy_top_bedrock_id +IDGeODin-Shortname;IDInfoGeol;IDOriginal;IDCanton;IDGeoQuat;IDGeoMol;IDGeoTherm;IDTopFels;IDGeODin;IDKernlager;original_name;project_name;name;date;restriction_id;restriction_until;original_reference_system;location_x;location_y;location_x_lv_03;location_y_lv_03;location_precision_id;elevation_z;elevation_precision_id;reference_elevation;reference_elevation_type_id;reference_elevation_precision_id;hrs_id;type_id;purpose_id;status_id;remarks;total_depth;depth_precision_id;top_bedrock_fresh_md;top_bedrock_weathered_md;has_groundwater;lithology_top_bedrock_id;chronostratigraphy_top_bedrock_id;lithostratigraphy_top_bedrock_id Id_1;Id_2;;;;;Id_3;;;kernlager AETHERMAGIC;Unit_Test_1;Projekt 1 ;Unit_Test_1_a;2021-08-06 00:36:21.991827+00;20111002;;20104001;2618962;1144995;;;20113005;640.7726659;20114001;317.9010264;20117002;20114004;20106001;20101001;22103001;22104003;this product is top-notch.;4232.711946;22108003;398.8529283;656.2476436;TRUE;15104669;15001073;15300261 Id_4;;Id_5;Id_6;;;;;;;Unit_Test_2;Projekt 2;Unit_Test_2_a;2021-03-31 12:20:10.341393+00;;;20104001;2631690;1170516;;;20113002;3430.769638;20114005;2016.314814;20117005;20114004;20106001;20101001;22103001;22104008;This product works certainly well. It perfectly improves my tennis by a lot.;794.1547194;22108005;958.2378855;549.9801019;;15104670;15001009;15302009 ;;Id_7;Id_8;;;;Id_9;;;Unit_Test_3;Projekt 3;Unit_Test_3_a;;20111002;01.12.2023;20104001;2614834;1178661;;;20113005;1720.766609;20114003;1829.812475;20117005;20114002;20106001;20101001;;22104002;This is a really good product.;2429.747725;22108002;759.7574008;827.8441205;TRUE;15104671;15001007;15302339 From 323b5a2b51098159be7b7f683dfb2101efb7cbfd Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 17:57:38 +0100 Subject: [PATCH 24/38] Improve changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb94608d..bb005ff65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ - Removed csv lithology import. - Removed import id from csv import. - Removed attachments from csv import. -- Updated recommended csv headers for borehole import to camel case e.g.`OriginalName` (snake case e.g.`original_name` is still supported). +- Updated recommended csv headers for borehole import to camel case e.g.`OriginalName` (snake case e.g.`original_name` is still supported for all properties except for custom identifiers). ### Fixed From 55747d2d1b27320490fbd55f1e6b5dcd49f1cd80 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Tue, 17 Dec 2024 20:53:43 +0100 Subject: [PATCH 25/38] Fix import cypress test --- src/client/cypress/e2e/mainPage/import.cy.js | 21 ------------------- .../import/boreholes-multiple-valid.csv | 4 ++-- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/client/cypress/e2e/mainPage/import.cy.js b/src/client/cypress/e2e/mainPage/import.cy.js index b7a9e4920..4897a8b0f 100644 --- a/src/client/cypress/e2e/mainPage/import.cy.js +++ b/src/client/cypress/e2e/mainPage/import.cy.js @@ -21,27 +21,6 @@ describe("Test for importing boreholes.", () => { }); }); - // Select borehole attachments - let attachmentFileList = new DataTransfer(); - getImportFileFromFixtures("borehole_attachment_1.pdf", "utf-8").then(fileContent => { - const file = new File([fileContent], "borehole_attachment_1.pdf", { - type: "application/pdf", - }); - attachmentFileList.items.add(file); - }); - getImportFileFromFixtures("borehole_attachment_2.zip", "utf-8").then(fileContent => { - const file = new File([fileContent], "borehole_attachment_2.zip", { - type: "application/zip", - }); - attachmentFileList.items.add(file); - }); - cy.get('[data-cy="import-boreholeFile-attachments-input"]').within(() => { - cy.get("input[type=file]", { force: true }).then(input => { - input[0].files = attachmentFileList.files; - input[0].dispatchEvent(new Event("change", { bubbles: true })); - }); - }); - // Import boreholes and attachments cy.get('[data-cy="import-button"]').click(); cy.wait("@borehole-upload"); diff --git a/src/client/cypress/fixtures/import/boreholes-multiple-valid.csv b/src/client/cypress/fixtures/import/boreholes-multiple-valid.csv index 7e73e3f42..2f955f650 100644 --- a/src/client/cypress/fixtures/import/boreholes-multiple-valid.csv +++ b/src/client/cypress/fixtures/import/boreholes-multiple-valid.csv @@ -1,5 +1,5 @@ -name;original_name;location_x;location_y;attachments; -BH-1001;Wellington 1;2156784;1154321;borehole_attachment_1.pdf,borehole_attachment_2.zip; +name;original_name;location_x;location_y; +BH-1001;Wellington 1;2156784;1154321; BH-1002;Wellington 2;2367999;1276543; BH-1003;Wellington 3;2189456;1334567; BH-1004;Wellington 4;2312345;1200987; From 769e1bac2483f2ac2db2f1d4f9e4a37ac4393f65 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 09:37:55 +0100 Subject: [PATCH 26/38] Fix doc --- src/client/docs/import.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client/docs/import.md b/src/client/docs/import.md index 450a83119..1e70e34b2 100644 --- a/src/client/docs/import.md +++ b/src/client/docs/import.md @@ -40,16 +40,16 @@ Die zu importierenden Daten müssen gemäss obigen Anforderungen im CSV-Format v | Feldname | Datentyp | Pflichtfeld | Beschreibung | | --------------------------- | -------------- | ----------- | ------------------------------------------------------------------------------------- | -| IdGeodinShortname | Zahl | Nein | ID GeODin-Shortname | -| IdInfoGeol | Zahl | Nein | ID InfoGeol | -| IdOriginal | Zahl | Nein | ID Original | -| IdCanton | Zahl | Nein | ID Kanton | -| IdGeoQuat | Zahl | Nein | ID GeoQuat | -| IdGeoMol | Zahl | Nein | ID GeoMol | -| IdGeoTherm | Zahl | Nein | ID GeoTherm | -| IdTopFels | Zahl | Nein | ID TopFels | -| IdGeodin | Zahl | Nein | ID GeODin | -| IdKernlager | Zahl | Nein | ID Kernlager | +| IDGeODin-Shortname | Zahl | Nein | ID GeODin-Shortname | +| IDInfoGeol | Zahl | Nein | ID InfoGeol | +| IDOriginal | Zahl | Nein | ID Original | +| IDCanton | Zahl | Nein | ID Kanton | +| IDGeoQuat | Zahl | Nein | ID GeoQuat | +| IDGeoMol | Zahl | Nein | ID GeoMol | +| IDGeoTherm | Zahl | Nein | ID GeoTherm | +| IDTopFels | Zahl | Nein | ID TopFels | +| IDGeODin | Zahl | Nein | ID GeODin | +| IDKernlager | Zahl | Nein | ID Kernlager | | OriginalName | Text | Ja | Originalname | | ProjectName | Text | Nein | Projektname | | Name | Text | Nein | Name | From f4ac170054ce890bca1e12b41717d61580b650bc Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 09:41:37 +0100 Subject: [PATCH 27/38] Fix changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb005ff65..d51cba132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed +- Removed attachments from csv import. +- Updated recommended csv headers for borehole import to camel case e.g. `OriginalName` (snake case e.g. `original_name` is still supported for all properties except for custom identifiers). + ## v2.1.993 - 2024-12-13 ### Added @@ -29,8 +33,6 @@ - When copying a borehole, attachments won't be copied. - Removed csv lithology import. - Removed import id from csv import. -- Removed attachments from csv import. -- Updated recommended csv headers for borehole import to camel case e.g.`OriginalName` (snake case e.g.`original_name` is still supported for all properties except for custom identifiers). ### Fixed From cad2b2425ce15d23d9a1de50a44a43093861a0f3 Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 09:54:17 +0100 Subject: [PATCH 28/38] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700f6625c..77f58377b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Observations were not included in exported borehole JSON file. + ## v2.1.993 - 2024-12-13 ### Added @@ -47,7 +51,6 @@ - When copying a borehole, the nested collections of observations were not copied. - There was a bug when changing the order, transparency or visibility of custom WMS user layers. - The borehole status was not translated everywhere in the workflow panel. -- Observations were not included in exported borehole JSON files. ## v2.1.870 - 2024-09-27 From 3854b35cc1c67816bdaba418e91c33633c2e573b Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 09:58:03 +0100 Subject: [PATCH 29/38] Add xml comments --- src/api/Controllers/BoreholeController.cs | 5 +---- src/api/Controllers/ObservationConverter.cs | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api/Controllers/BoreholeController.cs b/src/api/Controllers/BoreholeController.cs index 7f77e3be1..3f63ac555 100644 --- a/src/api/Controllers/BoreholeController.cs +++ b/src/api/Controllers/BoreholeController.cs @@ -130,10 +130,7 @@ public async Task> GetByIdAsync(int id) [Authorize(Policy = PolicyNames.Viewer)] public async Task ExportJsonAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) { - if (ids == null || !ids.Any()) - { - return BadRequest("The list of IDs must not be empty."); - } + if (ids == null || !ids.Any()) return BadRequest("The list of IDs must not be empty."); var boreholes = await GetBoreholesWithIncludes().AsNoTracking().Where(borehole => ids.Contains(borehole.Id)).ToListAsync().ConfigureAwait(false); diff --git a/src/api/Controllers/ObservationConverter.cs b/src/api/Controllers/ObservationConverter.cs index 817ad1774..0f9a900d4 100644 --- a/src/api/Controllers/ObservationConverter.cs +++ b/src/api/Controllers/ObservationConverter.cs @@ -4,6 +4,9 @@ namespace BDMS.Controllers; +/// +/// Serializes and deserializes objects based on their ObservationType. +/// public class ObservationConverter : JsonConverter { private static readonly JsonSerializerOptions observationDefaultOptions = new JsonSerializerOptions @@ -12,6 +15,7 @@ public class ObservationConverter : JsonConverter ReferenceHandler = ReferenceHandler.IgnoreCycles, }; + /// public override Observation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using JsonDocument doc = JsonDocument.ParseValue(ref reader); @@ -30,6 +34,7 @@ public class ObservationConverter : JsonConverter }; } + /// public override void Write(Utf8JsonWriter writer, Observation value, JsonSerializerOptions options) { switch (value) @@ -53,7 +58,7 @@ public override void Write(Utf8JsonWriter writer, Observation value, JsonSeriali JsonSerializer.Serialize(writer, observation, options); break; default: - throw new NotSupportedException($"Observation type is not supported"); + throw new NotSupportedException("Observation type is not supported"); } } } From 1df4a56f9190b8ceae694e8916fbdcedb10ddf59 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 10:18:39 +0100 Subject: [PATCH 30/38] Remove attachments from import controller --- src/api/Controllers/ImportController.cs | 53 +---- tests/api/Controllers/ImportControllerTest.cs | 205 ++---------------- tests/api/TestData/borehole_attachment_1.pdf | Bin 33938 -> 0 bytes tests/api/TestData/borehole_attachment_2.pdf | Bin 34272 -> 0 bytes tests/api/TestData/borehole_attachment_3.csv | 2 - tests/api/TestData/borehole_attachment_4.zip | Bin 27272 -> 0 bytes tests/api/TestData/borehole_attachment_5.png | Bin 1113 -> 0 bytes ...rehole_attachment_with_wrong_extension.txt | Bin 37820 -> 0 bytes .../TestData/borehole_with_attachments.csv | 2 - ...th_mixed_case_in_attachments_filenames.csv | 2 - .../borehole_with_not_present_attachments.csv | 2 - .../boreholes_not_all_have_attachments.csv | 4 - 12 files changed, 20 insertions(+), 250 deletions(-) delete mode 100644 tests/api/TestData/borehole_attachment_1.pdf delete mode 100644 tests/api/TestData/borehole_attachment_2.pdf delete mode 100644 tests/api/TestData/borehole_attachment_3.csv delete mode 100644 tests/api/TestData/borehole_attachment_4.zip delete mode 100644 tests/api/TestData/borehole_attachment_5.png delete mode 100644 tests/api/TestData/borehole_attachment_with_wrong_extension.txt delete mode 100644 tests/api/TestData/borehole_with_attachments.csv delete mode 100644 tests/api/TestData/borehole_with_mixed_case_in_attachments_filenames.csv delete mode 100644 tests/api/TestData/borehole_with_not_present_attachments.csv delete mode 100644 tests/api/TestData/boreholes_not_all_have_attachments.csv diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index d6522b620..07cb89e5c 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -117,13 +117,12 @@ public async Task> UploadJsonFileAsync(int workgroupId, IFormF ///
/// The of the new (s). /// The containing the borehole csv records that were uploaded. - /// The list of containing the borehole attachments referred in . /// The number of the newly created s. [HttpPost] [Authorize(Policy = PolicyNames.Viewer)] [RequestSizeLimit(int.MaxValue)] [RequestFormLimits(MultipartBodyLengthLimit = MaxFileSize)] - public async Task> UploadFileAsync(int workgroupId, IFormFile boreholesFile, IList? attachments = null) + public async Task> UploadFileAsync(int workgroupId, IFormFile boreholesFile) { // Increase max allowed errors to be able to return more validation errors at once. ModelState.MaxAllowedErrors = 1000; @@ -137,14 +136,8 @@ public async Task> UploadFileAsync(int workgroupId, IFormFile // Checks if the provided boreholes file is a CSV file. if (!FileTypeChecker.IsCsv(boreholesFile)) return BadRequest("Invalid file type for borehole csv."); - // Checks if any of the provided attachments has a whitespace in its file name. - if (attachments?.Any(a => a.FileName.Any(char.IsWhiteSpace)) == true) return BadRequest("One or more file name(s) contain a whitespace."); - - // Checks if any of the provided attachments exceeds the maximum file size. - if (attachments?.Any(a => a.Length > MaxFileSize) == true) return BadRequest($"One or more attachment exceed maximum file size of {MaxFileSize / 1024 / 1024} Mb."); - var boreholeImports = ReadBoreholesFromCsv(boreholesFile); - ValidateBoreholeImports(workgroupId, boreholeImports, false, attachments); + ValidateBoreholeImports(workgroupId, boreholeImports, false); // If any validation error occured, return a bad request. if (!ModelState.IsValid) return ValidationProblem(statusCode: (int)HttpStatusCode.BadRequest); @@ -216,21 +209,6 @@ public async Task> UploadFileAsync(int workgroupId, IFormFile await context.Boreholes.AddRangeAsync(boreholes).ConfigureAwait(false); var result = await SaveChangesAsync(() => Ok(boreholes.Count)).ConfigureAwait(false); - // Add attachments to borehole. - if (attachments != null) - { - var boreholeImportsWithAttachments = boreholeImports.Where(x => x.Attachments?.Length > 0).ToList(); - foreach (var boreholeImport in boreholeImportsWithAttachments) - { - var attachmentFileNames = boreholeImport.Attachments?.Split(",").Select(s => s.Replace(" ", "", StringComparison.InvariantCulture)).ToList(); - var attachmentFiles = attachments.Where(x => attachmentFileNames != null && attachmentFileNames.Contains(x.FileName.Replace(" ", "", StringComparison.InvariantCulture))).ToList(); - foreach (var attachmentFile in attachmentFiles) - { - await boreholeFileCloudService.UploadFileAndLinkToBorehole(attachmentFile, boreholeImport.Id).ConfigureAwait(false); - } - } - } - await transaction.CommitAsync().ConfigureAwait(false); return result; } @@ -259,20 +237,19 @@ internal static int GetPrecision(IReaderRow row, string fieldName) return 0; } - private void ValidateBoreholeImports(int workgroupId, List boreholesFromFile, bool isJsonFile, IList? attachments = null) + private void ValidateBoreholeImports(int workgroupId, List boreholesFromFile, bool isJsonFile) { foreach (var borehole in boreholesFromFile.Select((value, index) => (value, index))) { - ValidateBorehole(borehole.value, boreholesFromFile, workgroupId, borehole.index, isJsonFile, attachments); + ValidateBorehole(borehole.value, boreholesFromFile, workgroupId, borehole.index, isJsonFile); } } - private void ValidateBorehole(BoreholeImport borehole, List boreholesFromFile, int workgroupId, int boreholeIndex, bool isJsonFile, IList? attachments) + private void ValidateBorehole(BoreholeImport borehole, List boreholesFromFile, int workgroupId, int boreholeIndex, bool isJsonFile) { ValidateRequiredFields(borehole, boreholeIndex, isJsonFile); ValidateDuplicateInFile(borehole, boreholesFromFile, boreholeIndex, isJsonFile); ValidateDuplicateInDb(borehole, workgroupId, boreholeIndex, isJsonFile); - ValidateAttachments(borehole, attachments, boreholeIndex, isJsonFile); } private void ValidateRequiredFields(BoreholeImport borehole, int processingIndex, bool isJsonFile) @@ -312,26 +289,6 @@ private void ValidateDuplicateInDb(BoreholeImport borehole, int workgroupId, int } } - private void ValidateAttachments(BoreholeImport borehole, IList? attachments, int processingIndex, bool isJsonFile) - { - if (attachments == null || string.IsNullOrEmpty(borehole.Attachments)) return; - - var boreholeFileNames = borehole.Attachments - .Split(",") - .Select(s => s.Trim()) - .Where(s => !string.IsNullOrEmpty(s)) - .ToList(); - - foreach (var boreholeFileName in boreholeFileNames) - { - // Check if the name of any attached file matches the name of the borehole file - if (!attachments.Any(a => a.FileName.Equals(boreholeFileName, StringComparison.OrdinalIgnoreCase))) - { - AddValidationErrorToModelState(processingIndex, $"Attachment file '{boreholeFileName}' not found.", isJsonFile); - } - } - } - internal static bool CompareValuesWithTolerance(double? firstValue, double? secondValue, double tolerance) { if (firstValue == null && secondValue == null) return true; diff --git a/tests/api/Controllers/ImportControllerTest.cs b/tests/api/Controllers/ImportControllerTest.cs index 9e4c93396..417081cee 100644 --- a/tests/api/Controllers/ImportControllerTest.cs +++ b/tests/api/Controllers/ImportControllerTest.cs @@ -489,7 +489,7 @@ public async Task UploadShouldSaveDataToDatabaseAsync() var boreholeCsvFile = GetFormFileByExistingFile("testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -536,7 +536,7 @@ public async Task UploadShouldSaveMinimalDatasetAsync() var boreholeCsvFile = GetFormFileByExistingFile("minimal_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, attachments: null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -575,7 +575,7 @@ public async Task UploadShouldSavePrecisionDatasetAsync() var boreholeCsvFile = GetFormFileByExistingFile("precision_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, attachments: null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -610,74 +610,6 @@ public async Task UploadShouldSavePrecisionDatasetAsync() Assert.AreEqual(4, boreholeWithZeros.PrecisionLocationY); } - [TestMethod] - public async Task UploadShouldSaveBoreholeWithAttachmentsAsync() - { - httpClientFactoryMock - .Setup(cf => cf.CreateClient(It.IsAny())) - .Returns(() => new HttpClient()) - .Verifiable(); - - var boreholeCsvFormFile = GetFormFileByExistingFile("borehole_with_attachments.csv"); - var firstAttachmentFile = GetRandomPDFFile("attachment_1.pdf"); - var secondAttachmentFile = GetRandomFile("attachment_2.txt"); - var thirdAttachmentFile = GetRandomFile("attachment_3.zip"); - var fourthAttachmentFile = GetRandomFile("attachment_4.jpg"); - var fifthAttachmentFile = GetRandomFile("attachment_5.csv"); - var sixthAttachmentFile = GetFormFileByExistingFile("borehole_attachment_1.pdf"); - var seventhAttachmentFile = GetFormFileByExistingFile("borehole_attachment_2.pdf"); - var eighthAttachmentFile = GetFormFileByExistingFile("borehole_attachment_3.csv"); - var ninthAttachmentFile = GetFormFileByExistingFile("borehole_attachment_4.zip"); - var tenthAttachmentFile = GetFormFileByExistingFile("borehole_attachment_5.png"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFormFile, new List() { firstAttachmentFile, secondAttachmentFile, thirdAttachmentFile, fourthAttachmentFile, fifthAttachmentFile, sixthAttachmentFile, seventhAttachmentFile, eighthAttachmentFile, ninthAttachmentFile, tenthAttachmentFile }); - - ActionResultAssert.IsOk(response.Result); - OkObjectResult okResult = (OkObjectResult)response.Result!; - Assert.AreEqual(1, okResult.Value); - - var borehole = GetBoreholesWithIncludes(context.Boreholes).Single(b => b.OriginalName == "ACORNFLEA"); - Assert.AreEqual(10, borehole.BoreholeFiles.Count); - } - - [TestMethod] - public async Task UploadShouldSaveBoreholesWithNotAllHaveAttachmentsAsync() - { - httpClientFactoryMock - .Setup(cf => cf.CreateClient(It.IsAny())) - .Returns(() => new HttpClient()) - .Verifiable(); - - var boreholeCsvFormFile = GetFormFileByExistingFile("boreholes_not_all_have_attachments.csv"); - var firstAttachmentFile = GetFormFileByExistingFile("borehole_attachment_1.pdf"); - var secondAttachmentFile = GetFormFileByExistingFile("borehole_attachment_2.pdf"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFormFile, new List() { firstAttachmentFile, secondAttachmentFile }); - - ActionResultAssert.IsOk(response.Result); - OkObjectResult okResult = (OkObjectResult)response.Result!; - Assert.AreEqual(3, okResult.Value); - } - - [TestMethod] - public async Task UploadShouldSaveBoreholeWithAttachmentFileNamesMixedCaseAsync() - { - httpClientFactoryMock - .Setup(cf => cf.CreateClient(It.IsAny())) - .Returns(() => new HttpClient()) - .Verifiable(); - - var boreholeCsvFormFile = GetFormFileByExistingFile("borehole_with_mixed_case_in_attachments_filenames.csv"); - var firstPdfFormFile = GetFormFileByExistingFile("borehole_attachment_1.pdf"); - var secondPdfFormFile = GetFormFileByExistingFile("borehole_attachment_2.pdf"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFormFile, new List() { firstPdfFormFile, secondPdfFormFile }); - - ActionResultAssert.IsOk(response.Result); - OkObjectResult okResult = (OkObjectResult)response.Result!; - Assert.AreEqual(1, okResult.Value); - } - [TestMethod] public async Task UploadShouldSaveSpecialCharsDatasetAsync() { @@ -688,7 +620,7 @@ public async Task UploadShouldSaveSpecialCharsDatasetAsync() var boreholeCsvFile = GetFormFileByExistingFile("special_chars_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -707,7 +639,7 @@ public async Task UploadWithMissingCoordinatesAsync() { var boreholeCsvFile = GetFormFileByExistingFile("no_coordinates_provided_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); Assert.IsInstanceOfType(response.Result, typeof(ObjectResult)); ObjectResult result = (ObjectResult)response.Result!; @@ -729,7 +661,7 @@ public async Task UploadBoreholeWithLV95CoordinatesAsync() var boreholeCsvFile = GetFormFileByExistingFile("lv95_coordinates_provided_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -758,7 +690,7 @@ public async Task UploadBoreholeWithLV03CoordinatesAsync() var boreholeCsvFile = GetFormFileByExistingFile("lv03_coordinates_provided_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -787,7 +719,7 @@ public async Task UploadBoreholeWithLV03OutOfRangeCoordinatesAsync() var boreholeCsvFile = GetFormFileByExistingFile("lv03_out_of_range_coordinates_provided_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -811,7 +743,7 @@ public async Task UploadEmptyFileShouldReturnError() { var boreholeCsvFile = new FormFile(null, 0, 0, null, "non_existent_file.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsBadRequest(response.Result); BadRequestObjectResult badRequestResult = (BadRequestObjectResult)response.Result!; @@ -823,101 +755,13 @@ public async Task UploadInvalidFileTypeBoreholeCsvShouldReturnError() { var invalidFileTypeBoreholeFile = GetFormFileByContent(fileContent: "This is the content of the file.", fileName: "invalid_file_type.txt"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, invalidFileTypeBoreholeFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, invalidFileTypeBoreholeFile); ActionResultAssert.IsBadRequest(response.Result); BadRequestObjectResult badRequestResult = (BadRequestObjectResult)response.Result!; Assert.AreEqual("Invalid file type for borehole csv.", badRequestResult.Value); } - [TestMethod] - public async Task UploadBoreholeCsvFileWithoutAttachmentsButWithProvidedFilesShouldCreateBorehole() - { - httpClientFactoryMock - .Setup(cf => cf.CreateClient(It.IsAny())) - .Returns(() => new HttpClient()) - .Verifiable(); - - var boreholeCsvFile = GetFormFileByContent(fileContent: "original_name;location_x;location_y\r\nFrank Place;2000000;1000000", fileName: "boreholes.csv"); - var firstPdfFormFile = GetFormFileByExistingFile("borehole_attachment_1.pdf"); - var secondPdfFormFile = GetFormFileByExistingFile("borehole_attachment_2.pdf"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, new List() { firstPdfFormFile, secondPdfFormFile }); - - ActionResultAssert.IsOk(response.Result); - OkObjectResult okResult = (OkObjectResult)response.Result!; - Assert.AreEqual(1, okResult.Value); - } - - [TestMethod] - public async Task UploadBoreholeCsvFileWithAttachmentsLinkedPdfsShouldCreateBorehole() - { - httpClientFactoryMock - .Setup(cf => cf.CreateClient(It.IsAny())) - .Returns(() => new HttpClient()) - .Verifiable(); - - var firstAttachmentFileName = "borehole_attachment_1.pdf"; - var secondAttachmentFileName = "borehole_attachment_2.pdf"; - - var pdfContent = @"original_name;location_x;location_y;attachments -Frank Place;2000000;1000000;borehole_attachment_1.pdf,borehole_attachment_2.pdf"; - var boreholeCsvFile = GetFormFileByContent(fileContent: pdfContent, fileName: "boreholes.csv"); - var firstPdfFormFile = GetFormFileByExistingFile(firstAttachmentFileName); - var secondPdfFormFile = GetFormFileByExistingFile(secondAttachmentFileName); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, new List() { firstPdfFormFile, secondPdfFormFile }); - - ActionResultAssert.IsOk(response.Result); - OkObjectResult okResult = (OkObjectResult)response.Result!; - Assert.AreEqual(1, okResult.Value); - - // Get latest borehole Ids - var latestBoreholeId = context.Boreholes.OrderByDescending(b => b.Id).First().Id; - - var borehole = GetBoreholesWithIncludes(context.Boreholes) - .Single(b => b.Id == latestBoreholeId); - - Assert.AreEqual(borehole.BoreholeFiles.First().File.Name, firstAttachmentFileName); - Assert.AreEqual(borehole.BoreholeFiles.Last().File.Name, secondAttachmentFileName); - Assert.AreEqual(borehole.BoreholeFiles.Count, 2); - } - - [TestMethod] - public async Task UploadBoreholeCsvFileWithNotPresentAttachmentsShouldReturnError() - { - var boreholeCsvFile = GetFormFileByExistingFile("borehole_with_not_present_attachments.csv"); - var firstPdfFormFile = GetFormFileByExistingFile("borehole_attachment_1.pdf"); - var secondPdfFormFile = GetFormFileByExistingFile("borehole_attachment_2.pdf"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, new List() { firstPdfFormFile, secondPdfFormFile }); - - Assert.IsInstanceOfType(response.Result, typeof(ObjectResult)); - ObjectResult result = (ObjectResult)response.Result!; - ActionResultAssert.IsBadRequest(result); - - ValidationProblemDetails problemDetails = (ValidationProblemDetails)result.Value!; - Assert.AreEqual(1, problemDetails.Errors.Count); - - CollectionAssert.AreEquivalent( - new[] { "Attachment file 'is_not_present_in_upload_files.pdf' not found." }, - problemDetails.Errors["Row1"]); - } - - [TestMethod] - public async Task UploadBoreholeCsvFileWithWhiteSpaceInAttachmentFileNameShouldReturnError() - { - var boreholeCsvFile = GetFormFileByExistingFile("borehole_with_not_present_attachments.csv"); - var firstPdfFormFile = GetFormFileByExistingFile("borehole_attachment_1.pdf"); - var whiteSpacePdf = GetFormFileByExistingFile("white space.pdf"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, new List() { firstPdfFormFile, whiteSpacePdf }); - - ActionResultAssert.IsBadRequest(response.Result); - BadRequestObjectResult badRequestResult = (BadRequestObjectResult)response.Result!; - Assert.AreEqual("One or more file name(s) contain a whitespace.", badRequestResult.Value); - } - [TestMethod] public void IsCorrectFileType() { @@ -934,7 +778,7 @@ public void IsCorrectFileType() [TestMethod] public async Task UploadNoFileShouldReturnError() { - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, null, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, null); ActionResultAssert.IsBadRequest(response.Result); BadRequestObjectResult badRequestResult = (BadRequestObjectResult)response.Result!; @@ -946,7 +790,7 @@ public async Task UploadNoDataButRequiredHeadersSetShouldUploadNoBorehole() { var boreholeCsvFile = GetFormFileByExistingFile("no_data_but_required_headers.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; @@ -958,7 +802,7 @@ public async Task UploadMultipleRowsMissingRequiredFieldsShouldReturnError() { var boreholeCsvFile = GetFormFileByExistingFile("multiple_rows_missing_required_attributes_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); Assert.IsInstanceOfType(response.Result, typeof(ObjectResult)); ObjectResult result = (ObjectResult)response.Result!; @@ -983,7 +827,7 @@ public async Task UploadRequiredHeadersMissingShouldReturnError() { var boreholeCsvFile = GetFormFileByExistingFile("missing_required_headers_testdata.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); Assert.IsInstanceOfType(response.Result, typeof(ObjectResult)); var result = (ObjectResult)response.Result!; @@ -1100,25 +944,6 @@ public async Task UploadDuplicateBoreholesInDbButDifferentWorkgroupShouldUploadB Assert.AreEqual(2, okResult.Value); } - [TestMethod] - public async Task UploadWithAttachmentToLargeShouldThrowError() - { - var minBoreholeId = context.Boreholes.Min(b => b.Id); - var boreholeCsvFile = GetRandomFile("borehoel.csv"); - - long targetSizeInBytes = 201 * 1024 * 1024; // 201MB - byte[] content = new byte[targetSizeInBytes]; - var stream = new MemoryStream(content); - - var attachment = new FormFile(stream, 0, stream.Length, "file", "dummy.txt"); - - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, new[] { attachment }); - - ActionResultAssert.IsBadRequest(response.Result); - BadRequestObjectResult badRequestResult = (BadRequestObjectResult)response.Result!; - Assert.AreEqual($"One or more attachment exceed maximum file size of 200 Mb.", badRequestResult.Value); - } - [TestMethod] public async Task UploadWithMaxValidationErrorsExceededShouldReturnError() { @@ -1156,7 +981,7 @@ public async Task UploadShouldIgnoreLocationFields() var boreholeCsvFile = GetFormFileByExistingFile("borehole_and_location_data.csv"); - ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile, null); + ActionResult response = await controller.UploadFileAsync(workgroupId: 1, boreholeCsvFile); ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; diff --git a/tests/api/TestData/borehole_attachment_1.pdf b/tests/api/TestData/borehole_attachment_1.pdf deleted file mode 100644 index ca3c36c8df11f7a51fb2c7c6b7b7c519054e868b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33938 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 A?*IS* diff --git a/tests/api/TestData/borehole_attachment_2.pdf b/tests/api/TestData/borehole_attachment_2.pdf deleted file mode 100644 index 5cecb0fa6b8931e28cbcea67f94edaa6489928fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34272 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 N$7dq+U9MPu~+#M^AqqpGaR-vtQ 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 diff --git a/tests/api/TestData/borehole_attachment_5.png b/tests/api/TestData/borehole_attachment_5.png deleted file mode 100644 index a971b23556167285ee08e8cc354a4b831d501d8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1113 zcmV-f1g86mP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1MW#gK~!i%?V9gT zQ&$+rzx{n%S`z92(P-3Ae-0NRQ8(1!Z;dVijV2_hbNWUT$Kpi(0FCp-VlbZ7aW~IMDD9l%gUS%x21EAx$)`Y!5|WbrnT>@eYczkRocZ+o5dUf=D1R zBpMZ$g%nX`!$yisaQ3f$H zqgS4JyfrbHb3MqauHo5m!d+eH{ALBuCKfije~j=us;phdTN(rZHc*TNgD_=g!dAG7 za?uDEO)D*>=u`V8gFU*8x2I20RMu~xC>o)nO3_-FX3a+6??>o6{~NCYgWX;5{q-k8 zZEe(lO<7s|j5vgf+W2GZudBnf-~XUC!}vue<^KD%>L_6t`kY9FiJE52gk#4}M0`Ft zN=jf%Plvzu9-Rx+*Tb@CF>I^XP(HFBi$IttyLh%PWJ@L{)mwQS!Kd09vG3AF^xwJ( z%l!HDMX-KqU6xPhL?KRe<0gv6j10WHeoYm-L3xzk$_kQYvnGbJPV8M3BFaWqTVWx! znauwuPf&X#QCWs#*KQb`PHKx_ToVRUP7c!ceaEv{Y^tawZz;krU&41so7*z1`T6Q2 zS^!_wZG_v~c~zBE`?4{^tqxib5Nd7Z)#1E7{WEWkj*%0Faf@cxKjN8q*caAWw3=t5 zW5PrYiq^NrLfW3abo5sIgn_0eUL75w7sD_7Q%s2HcyS#mhwF!OyeTV1ph24oibDEa zcdC#Q)f*{My^#{t8!1r{ggdnPt#Nt|MVUwz?VxDVyJ)DTg`#D_0*W$`dq>+}A^iF^ zEMMmFiaewbM!}}XFwdO}=YfN~A_u9f7Y18kXH`|xPN2xYHR-FD!T);jX1}&O!7j$W z{ab2_M*1C%V#=Nl_vI_JLl}5ik3eH1{Iz!xYHsGA_&g?c;T@0kib~jvONMsq*-KpE z?rsdKN4T?tetRR{K6rmUN9RY&loO3&UHTQQ1qCoKTm(z52WF3FXe*R4*6W(GqVX`g z-LyYtbUMep^^#DcpTkH|>7(vs5hKd}Uy|s=bBGaTuYo2y@f>3MC?hG}*VQY@I~eJe fl&IcF5>enipEY&zs%TP100000NkvXXu0mjf_|6L` diff --git a/tests/api/TestData/borehole_attachment_with_wrong_extension.txt b/tests/api/TestData/borehole_attachment_with_wrong_extension.txt deleted file mode 100644 index f2c43b62e743c0174e28b0fb7eef4f70ab0f74c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37820 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 diff --git a/tests/api/TestData/borehole_with_attachments.csv b/tests/api/TestData/borehole_with_attachments.csv deleted file mode 100644 index 75b26ca6f..000000000 --- a/tests/api/TestData/borehole_with_attachments.csv +++ /dev/null @@ -1,2 +0,0 @@ -original_name;location_x;location_y;attachments -ACORNFLEA;0;0;attachment_1.pdf,attachment_2.txt,attachment_3.zip,attachment_4.jpg,attachment_5.csv,borehole_attachment_1.pdf,borehole_attachment_2.pdf,borehole_attachment_3.csv,borehole_attachment_4.zip,borehole_attachment_5.png diff --git a/tests/api/TestData/borehole_with_mixed_case_in_attachments_filenames.csv b/tests/api/TestData/borehole_with_mixed_case_in_attachments_filenames.csv deleted file mode 100644 index ab830ee9f..000000000 --- a/tests/api/TestData/borehole_with_mixed_case_in_attachments_filenames.csv +++ /dev/null @@ -1,2 +0,0 @@ -original_name;location_x;location_y;attachments -ACORNFLEA;0;0;Borehole_Attachment_1.pdf,borehole_attachment_2.pdf diff --git a/tests/api/TestData/borehole_with_not_present_attachments.csv b/tests/api/TestData/borehole_with_not_present_attachments.csv deleted file mode 100644 index 65fd39745..000000000 --- a/tests/api/TestData/borehole_with_not_present_attachments.csv +++ /dev/null @@ -1,2 +0,0 @@ -original_name;location_x;location_y;attachments -ACORNFLEA;0;0;borehole_attachment_1.pdf,is_not_present_in_upload_files.pdf diff --git a/tests/api/TestData/boreholes_not_all_have_attachments.csv b/tests/api/TestData/boreholes_not_all_have_attachments.csv deleted file mode 100644 index fbd3fb704..000000000 --- a/tests/api/TestData/boreholes_not_all_have_attachments.csv +++ /dev/null @@ -1,4 +0,0 @@ -original_name;location_x;location_y;attachments -ACORNFLEA;2000000;1000000;borehole_attachment_1.pdf,borehole_attachment_2.pdf -BERRYSNAIL;2000010;1000010; -BLUEBIRDY;2000020;1000020;; From 56cd2c4b8cb455ed0afbb636192872c5872f2943 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 10:30:13 +0100 Subject: [PATCH 31/38] Remove attachments from cypress fixtures --- src/client/cypress/e2e/mainPage/import.cy.js | 2 +- .../import/data-sets/invalid-lithology/borehole-valid.csv | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 src/client/cypress/fixtures/import/data-sets/invalid-lithology/borehole-valid.csv diff --git a/src/client/cypress/e2e/mainPage/import.cy.js b/src/client/cypress/e2e/mainPage/import.cy.js index 4897a8b0f..2551abf5b 100644 --- a/src/client/cypress/e2e/mainPage/import.cy.js +++ b/src/client/cypress/e2e/mainPage/import.cy.js @@ -21,7 +21,7 @@ describe("Test for importing boreholes.", () => { }); }); - // Import boreholes and attachments + // Import boreholes cy.get('[data-cy="import-button"]').click(); cy.wait("@borehole-upload"); diff --git a/src/client/cypress/fixtures/import/data-sets/invalid-lithology/borehole-valid.csv b/src/client/cypress/fixtures/import/data-sets/invalid-lithology/borehole-valid.csv deleted file mode 100644 index cc578434c..000000000 --- a/src/client/cypress/fixtures/import/data-sets/invalid-lithology/borehole-valid.csv +++ /dev/null @@ -1,5 +0,0 @@ -name;original_name;location_x;location_y; -BH-1001;Wellington 1;2156784;1154321; -BH-1002;Wellington 2;2367999;1276543; -BH-1003;Wellington 3;2189456;1334567; -BH-1004;Wellington 4;2312345;1200987; \ No newline at end of file From 01c15d1286a53f8c137e39dd7795347548135194 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 10:45:41 +0100 Subject: [PATCH 32/38] Change order of fields --- .../detail/form/borehole/boreholeForm.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client/src/pages/detail/form/borehole/boreholeForm.tsx b/src/client/src/pages/detail/form/borehole/boreholeForm.tsx index 1455abf55..de0add108 100644 --- a/src/client/src/pages/detail/form/borehole/boreholeForm.tsx +++ b/src/client/src/pages/detail/form/borehole/boreholeForm.tsx @@ -140,29 +140,29 @@ export const BoreholeForm = forwardRef(({ borehole, editingEnabled, onSubmit }: From 66aea7127d6bf4980fe696f4ab3ba4a4d0302127 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 10:58:00 +0100 Subject: [PATCH 33/38] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d51cba132..a27cc9276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Removed attachments from csv import. - Updated recommended csv headers for borehole import to camel case e.g. `OriginalName` (snake case e.g. `original_name` is still supported for all properties except for custom identifiers). +- Changed order of `Top Bedrock (fresh)` and `Top Bedrock (weathered)` fields in borehole form. ## v2.1.993 - 2024-12-13 From 3f6b5695b6ba71690ee5ef6f8e048e1a8cef84ed Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 10:47:39 +0100 Subject: [PATCH 34/38] Move export json endpoint to export controller --- src/api/BoreholeExtensions.cs | 36 +++++ src/api/Controllers/BoreholeController.cs | 126 +--------------- src/api/Controllers/ExportController.cs | 24 +++ .../api/Controllers/BoreholeControllerTest.cs | 85 ----------- tests/api/Controllers/ExportControllerTest.cs | 137 ++++++++++++++++++ 5 files changed, 200 insertions(+), 208 deletions(-) create mode 100644 src/api/BoreholeExtensions.cs diff --git a/src/api/BoreholeExtensions.cs b/src/api/BoreholeExtensions.cs new file mode 100644 index 000000000..9858bb418 --- /dev/null +++ b/src/api/BoreholeExtensions.cs @@ -0,0 +1,36 @@ +using BDMS.Models; +using Microsoft.EntityFrameworkCore; + +namespace BDMS; + +public static class BoreholeExtensions +{ + public static IQueryable GetAllWithIncludes(this DbSet boreholes) + { + return boreholes.Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerColorCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerDebrisCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainAngularityCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainShapeCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerOrganicComponentCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerUscs3Codes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.LithologicalDescriptions) + .Include(b => b.Stratigraphies).ThenInclude(s => s.FaciesDescriptions) + .Include(b => b.Stratigraphies).ThenInclude(s => s.ChronostratigraphyLayers) + .Include(b => b.Stratigraphies).ThenInclude(s => s.LithostratigraphyLayers) + .Include(b => b.Completions).ThenInclude(c => c.Casings).ThenInclude(c => c.CasingElements) + .Include(b => b.Completions).ThenInclude(c => c.Instrumentations) + .Include(b => b.Completions).ThenInclude(c => c.Backfills) + .Include(b => b.Sections).ThenInclude(s => s.SectionElements) + .Include(b => b.Observations).ThenInclude(o => (o as FieldMeasurement)!.FieldMeasurementResults) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestResults) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestEvaluationMethodCodes) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestFlowDirectionCodes) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestKindCodes) + .Include(b => b.BoreholeCodelists) + .Include(b => b.Workflows) + .Include(b => b.BoreholeFiles) + .Include(b => b.BoreholeGeometry) + .Include(b => b.Workgroup) + .Include(b => b.UpdatedBy); + } +} diff --git a/src/api/Controllers/BoreholeController.cs b/src/api/Controllers/BoreholeController.cs index 3f63ac555..dc04e4db5 100644 --- a/src/api/Controllers/BoreholeController.cs +++ b/src/api/Controllers/BoreholeController.cs @@ -1,15 +1,10 @@ using BDMS.Authentication; using BDMS.Models; -using CsvHelper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NetTopologySuite.Geometries; using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; namespace BDMS.Controllers; @@ -87,7 +82,7 @@ public async Task> GetAllAsync([FromQuer pageSize = Math.Min(MaxPageSize, Math.Max(1, pageSize)); var skip = (pageNumber - 1) * pageSize; - var query = GetBoreholesWithIncludes().AsNoTracking(); + var query = Context.Boreholes.GetAllWithIncludes().AsNoTracking(); if (ids != null && ids.Any()) { @@ -109,7 +104,7 @@ public async Task> GetAllAsync([FromQuer [Authorize(Policy = PolicyNames.Viewer)] public async Task> GetByIdAsync(int id) { - var borehole = await GetBoreholesWithIncludes() + var borehole = await Context.Boreholes.GetAllWithIncludes() .AsNoTracking() .SingleOrDefaultAsync(l => l.Id == id) .ConfigureAwait(false); @@ -122,92 +117,6 @@ public async Task> GetByIdAsync(int id) return Ok(borehole); } - /// - /// Asynchronously gets all records filtered by ids. Additional data is included in the response. - /// - /// The required list of borehole ids to filter by. - [HttpGet("json")] - [Authorize(Policy = PolicyNames.Viewer)] - public async Task ExportJsonAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) - { - if (ids == null || !ids.Any()) return BadRequest("The list of IDs must not be empty."); - - var boreholes = await GetBoreholesWithIncludes().AsNoTracking().Where(borehole => ids.Contains(borehole.Id)).ToListAsync().ConfigureAwait(false); - - // Create a new JsonSerializerOptions for this specific endpoint - var options = new JsonSerializerOptions() - { - ReferenceHandler = ReferenceHandler.IgnoreCycles, - WriteIndented = true, - }; - - // Add the default converters from the global configuration - options.Converters.Add(new DateOnlyJsonConverter()); - options.Converters.Add(new LTreeJsonConverter()); - - // Add special converter for the 'Observations' collection - options.Converters.Add(new ObservationConverter()); - - return new JsonResult(boreholes, options); - } - - /// - /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. - /// - /// The list of IDs for the boreholes to be exported. - /// A CSV file containing the details specified boreholes. - [HttpGet("export-csv")] - [Authorize(Policy = PolicyNames.Viewer)] - public async Task DownloadCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) - { - ids = ids.Take(MaxPageSize).ToList(); - if (!ids.Any()) return BadRequest("The list of IDs must not be empty."); - - var boreholes = await Context.Boreholes - .Where(borehole => ids.Contains(borehole.Id)) - .Select(b => new - { - b.Id, - b.OriginalName, - b.ProjectName, - b.Name, - b.RestrictionId, - b.RestrictionUntil, - b.NationalInterest, - b.LocationX, - b.LocationY, - b.LocationPrecisionId, - b.ElevationZ, - b.ElevationPrecisionId, - b.ReferenceElevation, - b.ReferenceElevationTypeId, - b.ReferenceElevationPrecisionId, - b.HrsId, - b.TypeId, - b.PurposeId, - b.StatusId, - b.Remarks, - b.TotalDepth, - b.DepthPrecisionId, - b.TopBedrockFreshMd, - b.TopBedrockWeatheredMd, - b.HasGroundwater, - b.LithologyTopBedrockId, - b.ChronostratigraphyTopBedrockId, - b.LithostratigraphyTopBedrockId, - }) - .ToListAsync() - .ConfigureAwait(false); - - if (boreholes.Count == 0) return NotFound("No borehole(s) found for the provided id(s)."); - - using var stringWriter = new StringWriter(); - using var csvWriter = new CsvWriter(stringWriter, CultureInfo.InvariantCulture); - await csvWriter.WriteRecordsAsync(boreholes).ConfigureAwait(false); - - return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv"); - } - /// /// Asynchronously copies a . /// @@ -231,7 +140,7 @@ public async Task> CopyAsync([Required] int id, [Required] int return Unauthorized(); } - var borehole = await GetBoreholesWithIncludes() + var borehole = await Context.Boreholes.GetAllWithIncludes() .AsNoTracking() .SingleOrDefaultAsync(b => b.Id == id) .ConfigureAwait(false); @@ -343,33 +252,4 @@ public async Task> CopyAsync([Required] int id, [Required] int if (entity == null) return default; return await Task.FromResult(entity.Id).ConfigureAwait(false); } - - private IQueryable GetBoreholesWithIncludes() - { - return Context.Boreholes.Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerColorCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerDebrisCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainAngularityCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainShapeCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerOrganicComponentCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerUscs3Codes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.LithologicalDescriptions) - .Include(b => b.Stratigraphies).ThenInclude(s => s.FaciesDescriptions) - .Include(b => b.Stratigraphies).ThenInclude(s => s.ChronostratigraphyLayers) - .Include(b => b.Stratigraphies).ThenInclude(s => s.LithostratigraphyLayers) - .Include(b => b.Completions).ThenInclude(c => c.Casings).ThenInclude(c => c.CasingElements) - .Include(b => b.Completions).ThenInclude(c => c.Instrumentations) - .Include(b => b.Completions).ThenInclude(c => c.Backfills) - .Include(b => b.Sections).ThenInclude(s => s.SectionElements) - .Include(b => b.Observations).ThenInclude(o => (o as FieldMeasurement)!.FieldMeasurementResults) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestResults) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestEvaluationMethodCodes) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestFlowDirectionCodes) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestKindCodes) - .Include(b => b.BoreholeCodelists) - .Include(b => b.Workflows) - .Include(b => b.BoreholeFiles) - .Include(b => b.BoreholeGeometry) - .Include(b => b.Workgroup) - .Include(b => b.UpdatedBy); - } } diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 386eeb19a..a65ae23bd 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace BDMS.Controllers; @@ -18,11 +20,33 @@ public class ExportController : ControllerBase private const int MaxPageSize = 100; private readonly BdmsContext context; + private static readonly JsonSerializerOptions jsonExportOptions = new() + { + WriteIndented = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + Converters = { new DateOnlyJsonConverter(), new LTreeJsonConverter(), new ObservationConverter() }, + }; + public ExportController(BdmsContext context) { this.context = context; } + /// + /// Asynchronously gets all records filtered by ids. Additional data is included in the response. + /// + /// The required list of borehole ids to filter by. + [HttpGet("json")] + [Authorize(Policy = PolicyNames.Viewer)] + public async Task ExportJsonAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) + { + if (ids == null || !ids.Any()) return BadRequest("The list of IDs must not be empty."); + + var boreholes = await context.Boreholes.GetAllWithIncludes().AsNoTracking().Where(borehole => ids.Contains(borehole.Id)).ToListAsync().ConfigureAwait(false); + + return new JsonResult(boreholes, jsonExportOptions); + } + /// /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. /// diff --git a/tests/api/Controllers/BoreholeControllerTest.cs b/tests/api/Controllers/BoreholeControllerTest.cs index 06bd67fc0..22c17b6a5 100644 --- a/tests/api/Controllers/BoreholeControllerTest.cs +++ b/tests/api/Controllers/BoreholeControllerTest.cs @@ -390,91 +390,6 @@ public async Task CopyBoreholeWithHydrotests() Assert.AreEqual(waterIngress.ConditionsId, copiedWaterIngress.ConditionsId); } - [TestMethod] - public async Task ExportJson() - { - var newBorehole = GetBoreholeToAdd(); - - var fieldMeasurementResult = new FieldMeasurementResult - { - ParameterId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementParameterSchema).FirstAsync().ConfigureAwait(false)).Id, - SampleTypeId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementSampleTypeSchema).FirstAsync().ConfigureAwait(false)).Id, - Value = 10.0, - }; - - var fieldMeasurement = new FieldMeasurement - { - Borehole = newBorehole, - StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), - EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), - Type = ObservationType.FieldMeasurement, - Comment = "Field measurement observation for testing", - FieldMeasurementResults = new List { fieldMeasurementResult }, - }; - - var groundwaterLevelMeasurement = new GroundwaterLevelMeasurement - { - Borehole = newBorehole, - StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), - EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), - Type = ObservationType.GroundwaterLevelMeasurement, - Comment = "Groundwater level measurement observation for testing", - LevelM = 10.0, - LevelMasl = 11.0, - KindId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.GroundwaterLevelMeasurementKindSchema).FirstAsync().ConfigureAwait(false)).Id, - }; - - var waterIngress = new WaterIngress - { - Borehole = newBorehole, - IsOpenBorehole = true, - Type = ObservationType.WaterIngress, - Comment = "Water ingress observation for testing", - QuantityId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressQualitySchema).FirstAsync().ConfigureAwait(false)).Id, - ConditionsId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressConditionsSchema).FirstAsync().ConfigureAwait(false)).Id, - }; - - var hydroTestResult = new HydrotestResult - { - ParameterId = 15203191, - Value = 10.0, - MaxValue = 15.0, - MinValue = 5.0, - }; - - var kindCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.HydrotestKindSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); - var flowDirectionCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FlowdirectionSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); - var evaluationMethodCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.EvaluationMethodSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); - - var kindCodelists = await GetCodelists(context, kindCodelistIds).ConfigureAwait(false); - var flowDirectionCodelists = await GetCodelists(context, flowDirectionCodelistIds).ConfigureAwait(false); - var evaluationMethodCodelists = await GetCodelists(context, evaluationMethodCodelistIds).ConfigureAwait(false); - - var hydroTest = new Hydrotest - { - Borehole = newBorehole, - StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), - EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), - Type = ObservationType.Hydrotest, - Comment = "Hydrotest observation for testing", - HydrotestResults = new List() { hydroTestResult }, - HydrotestFlowDirectionCodes = new List { new() { CodelistId = flowDirectionCodelists[0].Id }, new() { CodelistId = flowDirectionCodelists[1].Id } }, - HydrotestKindCodes = new List { new() { CodelistId = kindCodelists[0].Id }, new() { CodelistId = kindCodelists[1].Id } }, - HydrotestEvaluationMethodCodes = new List { new() { CodelistId = evaluationMethodCodelists[0].Id }, new() { CodelistId = evaluationMethodCodelists[1].Id } }, - }; - - newBorehole.Observations = new List { hydroTest, fieldMeasurement, groundwaterLevelMeasurement, waterIngress }; - - context.Add(newBorehole); - await context.SaveChangesAsync().ConfigureAwait(false); - - var response = await controller.ExportJsonAsync(new List() { newBorehole.Id }).ConfigureAwait(false); - JsonResult jsonResult = (JsonResult)response!; - Assert.IsNotNull(jsonResult.Value); - List boreholes = (List)jsonResult.Value; - Assert.AreEqual(1, boreholes.Count); - } - [TestMethod] public async Task Copy() { diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index a18d32d41..ada15eebe 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -26,6 +26,91 @@ public void TestInitialize() controller = new ExportController(context) { ControllerContext = GetControllerContextAdmin() }; } + [TestMethod] + public async Task ExportJson() + { + var newBorehole = GetBoreholeToAdd(); + + var fieldMeasurementResult = new FieldMeasurementResult + { + ParameterId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementParameterSchema).FirstAsync().ConfigureAwait(false)).Id, + SampleTypeId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementSampleTypeSchema).FirstAsync().ConfigureAwait(false)).Id, + Value = 10.0, + }; + + var fieldMeasurement = new FieldMeasurement + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.FieldMeasurement, + Comment = "Field measurement observation for testing", + FieldMeasurementResults = new List { fieldMeasurementResult }, + }; + + var groundwaterLevelMeasurement = new GroundwaterLevelMeasurement + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.GroundwaterLevelMeasurement, + Comment = "Groundwater level measurement observation for testing", + LevelM = 10.0, + LevelMasl = 11.0, + KindId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.GroundwaterLevelMeasurementKindSchema).FirstAsync().ConfigureAwait(false)).Id, + }; + + var waterIngress = new WaterIngress + { + Borehole = newBorehole, + IsOpenBorehole = true, + Type = ObservationType.WaterIngress, + Comment = "Water ingress observation for testing", + QuantityId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressQualitySchema).FirstAsync().ConfigureAwait(false)).Id, + ConditionsId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressConditionsSchema).FirstAsync().ConfigureAwait(false)).Id, + }; + + var hydroTestResult = new HydrotestResult + { + ParameterId = 15203191, + Value = 10.0, + MaxValue = 15.0, + MinValue = 5.0, + }; + + var kindCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.HydrotestKindSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + var flowDirectionCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FlowdirectionSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + var evaluationMethodCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.EvaluationMethodSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + + var kindCodelists = await GetCodelists(context, kindCodelistIds).ConfigureAwait(false); + var flowDirectionCodelists = await GetCodelists(context, flowDirectionCodelistIds).ConfigureAwait(false); + var evaluationMethodCodelists = await GetCodelists(context, evaluationMethodCodelistIds).ConfigureAwait(false); + + var hydroTest = new Hydrotest + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.Hydrotest, + Comment = "Hydrotest observation for testing", + HydrotestResults = new List() { hydroTestResult }, + HydrotestFlowDirectionCodes = new List { new() { CodelistId = flowDirectionCodelists[0].Id }, new() { CodelistId = flowDirectionCodelists[1].Id } }, + HydrotestKindCodes = new List { new() { CodelistId = kindCodelists[0].Id }, new() { CodelistId = kindCodelists[1].Id } }, + HydrotestEvaluationMethodCodes = new List { new() { CodelistId = evaluationMethodCodelists[0].Id }, new() { CodelistId = evaluationMethodCodelists[1].Id } }, + }; + + newBorehole.Observations = new List { hydroTest, fieldMeasurement, groundwaterLevelMeasurement, waterIngress }; + + context.Add(newBorehole); + await context.SaveChangesAsync().ConfigureAwait(false); + + var response = await controller.ExportJsonAsync(new List() { newBorehole.Id }).ConfigureAwait(false); + JsonResult jsonResult = (JsonResult)response!; + Assert.IsNotNull(jsonResult.Value); + List boreholes = (List)jsonResult.Value; + Assert.AreEqual(1, boreholes.Count); + } + [TestMethod] public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes() { @@ -224,4 +309,56 @@ private static List GetRecordsFromFileContent(FileContentResult result) var csv = new CsvReader(reader, CsvConfigHelper.CsvWriteConfig); return csv.GetRecords().ToList(); } + + private Borehole GetBoreholeToAdd() + { + return new Borehole + { + CreatedById = 4, + UpdatedById = 4, + Locked = null, + LockedById = null, + WorkgroupId = 1, + IsPublic = true, + TypeId = 20101003, + LocationX = 2600000.0, + PrecisionLocationX = 5, + LocationY = 1200000.0, + PrecisionLocationY = 5, + LocationXLV03 = 600000.0, + PrecisionLocationXLV03 = 5, + LocationYLV03 = 200000.0, + PrecisionLocationYLV03 = 5, + OriginalReferenceSystem = ReferenceSystem.LV95, + ElevationZ = 450.5, + HrsId = 20106001, + TotalDepth = 100.0, + RestrictionId = 20111003, + RestrictionUntil = DateTime.UtcNow.AddYears(1), + NationalInterest = false, + OriginalName = "BH-257", + Name = "Borehole 257", + LocationPrecisionId = 20113002, + ElevationPrecisionId = null, + ProjectName = "Project Alpha", + Country = "CH", + Canton = "ZH", + Municipality = "Zurich", + PurposeId = 22103002, + StatusId = 22104001, + DepthPrecisionId = 22108005, + TopBedrockFreshMd = 10.5, + TopBedrockWeatheredMd = 8.0, + HasGroundwater = true, + Geometry = null, + Remarks = "Test borehole for project", + LithologyTopBedrockId = 15104934, + LithostratigraphyTopBedrockId = 15300259, + ChronostratigraphyTopBedrockId = 15001141, + ReferenceElevation = 500.0, + ReferenceElevationPrecisionId = 20114002, + ReferenceElevationTypeId = 20117003, + }; + } + } From 31024aafb7df7cc8251628613143ab09f3954a2f Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 11:19:10 +0100 Subject: [PATCH 35/38] Remove blank line --- tests/api/Controllers/ExportControllerTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index ada15eebe..0fd43e998 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -360,5 +360,4 @@ private Borehole GetBoreholeToAdd() ReferenceElevationTypeId = 20117003, }; } - } From 4db18f8be712c06fa1c47513c7d49ba5e6062961 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 11:21:47 +0100 Subject: [PATCH 36/38] Adapt import help --- src/client/docs/import.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client/docs/import.md b/src/client/docs/import.md index 1e70e34b2..7307fa67e 100644 --- a/src/client/docs/import.md +++ b/src/client/docs/import.md @@ -56,8 +56,8 @@ Die zu importierenden Daten müssen gemäss obigen Anforderungen im CSV-Format v | RestrictionId | ID (Codeliste) | Nein | Beschränkung | | RestrictionUntil | Datum | Nein | Ablaufdatum der Beschränkung | | NationalInterest | True/False | Nein | Nationales Interesse | -| LocationX | Dezimalzahl | Ja | Koordinate Ost LV95 | -| LocationY | Dezimalzahl | Ja | Koordinate Nord LV95 | +| LocationX | Dezimalzahl | Ja | Koordinate Ost in LV95 oder LV03 | +| LocationY | Dezimalzahl | Ja | Koordinate Nord in LV95 oder LV03 | | LocationPrecisionId | ID (Codeliste) | Nein | +/- Koordinaten [m] | | ElevationZ | Dezimalzahl | Nein | Terrainhöhe [m ü.M.] | | ElevationPrecisionId | ID (Codeliste) | Nein | +/- Terrainhöhe [m] | @@ -78,6 +78,10 @@ Die zu importierenden Daten müssen gemäss obigen Anforderungen im CSV-Format v | ChronostratigraphyTopBedrockId| ID (Codeliste) | Nein | Chronostratigraphie Top Fels | | LithostratigraphyTopBedrockId | ID (Codeliste) | Nein | Lithostratigraphie Top Fels | +### Koordinaten + +Koordinaten können in LV95 und LV03 importiert werden, das Räumliche Bezugssystem wird aus den Koordinaten erkannt und abgespeichert. + ## Validierung ### Fehlende Werte From 41013cee2d1c456b38b906f6251d359e2b606d3d Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 11:22:44 +0100 Subject: [PATCH 37/38] Fix typo --- src/client/docs/import.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/docs/import.md b/src/client/docs/import.md index 7307fa67e..1d64b0144 100644 --- a/src/client/docs/import.md +++ b/src/client/docs/import.md @@ -80,7 +80,7 @@ Die zu importierenden Daten müssen gemäss obigen Anforderungen im CSV-Format v ### Koordinaten -Koordinaten können in LV95 und LV03 importiert werden, das Räumliche Bezugssystem wird aus den Koordinaten erkannt und abgespeichert. +Koordinaten können in LV95 und LV03 importiert werden, das räumliche Bezugssystem wird aus den Koordinaten erkannt und abgespeichert. ## Validierung From 769f9900aa8efe15049519d7602deafe5bc8d6e8 Mon Sep 17 00:00:00 2001 From: MiraGeowerkstatt Date: Thu, 19 Dec 2024 11:52:41 +0100 Subject: [PATCH 38/38] Update src/client/docs/import.md Co-authored-by: Daniel Jovanovic --- src/client/docs/import.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/docs/import.md b/src/client/docs/import.md index 1d64b0144..6e9e92140 100644 --- a/src/client/docs/import.md +++ b/src/client/docs/import.md @@ -80,7 +80,7 @@ Die zu importierenden Daten müssen gemäss obigen Anforderungen im CSV-Format v ### Koordinaten -Koordinaten können in LV95 und LV03 importiert werden, das räumliche Bezugssystem wird aus den Koordinaten erkannt und abgespeichert. +Koordinaten können in LV95 oder LV03 importiert werden, das räumliche Bezugssystem wird aus den Koordinaten erkannt und abgespeichert. ## Validierung