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
zcmdSAWmsI>vM!7SNbq36X&^}B?h@Rsao5J(H9!dN4#C~sf?IG4?(QDk;UigR?X%aB
zd;i=&A5ZhlF~_JH^;XqeH70a7nY^$lEh8N}5kkq#m)AD|I{=tomqGN>`qn0tU~5_-aUg(R(b3+?&{4@A3