Skip to content

Commit

Permalink
Fix json import casing references (#1803)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lehats authored Jan 16, 2025
2 parents 15cbf6f + 6a5678a commit 2a6870c
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Fixed bug where `Lithostratigraphhy Top Bedrock` and `Chronostratigraphhy Top Bedrock` were not displayed in form after updating them and navigating away.
- Ensure all hydrotest codelist values are imported.
- JSON export/import did not handle borehole geometry and location geometry correctly.
- JSON import did not handle casing references of observations, backfills and instrumentations correctly.
- In some cases, the color of the workflow badge did not match the publication status.

## v2.1.993 - 2024-12-13
Expand Down
41 changes: 41 additions & 0 deletions src/api/Controllers/ImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public async Task<ActionResult<int>> UploadJsonFileAsync(int workgroupId, IFormF
borehole.UpdatedBy = null;
borehole.CreatedBy = null;

MapCasingReferences(borehole);

borehole.Stratigraphies?.MarkAsNew();
borehole.Completions?.MarkAsNew();
borehole.Sections?.MarkAsNew();
Expand Down Expand Up @@ -265,6 +267,20 @@ private async Task<List<Codelist>> GetHydrotestCodelistsAsync()
.ConfigureAwait(false);
}

private static void MapCasingReferences(BoreholeImport borehole)
{
var casings = borehole.Completions?.SelectMany(c => c.Casings ?? Enumerable.Empty<Casing>()).ToDictionary(c => c.Id);
if (casings == null) return;

borehole.Observations?.MapCasings(casings);

foreach (var completion in borehole.Completions)
{
completion.Instrumentations?.MapCasings(casings);
completion.Backfills?.MapCasings(casings);
}
}

private static void MapHydrotestCodelists(BoreholeImport borehole, List<Codelist> hydrotestCodelists)
{
var hydroTests = borehole.Observations?.OfType<Hydrotest>().ToList();
Expand Down Expand Up @@ -305,6 +321,7 @@ private void ValidateBorehole(BoreholeImport borehole, List<BoreholeImport> bore
ValidateRequiredFields(borehole, boreholeIndex, isJsonFile);
ValidateDuplicateInFile(borehole, boreholesFromFile, boreholeIndex, isJsonFile);
ValidateDuplicateInDb(borehole, workgroupId, boreholeIndex, isJsonFile);
ValidateCasingReferences(borehole, boreholeIndex);
}

private void ValidateRequiredFields(BoreholeImport borehole, int processingIndex, bool isJsonFile)
Expand Down Expand Up @@ -344,6 +361,30 @@ private void ValidateDuplicateInDb(BoreholeImport borehole, int workgroupId, int
}
}

private void ValidateCasingReferences(BoreholeImport borehole, int processingIndex)
{
// Get all casing Ids from the borehole's completions
var casingIds = borehole.Completions?
.SelectMany(c => c.Casings ?? Enumerable.Empty<Casing>())
.Select(c => c.Id)
.ToHashSet() ?? new HashSet<int>();

// Aggregate all CasingId references from Observations, Instrumentations, and Backfills
var casingReferenceIdsInBorehole = new HashSet<int>(borehole.Observations?.Where(o => o.CasingId.HasValue).Select(o => o.CasingId!.Value) ?? []);
casingReferenceIdsInBorehole
.UnionWith(borehole.Completions?.SelectMany(c => c.Instrumentations ?? Enumerable.Empty<Instrumentation>())
.Where(i => i.CasingId.HasValue)
.Select(i => i.CasingId!.Value) ?? []);
casingReferenceIdsInBorehole
.UnionWith(borehole.Completions?.SelectMany(c => c.Backfills ?? Enumerable.Empty<Backfill>())
.Where(b => b.CasingId.HasValue)
.Select(b => b.CasingId!.Value) ?? []);

// Check if any referenced CasingId is not found in the casingIds set
var invalidReferences = casingReferenceIdsInBorehole.Except(casingIds).ToList();
if (invalidReferences.Count > 0) AddValidationErrorToModelState(processingIndex, $"Some {nameof(ICasingReference.CasingId)} in {nameof(Borehole.Observations)}/{nameof(Completion.Backfills)}/{nameof(Completion.Instrumentations)} do not exist in the borehole's casings.", true);
}

internal static bool CompareValuesWithTolerance(double? firstValue, double? secondValue, double tolerance)
{
if (firstValue == null && secondValue == null) return true;
Expand Down
2 changes: 1 addition & 1 deletion src/api/Models/Backfill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BDMS.Models;
/// Represents a Backfill entity in the database.
/// </summary>
[Table("backfill")]
public class Backfill : IChangeTracking, IIdentifyable
public class Backfill : IChangeTracking, IIdentifyable, ICasingReference
{
/// <inheritdoc />
[Column("id")]
Expand Down
17 changes: 17 additions & 0 deletions src/api/Models/ICasingReference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace BDMS.Models;

/// <summary>
/// An object that has a reference to a <see cref="Models.Casing"/>.
/// </summary>
public interface ICasingReference
{
/// <summary>
/// Gets or sets the ID of the casing in the join table.
/// </summary>
int? CasingId { get; set; }

/// <summary>
/// Gets or sets the casing in the join table.
/// </summary>
Casing? Casing { get; set; }
}
38 changes: 38 additions & 0 deletions src/api/Models/ICasingReferenceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace BDMS.Models;

internal static class ICasingReferenceExtensions
{
/// <summary>
/// Maps a single ICasingReference to its corresponding Casing from the dictionary.
/// </summary>
public static void MapCasing(this ICasingReference casingReference, Dictionary<int, Casing> casings)
{
if (casingReference == null) return;

casingReference.Casing = null;
if (casingReference.CasingId.HasValue)
{
if (casings.TryGetValue(casingReference.CasingId.Value, out var casing))
{
casingReference.Casing = casing;
}
else
{
throw new InvalidOperationException($"Casing with ID {casingReference.CasingId} not found.");
}
}
}

/// <summary>
/// Maps a list of ICasingReference objects to their corresponding Casings from the dictionary.
/// </summary>
public static void MapCasings(this IEnumerable<ICasingReference> casingReferences, Dictionary<int, Casing> casings)
{
if (casingReferences == null) return;

foreach (var casingReference in casingReferences)
{
casingReference.MapCasing(casings);
}
}
}
2 changes: 1 addition & 1 deletion src/api/Models/Instrumentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BDMS.Models;
/// Represents a Instrumentation entity in the database.
/// </summary>
[Table("instrumentation")]
public class Instrumentation : IChangeTracking, IIdentifyable
public class Instrumentation : IChangeTracking, IIdentifyable, ICasingReference
{
/// <inheritdoc />
[Column("id")]
Expand Down
2 changes: 1 addition & 1 deletion src/api/Models/Observation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace BDMS.Models;
/// Represents an observation in the boring process.
/// </summary>
[Table("observation")]
public class Observation : IChangeTracking, IIdentifyable
public class Observation : IChangeTracking, IIdentifyable, ICasingReference
{
/// <summary>
/// Gets or sets the <see cref="Observation"/>'s id.
Expand Down
3 changes: 3 additions & 0 deletions tests/api/BDMS.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
<None Update="TestData\json_import_duplicated_by_location.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\json_import_invalid_casing_ids.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\json_import_single.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
30 changes: 28 additions & 2 deletions tests/api/Controllers/ImportControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,15 @@ public async Task UploadJsonWithValidJsonShouldSaveData()
Assert.AreEqual("Ratione ut non in recusandae labore.", completion.Notes, nameof(completion.Notes));
Assert.AreEqual(DateOnly.Parse("2021-01-24", CultureInfo.InvariantCulture), completion.AbandonDate, nameof(completion.AbandonDate));

// Assert casing ids of instrumentations, backfills and observations
var casingIds = borehole.Completions.Where(c => c.Casings != null).SelectMany(c => c.Casings!).Select(c => c.Id).ToList();
var instrumentationCasingIds = borehole.Completions.Where(c => c.Instrumentations != null).SelectMany(c => c.Instrumentations!).Where(i => i.CasingId.HasValue).Select(i => (int)i.CasingId).ToList();
var backfillCasingIds = borehole.Completions.Where(c => c.Backfills != null).SelectMany(c => c.Backfills!).Where(i => i.CasingId.HasValue).Select(i => (int)i.CasingId).ToList();
var observationCasingIds = borehole.Observations.Where(i => i.CasingId.HasValue).Select(i => (int)i.CasingId).ToList();
Assert.IsTrue(instrumentationCasingIds.All(c => casingIds.Contains(c)), $"{nameof(instrumentationCasingIds)} in {nameof(casingIds)}");
Assert.IsTrue(backfillCasingIds.All(c => casingIds.Contains(c)), $"{nameof(backfillCasingIds)} in {nameof(casingIds)}");
Assert.IsTrue(observationCasingIds.All(c => casingIds.Contains(c)), $"{nameof(observationCasingIds)} in {nameof(casingIds)}");

// Assert completion's instrumentations
Assert.AreEqual(1, completion.Instrumentations.Count, nameof(completion.Instrumentations.Count));
var instrumentation = completion.Instrumentations.First();
Expand All @@ -326,7 +335,6 @@ public async Task UploadJsonWithValidJsonShouldSaveData()
Assert.AreEqual(25000213, instrumentation.StatusId, nameof(instrumentation.StatusId));
Assert.IsNull(instrumentation.Status, nameof(instrumentation.Status).ShouldBeNullMessage());
Assert.IsFalse(instrumentation.IsOpenBorehole, nameof(instrumentation.IsOpenBorehole));
Assert.AreEqual(17000312, instrumentation.CasingId, nameof(instrumentation.CasingId));
Assert.IsNotNull(instrumentation.Casing, nameof(instrumentation.Casing).ShouldNotBeNullMessage());
Assert.AreEqual("copy Field bandwidth Burg", instrumentation.Notes, nameof(instrumentation.Notes));

Expand All @@ -345,7 +353,6 @@ public async Task UploadJsonWithValidJsonShouldSaveData()
Assert.AreEqual(25000306, backfill.MaterialId, nameof(backfill.MaterialId));
Assert.IsNull(backfill.Material, nameof(backfill.Material).ShouldBeNullMessage());
Assert.IsFalse(backfill.IsOpenBorehole, nameof(backfill.IsOpenBorehole));
Assert.AreEqual(17000011, backfill.CasingId, nameof(backfill.CasingId));
Assert.IsNotNull(backfill.Casing, nameof(backfill.Casing).ShouldNotBeNullMessage());
Assert.AreEqual("Licensed Plastic Soap Managed withdrawal Tools & Industrial", backfill.Notes, nameof(backfill.Notes));

Expand Down Expand Up @@ -486,6 +493,25 @@ public async Task UploadJsonWithNoJsonFileShouldReturnError()
Assert.AreEqual("Invalid or empty file uploaded.", badRequestResult.Value);
}

[TestMethod]
public async Task UploadJsonWithInvalidCasingIdsShouldReturnError()
{
var boreholeJsonFile = GetFormFileByExistingFile("json_import_invalid_casing_ids.json");

ActionResult<int> response = await controller.UploadJsonFileAsync(workgroupId: 1, boreholeJsonFile);

Assert.IsInstanceOfType(response.Result, typeof(ObjectResult));
ObjectResult result = (ObjectResult)response.Result!;
ActionResultAssert.IsBadRequest(result);

ValidationProblemDetails problemDetails = (ValidationProblemDetails)result.Value!;
Assert.AreEqual(3, problemDetails.Errors.Count);

CollectionAssert.AreEquivalent(new[] { $"Some {nameof(ICasingReference.CasingId)} in {nameof(Borehole.Observations)}/{nameof(Completion.Backfills)}/{nameof(Completion.Instrumentations)} do not exist in the borehole's casings.", }, problemDetails.Errors["Borehole0"]);
CollectionAssert.AreEquivalent(new[] { $"Some {nameof(ICasingReference.CasingId)} in {nameof(Borehole.Observations)}/{nameof(Completion.Backfills)}/{nameof(Completion.Instrumentations)} do not exist in the borehole's casings.", }, problemDetails.Errors["Borehole1"]);
CollectionAssert.AreEquivalent(new[] { $"Some {nameof(ICasingReference.CasingId)} in {nameof(Borehole.Observations)}/{nameof(Completion.Backfills)}/{nameof(Completion.Instrumentations)} do not exist in the borehole's casings.", }, problemDetails.Errors["Borehole2"]);
}

[TestMethod]
public async Task UploadJsonWithDuplicateBoreholesByLocationShouldReturnError()
{
Expand Down
24 changes: 12 additions & 12 deletions tests/api/TestData/json_import_duplicated_by_location.json
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@
"statusId": 25000213,
"status": null,
"isOpenBorehole": false,
"casingId": 17000312,
"casingId": null,
"casing": null,
"notes": "copy Field bandwidth Burg",
"createdById": 5,
Expand All @@ -738,7 +738,7 @@
"materialId": 25000306,
"material": null,
"isOpenBorehole": false,
"casingId": 17000011,
"casingId": null,
"casing": null,
"notes": "Licensed Plastic Soap Managed withdrawal Tools & Industrial",
"createdById": 2,
Expand Down Expand Up @@ -839,7 +839,7 @@
"statusId": 25000213,
"status": null,
"isOpenBorehole": false,
"casingId": 17000282,
"casingId": null,
"casing": null,
"notes": "monitor alarm Fresh SSL",
"createdById": 2,
Expand All @@ -862,7 +862,7 @@
"materialId": 25000310,
"material": null,
"isOpenBorehole": false,
"casingId": 17000421,
"casingId": null,
"casing": null,
"notes": "Handmade Soft Fish Checking Account transmitting salmon",
"createdById": 1,
Expand Down Expand Up @@ -1086,7 +1086,7 @@
"toDepthM": 2227.610979433456,
"fromDepthMasl": 3136.3928836828063,
"toDepthMasl": 4047.543691819787,
"casingId": 17000130,
"casingId": null,
"isOpenBorehole": true,
"casing": null,
"comment": "Quis repellendus nihil et ipsam ut ad eius.",
Expand All @@ -1111,7 +1111,7 @@
"toDepthM": 759.5223660401639,
"fromDepthMasl": 4940.758798705768,
"toDepthMasl": 587.5749591791886,
"casingId": 17000495,
"casingId": null,
"isOpenBorehole": true,
"casing": null,
"comment": "Ut et accusamus praesentium consequuntur nulla dolor.",
Expand Down Expand Up @@ -1873,7 +1873,7 @@
"statusId": 25000213,
"status": null,
"isOpenBorehole": false,
"casingId": 17000312,
"casingId": null,
"casing": null,
"notes": "copy Field bandwidth Burg",
"createdById": 5,
Expand All @@ -1896,7 +1896,7 @@
"materialId": 25000306,
"material": null,
"isOpenBorehole": false,
"casingId": 17000011,
"casingId": null,
"casing": null,
"notes": "Licensed Plastic Soap Managed withdrawal Tools & Industrial",
"createdById": 2,
Expand Down Expand Up @@ -1997,7 +1997,7 @@
"statusId": 25000213,
"status": null,
"isOpenBorehole": false,
"casingId": 17000282,
"casingId": null,
"casing": null,
"notes": "monitor alarm Fresh SSL",
"createdById": 2,
Expand All @@ -2020,7 +2020,7 @@
"materialId": 25000310,
"material": null,
"isOpenBorehole": false,
"casingId": 17000421,
"casingId": null,
"casing": null,
"notes": "Handmade Soft Fish Checking Account transmitting salmon",
"createdById": 1,
Expand Down Expand Up @@ -2244,7 +2244,7 @@
"toDepthM": 2227.610979433456,
"fromDepthMasl": 3136.3928836828063,
"toDepthMasl": 4047.543691819787,
"casingId": 17000130,
"casingId": null,
"isOpenBorehole": true,
"casing": null,
"comment": "Quis repellendus nihil et ipsam ut ad eius.",
Expand All @@ -2269,7 +2269,7 @@
"toDepthM": 759.5223660401639,
"fromDepthMasl": 4940.758798705768,
"toDepthMasl": 587.5749591791886,
"casingId": 17000495,
"casingId": null,
"isOpenBorehole": true,
"casing": null,
"comment": "Ut et accusamus praesentium consequuntur nulla dolor.",
Expand Down
Loading

0 comments on commit 2a6870c

Please sign in to comment.