Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Some refactoring #1767

Merged
merged 9 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
MiraGeowerkstatt marked this conversation as resolved.
Show resolved Hide resolved
}
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");
MiraGeowerkstatt marked this conversation as resolved.
Show resolved Hide resolved
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
Loading