Skip to content

Commit

Permalink
Merge branch 'main' into adapt-export-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
MiraGeowerkstatt authored Dec 17, 2024
2 parents b2e08a6 + f88c844 commit 95e2fd1
Show file tree
Hide file tree
Showing 15 changed files with 107 additions and 87 deletions.
4 changes: 2 additions & 2 deletions src/api/BoreholeGeometry/AzIncFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ internal sealed class AzIncFormat : IBoreholeGeometryFormat

public string Name => "Azimuth Inclination";

private Lazy<string> expectedCsvHeader = new(Helper.GetCSVHeader<Geometry>);
private Lazy<string> expectedCsvHeader = new(CsvConfigHelper.GetCsvHeader<Geometry>);

public string CsvHeader => expectedCsvHeader.Value;

public IList<BoreholeGeometryElement> ReadCsv(IFormFile file, int boreholeId)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, Helper.CsvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig);

var data = csv.GetRecords<Geometry>().ToList();

Expand Down
38 changes: 38 additions & 0 deletions src/api/BoreholeGeometry/CsvConfigHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using BDMS.Models;
using CsvHelper;
using CsvHelper.Configuration;
using Humanizer;
using NetTopologySuite.Mathematics;
using NetTopologySuite.Utilities;
using System.Globalization;

namespace BDMS.BoreholeGeometry;

public static class CsvConfigHelper
{
internal static readonly CsvConfiguration CsvConfig = new(new CultureInfo("de-CH"))
{
Delimiter = ";",
IgnoreReferences = true,
PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title),
MissingFieldFound = null,
};

/// <summary>
/// Get the CSV header <see cref="CsvHelper"/> for a class of type <typeparamref name="T"/>.
/// Uses the map generated by <see cref="CsvHelper.CsvContext.AutoMap{T}()"/>.
/// If a property has multiple possible column names only the first is considered.
/// </summary>
/// <typeparam name="T">The class to get the header for.</typeparam>
internal static string GetCsvHeader<T>()
{
var context = new CsvContext(CsvConfig);
var map = context.AutoMap<T>();
return string.Join("; ", map.MemberMaps
.Select(m =>
{
var name = m.Data.Names.FirstOrDefault(m.Data.Member.Name);
return m.Data.IsOptional ? $"[{name}]" : name;
}));
}
}
4 changes: 2 additions & 2 deletions src/api/BoreholeGeometry/PitchRollFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ internal sealed class PitchRollFormat : IBoreholeGeometryFormat

public string Name => "Pitch Roll";

private Lazy<string> expectedCsvHeader = new(Helper.GetCSVHeader<Geometry>);
private Lazy<string> expectedCsvHeader = new(CsvConfigHelper.GetCsvHeader<Geometry>);

public string CsvHeader => expectedCsvHeader.Value;

public IList<BoreholeGeometryElement> ReadCsv(IFormFile file, int boreholeId)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, Helper.CsvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig);

var data = csv.GetRecords<Geometry>().ToList();

Expand Down
4 changes: 2 additions & 2 deletions src/api/BoreholeGeometry/XYZFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ internal sealed class XYZFormat : IBoreholeGeometryFormat

public string Name => "X Y Z";

private Lazy<string> expectedCsvHeader = new(Helper.GetCSVHeader<Geometry>);
private Lazy<string> expectedCsvHeader = new(CsvConfigHelper.GetCsvHeader<Geometry>);

public string CsvHeader => expectedCsvHeader.Value;

public IList<BoreholeGeometryElement> ReadCsv(IFormFile file, int boreholeId)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, Helper.CsvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig);

var data = csv.GetRecords<Geometry>();
return ToBoreholeGeometry(data, boreholeId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
using BDMS.Models;
using CsvHelper;
using CsvHelper.Configuration;
using Humanizer;
using NetTopologySuite.Mathematics;
using NetTopologySuite.Utilities;
using System.Globalization;

namespace BDMS.BoreholeGeometry;
namespace BDMS;

public static class Helper
public static class BoreholeGeometryExtensions
{
internal static readonly CsvConfiguration CsvConfig = new(new CultureInfo("de-CH"))
{
Delimiter = ";",
IgnoreReferences = true,
PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title),
MissingFieldFound = null,
};

/// <summary>
/// Get the CSV header <see cref="CsvHelper"/> expects to read a class <typeparamref name="T"/>.
/// Uses the map generated by <see cref="CsvHelper.CsvContext.AutoMap{T}()"/>.
/// If a property has multiple possible column names only the first is considered.
/// Get the TVD of <paramref name="depthMD"/> according to the <paramref name="geometry"/> if the geometry exists.
/// </summary>
/// <typeparam name="T">The class to get the header for.</typeparam>
internal static string GetCSVHeader<T>()
/// <param name="geometry">The list of <see cref="BoreholeGeometryElement"/> representing the borehole's path geometry.</param>
/// <param name="depthMD">The measured depth (MD) at which to calculate the TVD.</param>
/// <returns>The TVD at <paramref name="depthMD"/> if the geometry exists; otherwise <see langword="null"/>.</returns>
internal static double? GetTVDIfGeometryExists(this List<BoreholeGeometryElement> geometry, double? depthMD)
{
var context = new CsvContext(CsvConfig);
var map = context.AutoMap<T>();
return string.Join("; ", map.MemberMaps
.Select(m =>
if (geometry == null || geometry.Count < 2)
{
if (depthMD != null && depthMD >= 0)
{
var name = m.Data.Names.FirstOrDefault(m.Data.Member.Name);
return m.Data.IsOptional ? $"[{name}]" : name;
}));
}
// Return the depthMD unchanged as if the borehole is perfectly vertical and infinitely long.
return depthMD;
}
}
else if (depthMD != null)
{
try
{
return geometry.GetDepthTVD(depthMD.Value);
}
catch (ArgumentOutOfRangeException)
{
// Exception is ignored so that the method returns null in case the input was invalid.
}
}

private static readonly IComparer<BoreholeGeometryElement> geometryMDComparer = Comparer<BoreholeGeometryElement>.Create((a, b) => a.MD.CompareTo(b.MD));
return null;
}

/// <summary>
/// Get the TVD of <paramref name="depthMD"/> according to the <paramref name="geometry"/>.
Expand Down Expand Up @@ -130,6 +129,8 @@ private static double InterpolateDepthTVD(List<BoreholeGeometryElement> geometry
}
}

private static readonly IComparer<BoreholeGeometryElement> geometryMDComparer = Comparer<BoreholeGeometryElement>.Create((a, b) => a.MD.CompareTo(b.MD));

internal static Vector3D ToVector3D(this BoreholeGeometryElement element)
=> new Vector3D(element.X, element.Y, element.Z);

Expand Down
23 changes: 4 additions & 19 deletions src/api/Controllers/BoreholeGeometryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,28 +118,13 @@ public async Task<IActionResult> GetDepthTVD([FromQuery] int boreholeId, [FromQu
{
var geometry = await GetBoreholeGeometry(boreholeId).ConfigureAwait(false);

if (geometry.Count < 2)
var tvd = geometry.GetTVDIfGeometryExists(depthMD);
if (tvd == null)
{
if (depthMD >= 0)
{
// Return the depthMD unchanged as if the borehole is perfectly vertical and infinitely long.
return Ok(depthMD);
}
}
else
{
try
{
return Ok(geometry.GetDepthTVD(depthMD));
}
catch (ArgumentOutOfRangeException)
{
// Exception is ignored so that the action returns an empty response in case the input was invalid.
}
logger?.LogInformation($"Invalid input, could not calculate true vertical depth from measured depth of {depthMD}");
}

logger?.LogInformation($"Invalid input, could not calculate true vertical depth from measured depth of {depthMD}");
return Ok();
return Ok(tvd);
}

private async Task<List<BoreholeGeometryElement>> GetBoreholeGeometry(int boreholeId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace BDMS.Controllers;

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class UploadController : ControllerBase
public class ImportController : ControllerBase
{
private const int MaxFileSize = 210_000_000; // 1024 x 1024 x 200 = 209715200 bytes
private readonly BdmsContext context;
Expand All @@ -35,7 +35,7 @@ public class UploadController : ControllerBase

private static readonly JsonSerializerOptions jsonImportOptions = new() { PropertyNameCaseInsensitive = true };

public UploadController(BdmsContext context, ILogger<UploadController> logger, LocationService locationService, CoordinateService coordinateService, BoreholeFileCloudService boreholeFileCloudService)
public ImportController(BdmsContext context, ILogger<ImportController> logger, LocationService locationService, CoordinateService coordinateService, BoreholeFileCloudService boreholeFileCloudService)
{
this.context = context;
this.logger = logger;
Expand Down
2 changes: 2 additions & 0 deletions src/client/cypress/e2e/helpers/testHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const interceptApiCalls = () => {
cy.intercept("dataextraction/api/V1/extract_data").as("extract-data");

cy.intercept("https://api3.geo.admin.ch/rest/services/height*").as("height");

cy.intercept("/api/v2/import*").as("borehole-upload");
};

/**
Expand Down
6 changes: 0 additions & 6 deletions src/client/cypress/e2e/mainPage/import.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ describe("Test for importing boreholes.", () => {
});
});

// Intercept upload request
cy.intercept("/api/v2/upload?workgroupId=1").as("borehole-upload");

// Import boreholes and attachments
cy.get('[data-cy="import-button"]').click();
cy.wait("@borehole-upload");
Expand Down Expand Up @@ -75,9 +72,6 @@ describe("Test for importing boreholes.", () => {
});
});
});

cy.intercept("/api/v2/upload?workgroupId=1").as("borehole-upload");

cy.get('[data-cy="import-button"]').click();

cy.wait("@borehole-upload");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
alternate_name;original_name;location_x;location_y
name;original_name;location_x;location_y
BH-1001;Wellington 1;
BH-1002;Wellington 2;
BH-1003;Wellington 3;2189456;1334567
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
alternate_name;original_name;location_x;location_y;attachments;
name;original_name;location_x;location_y;attachments;
BH-1001;Wellington 1;2156784;1154321;borehole_attachment_1.pdf,borehole_attachment_2.zip;
BH-1002;Wellington 2;2367999;1276543;
BH-1003;Wellington 3;2189456;1334567;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import_id;alternate_name;original_name;location_x;location_y;
344;BH-1001;Wellington 1;2156784;1154321;
33;BH-1002;Wellington 2;2367999;1276543;
2;BH-1003;Wellington 3;2189456;1334567;
55;BH-1004;Wellington 4;2312345;1200987;
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;
2 changes: 1 addition & 1 deletion src/client/src/api/borehole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const updateBorehole = async (borehole: BoreholeV2) => {

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

export const copyBorehole = async (boreholeId: GridRowSelectionModel, workgroupId: string | null) => {
Expand Down
12 changes: 6 additions & 6 deletions tests/api/Controllers/BoreholeControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -643,23 +643,23 @@ public async Task CopyInvalidBoreholeId()
[TestMethod]
public async Task CopyInvalidWorkgroupId()
{
boreholeId = GetBoreholeIdToCopy();
boreholeId = testBoreholeId;
var result = await controller.CopyAsync(boreholeId, workgroupId: 0).ConfigureAwait(false);
ActionResultAssert.IsUnauthorized(result.Result);
}

[TestMethod]
public async Task CopyMissingWorkgroupPermission()
{
boreholeId = GetBoreholeIdToCopy();
boreholeId = testBoreholeId;
var result = await controller.CopyAsync(boreholeId, workgroupId: 2).ConfigureAwait(false);
ActionResultAssert.IsUnauthorized(result.Result);
}

[TestMethod]
public async Task CopyWithUnknownUser()
{
boreholeId = GetBoreholeIdToCopy();
boreholeId = testBoreholeId;
controller.HttpContext.SetClaimsPrincipal("NON-EXISTENT-NAME", PolicyNames.Admin);
var result = await controller.CopyAsync(boreholeId, workgroupId: DefaultWorkgroupId).ConfigureAwait(false);
ActionResultAssert.IsUnauthorized(result.Result);
Expand All @@ -668,7 +668,7 @@ public async Task CopyWithUnknownUser()
[TestMethod]
public async Task CopyWithUserNotSet()
{
boreholeId = GetBoreholeIdToCopy();
boreholeId = testBoreholeId;
controller.ControllerContext.HttpContext.User = null;
await Assert.ThrowsExceptionAsync<InvalidOperationException>(async () =>
{
Expand All @@ -679,7 +679,7 @@ await Assert.ThrowsExceptionAsync<InvalidOperationException>(async () =>
[TestMethod]
public async Task CopyWithNonAdminUser()
{
boreholeId = GetBoreholeIdToCopy();
boreholeId = testBoreholeId;
controller.HttpContext.SetClaimsPrincipal("sub_editor", PolicyNames.Viewer);
var result = await controller.CopyAsync(boreholeId, workgroupId: DefaultWorkgroupId).ConfigureAwait(false);
ActionResultAssert.IsOk(result.Result);
Expand All @@ -701,7 +701,7 @@ public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes()
var csvData = Encoding.UTF8.GetString(result.FileContents);
var fileLength = csvData.Split('\n').Length;
var recordCount = fileLength - 2; // Remove header and last line break
Assert.IsTrue(recordCount <= 100);
Assert.AreEqual(100, recordCount);
}

[TestMethod]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ namespace BDMS.Controllers;

[DeploymentItem("TestData")]
[TestClass]
public class UploadControllerTest
public class ImportControllerTest
{
private const int MaxBoreholeSeedId = 1002999;
private const int MaxStratigraphySeedId = 6002999;
private const int MaxLayerSeedId = 7029999;

private BdmsContext context;
private UploadController controller;
private ImportController controller;
private Mock<IHttpClientFactory> httpClientFactoryMock;
private Mock<ILogger<UploadController>> loggerMock;
private Mock<ILogger<ImportController>> loggerMock;
private Mock<ILogger<LocationService>> loggerLocationServiceMock;
private Mock<ILogger<CoordinateService>> loggerCoordinateServiceMock;

Expand All @@ -36,7 +36,7 @@ public void TestInitialize()

context = ContextFactory.CreateContext();
httpClientFactoryMock = new Mock<IHttpClientFactory>(MockBehavior.Strict);
loggerMock = new Mock<ILogger<UploadController>>();
loggerMock = new Mock<ILogger<ImportController>>();

loggerLocationServiceMock = new Mock<ILogger<LocationService>>(MockBehavior.Strict);
var locationService = new LocationService(loggerLocationServiceMock.Object, httpClientFactoryMock.Object);
Expand All @@ -56,7 +56,7 @@ public void TestInitialize()
contextAccessorMock.Object.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, context.Users.FirstOrDefault().SubjectId) }));
var boreholeFileCloudService = new BoreholeFileCloudService(context, configuration, loggerBoreholeFileCloudService.Object, contextAccessorMock.Object, s3ClientMock);

controller = new UploadController(context, loggerMock.Object, locationService, coordinateService, boreholeFileCloudService) { ControllerContext = GetControllerContextAdmin() };
controller = new ImportController(context, loggerMock.Object, locationService, coordinateService, boreholeFileCloudService) { ControllerContext = GetControllerContextAdmin() };
}

[TestCleanup]
Expand Down Expand Up @@ -1137,13 +1137,13 @@ public async Task UploadWithMaxValidationErrorsExceededShouldReturnError()
[TestMethod]
public void CompareValueWithTolerance()
{
Assert.AreEqual(true, UploadController.CompareValuesWithTolerance(null, null, 0));
Assert.AreEqual(true, UploadController.CompareValuesWithTolerance(2100000, 2099998, 2));
Assert.AreEqual(false, UploadController.CompareValuesWithTolerance(2100000, 2000098, 1.99));
Assert.AreEqual(false, UploadController.CompareValuesWithTolerance(2100002, 2000000, 1.99));
Assert.AreEqual(false, UploadController.CompareValuesWithTolerance(21000020, 2000000, 20));
Assert.AreEqual(false, UploadController.CompareValuesWithTolerance(null, 2000000, 0));
Assert.AreEqual(false, UploadController.CompareValuesWithTolerance(2000000, null, 2));
Assert.AreEqual(true, ImportController.CompareValuesWithTolerance(null, null, 0));
Assert.AreEqual(true, ImportController.CompareValuesWithTolerance(2100000, 2099998, 2));
Assert.AreEqual(false, ImportController.CompareValuesWithTolerance(2100000, 2000098, 1.99));
Assert.AreEqual(false, ImportController.CompareValuesWithTolerance(2100002, 2000000, 1.99));
Assert.AreEqual(false, ImportController.CompareValuesWithTolerance(21000020, 2000000, 20));
Assert.AreEqual(false, ImportController.CompareValuesWithTolerance(null, 2000000, 0));
Assert.AreEqual(false, ImportController.CompareValuesWithTolerance(2000000, null, 2));
}

[TestMethod]
Expand Down

0 comments on commit 95e2fd1

Please sign in to comment.