From 0504c162c11292ef17937d607072a3b482a2e30e Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Wed, 18 Dec 2024 12:02:25 +0100 Subject: [PATCH 01/10] Update observationtype for new hydrotest --- .../src/pages/detail/form/hydrogeology/hydrotestInput.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/src/pages/detail/form/hydrogeology/hydrotestInput.jsx b/src/client/src/pages/detail/form/hydrogeology/hydrotestInput.jsx index 338e9283c..568867e96 100644 --- a/src/client/src/pages/detail/form/hydrogeology/hydrotestInput.jsx +++ b/src/client/src/pages/detail/form/hydrogeology/hydrotestInput.jsx @@ -157,7 +157,7 @@ const HydrotestInput = props => { data = prepareCasingDataForSubmit(data); data?.startTime ? (data.startTime += ":00.000Z") : (data.startTime = null); data?.endTime ? (data.endTime += ":00.000Z") : (data.endTime = null); - data.type = ObservationType.fieldMeasurement; + data.type = ObservationType.hydrotest; data.boreholeId = parentId; if (Array.isArray(data.testKindId)) { From 9730e4008241560329b562f6856f789482fa3756 Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Wed, 18 Dec 2024 12:28:09 +0100 Subject: [PATCH 02/10] Add json export endpoint --- src/api/Controllers/BoreholeController.cs | 34 ++++++++ src/api/Controllers/ObservationConverter.cs | 59 +++++++++++++ src/client/src/api/borehole.ts | 5 ++ .../api/Controllers/BoreholeControllerTest.cs | 85 +++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 src/api/Controllers/ObservationConverter.cs diff --git a/src/api/Controllers/BoreholeController.cs b/src/api/Controllers/BoreholeController.cs index d25d2ab0c..7f77e3be1 100644 --- a/src/api/Controllers/BoreholeController.cs +++ b/src/api/Controllers/BoreholeController.cs @@ -8,6 +8,8 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace BDMS.Controllers; @@ -120,6 +122,38 @@ public async Task> GetByIdAsync(int id) return Ok(borehole); } + /// + /// Asynchronously gets all records filtered by ids. Additional data is included in the response. + /// + /// The required list of borehole ids to filter by. + [HttpGet("json")] + [Authorize(Policy = PolicyNames.Viewer)] + public async Task ExportJsonAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) + { + if (ids == null || !ids.Any()) + { + return BadRequest("The list of IDs must not be empty."); + } + + var boreholes = await GetBoreholesWithIncludes().AsNoTracking().Where(borehole => ids.Contains(borehole.Id)).ToListAsync().ConfigureAwait(false); + + // Create a new JsonSerializerOptions for this specific endpoint + var options = new JsonSerializerOptions() + { + ReferenceHandler = ReferenceHandler.IgnoreCycles, + WriteIndented = true, + }; + + // Add the default converters from the global configuration + options.Converters.Add(new DateOnlyJsonConverter()); + options.Converters.Add(new LTreeJsonConverter()); + + // Add special converter for the 'Observations' collection + options.Converters.Add(new ObservationConverter()); + + return new JsonResult(boreholes, options); + } + /// /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. /// diff --git a/src/api/Controllers/ObservationConverter.cs b/src/api/Controllers/ObservationConverter.cs new file mode 100644 index 000000000..c14fad58e --- /dev/null +++ b/src/api/Controllers/ObservationConverter.cs @@ -0,0 +1,59 @@ +using BDMS.Models; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BDMS.Controllers; + +public class ObservationConverter : JsonConverter +{ + private static readonly JsonSerializerOptions observationDefaultOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + }; + + public override Observation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using JsonDocument doc = JsonDocument.ParseValue(ref reader); + var jsonObject = doc.RootElement; + + // Deserialize the observation type to determine the observation type + var observation = JsonSerializer.Deserialize(jsonObject.GetRawText(), observationDefaultOptions); + + return observation.Type switch + { + ObservationType.Hydrotest => JsonSerializer.Deserialize(jsonObject.GetRawText(), options), + ObservationType.FieldMeasurement => JsonSerializer.Deserialize(jsonObject.GetRawText(), options), + ObservationType.WaterIngress => JsonSerializer.Deserialize(jsonObject.GetRawText(), options), + ObservationType.GroundwaterLevelMeasurement => JsonSerializer.Deserialize(jsonObject.GetRawText(), options), + _ => observation, + }; + } + + public override void Write(Utf8JsonWriter writer, Observation value, JsonSerializerOptions options) + { + switch (value) + { + case Hydrotest hydrotest: + hydrotest.EvaluationMethodCodelistIds = hydrotest.HydrotestEvaluationMethodCodes?.Select(x => x.CodelistId).ToList(); + hydrotest.FlowDirectionCodelistIds = hydrotest.HydrotestFlowDirectionCodes?.Select(x => x.CodelistId).ToList(); + hydrotest.KindCodelistIds = hydrotest.HydrotestKindCodes?.Select(x => x.CodelistId).ToList(); + JsonSerializer.Serialize(writer, hydrotest, options); + break; + case FieldMeasurement fieldMeasurement: + JsonSerializer.Serialize(writer, fieldMeasurement, options); + break; + case WaterIngress waterIngress: + JsonSerializer.Serialize(writer, waterIngress, options); + break; + case GroundwaterLevelMeasurement groundwaterLevelMeasurement: + JsonSerializer.Serialize(writer, groundwaterLevelMeasurement, options); + break; + case Observation observation: + JsonSerializer.Serialize(writer, observation, options); + break; + default: + throw new NotSupportedException($"Observation type '{value.GetType().Name}' is not supported"); + } + } +} diff --git a/src/client/src/api/borehole.ts b/src/client/src/api/borehole.ts index 2f4ff93dc..af19fa413 100644 --- a/src/client/src/api/borehole.ts +++ b/src/client/src/api/borehole.ts @@ -67,6 +67,11 @@ export interface BoreholeV2 { export const getBoreholeById = async (id: number) => await fetchApiV2(`borehole/${id}`, "GET"); +export const exportJsonBoreholes = async (ids: number[] | GridRowSelectionModel) => { + const idsQuery = ids.map(id => `ids=${id}`).join("&"); + return await fetchApiV2(`borehole/json?${idsQuery}`, "GET"); +}; + export const updateBorehole = async (borehole: BoreholeV2) => { return await fetchApiV2("borehole", "PUT", borehole); }; diff --git a/tests/api/Controllers/BoreholeControllerTest.cs b/tests/api/Controllers/BoreholeControllerTest.cs index 68708fe28..b15bb7c5e 100644 --- a/tests/api/Controllers/BoreholeControllerTest.cs +++ b/tests/api/Controllers/BoreholeControllerTest.cs @@ -390,6 +390,91 @@ public async Task CopyBoreholeWithHydrotests() Assert.AreEqual(waterIngress.ConditionsId, copiedWaterIngress.ConditionsId); } + [TestMethod] + public async Task ExportJson() + { + var newBorehole = GetBoreholeToAdd(); + + var fieldMeasurementResult = new FieldMeasurementResult + { + ParameterId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementParameterSchema).FirstAsync().ConfigureAwait(false)).Id, + SampleTypeId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementSampleTypeSchema).FirstAsync().ConfigureAwait(false)).Id, + Value = 10.0, + }; + + var fieldMeasurement = new FieldMeasurement + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.FieldMeasurement, + Comment = "Field measurement observation for testing", + FieldMeasurementResults = new List { fieldMeasurementResult }, + }; + + var groundwaterLevelMeasurement = new GroundwaterLevelMeasurement + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.GroundwaterLevelMeasurement, + Comment = "Groundwater level measurement observation for testing", + LevelM = 10.0, + LevelMasl = 11.0, + KindId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.GroundwaterLevelMeasurementKindSchema).FirstAsync().ConfigureAwait(false)).Id, + }; + + var waterIngress = new WaterIngress + { + Borehole = newBorehole, + IsOpenBorehole = true, + Type = ObservationType.WaterIngress, + Comment = "Water ingress observation for testing", + QuantityId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressQualitySchema).FirstAsync().ConfigureAwait(false)).Id, + ConditionsId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressConditionsSchema).FirstAsync().ConfigureAwait(false)).Id, + }; + + var hydroTestResult = new HydrotestResult + { + ParameterId = 15203191, + Value = 10.0, + MaxValue = 15.0, + MinValue = 5.0, + }; + + var kindCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.HydrotestKindSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + var flowDirectionCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FlowdirectionSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + var evaluationMethodCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.EvaluationMethodSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + + var kindCodelists = await GetCodelists(context, kindCodelistIds).ConfigureAwait(false); + var flowDirectionCodelists = await GetCodelists(context, flowDirectionCodelistIds).ConfigureAwait(false); + var evaluationMethodCodelists = await GetCodelists(context, evaluationMethodCodelistIds).ConfigureAwait(false); + + var hydroTest = new Hydrotest + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.Hydrotest, + Comment = "Hydrotest observation for testing", + HydrotestResults = new List() { hydroTestResult }, + HydrotestFlowDirectionCodes = new List { new() { CodelistId = flowDirectionCodelists[0].Id }, new() { CodelistId = flowDirectionCodelists[1].Id } }, + HydrotestKindCodes = new List { new() { CodelistId = kindCodelists[0].Id }, new() { CodelistId = kindCodelists[1].Id } }, + HydrotestEvaluationMethodCodes = new List { new() { CodelistId = evaluationMethodCodelists[0].Id }, new() { CodelistId = evaluationMethodCodelists[1].Id } }, + }; + + newBorehole.Observations = new List { hydroTest, fieldMeasurement, groundwaterLevelMeasurement, waterIngress }; + + context.Add(newBorehole); + await context.SaveChangesAsync().ConfigureAwait(false); + + var response = await controller.ExportJsonAsync(new List() { newBorehole.Id }).ConfigureAwait(false); + JsonResult jsonResult = (JsonResult)response!; + Assert.IsNotNull(jsonResult.Value); + List boreholes = (List)jsonResult.Value; + Assert.AreEqual(1, boreholes.Count); + } + [TestMethod] public async Task Copy() { From 80005a97c4b7c904740f61d3a54c7c9ecfca53f8 Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Wed, 18 Dec 2024 13:09:33 +0100 Subject: [PATCH 03/10] Ensure observations are correctly imported via json --- src/api/Controllers/ImportController.cs | 14 +- tests/api/Controllers/ImportControllerTest.cs | 41 +- tests/api/TestData/json_import_valid.json | 426 +++++++++++++++++- 3 files changed, 469 insertions(+), 12 deletions(-) diff --git a/src/api/Controllers/ImportController.cs b/src/api/Controllers/ImportController.cs index 091e85cf0..3df0a8b9a 100644 --- a/src/api/Controllers/ImportController.cs +++ b/src/api/Controllers/ImportController.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; namespace BDMS.Controllers; @@ -33,7 +34,12 @@ public class ImportController : ControllerBase MissingFieldFound = null, }; - private static readonly JsonSerializerOptions jsonImportOptions = new() { PropertyNameCaseInsensitive = true }; + private static readonly JsonSerializerOptions jsonImportOptions = new() + { + PropertyNameCaseInsensitive = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + Converters = { new DateOnlyJsonConverter(), new LTreeJsonConverter(), new ObservationConverter() }, + }; public ImportController(BdmsContext context, ILogger logger, LocationService locationService, CoordinateService coordinateService, BoreholeFileCloudService boreholeFileCloudService) { @@ -96,8 +102,12 @@ public async Task> UploadJsonFileAsync(int workgroupId, IFormF foreach (var borehole in boreholes) { borehole.MarkAsNew(); + borehole.Workgroup = null; borehole.WorkgroupId = workgroupId; + borehole.LockedBy = null; borehole.LockedById = null; + borehole.UpdatedBy = null; + borehole.CreatedBy = null; borehole.Stratigraphies?.MarkAsNew(); borehole.Completions?.MarkAsNew(); @@ -106,7 +116,7 @@ public async Task> UploadJsonFileAsync(int workgroupId, IFormF // Do not import any workflows from the json file but add a new unfinished workflow for the current user. borehole.Workflows.Clear(); - borehole.Workflows.Add(new Workflow { Borehole = borehole, Role = Role.Editor, UserId = user.Id, Started = DateTime.Now.ToUniversalTime() }); + borehole.Workflows.Add(new Workflow { Role = Role.Editor, UserId = user.Id, Started = DateTime.Now.ToUniversalTime() }); } await context.Boreholes.AddRangeAsync(boreholes).ConfigureAwait(false); diff --git a/tests/api/Controllers/ImportControllerTest.cs b/tests/api/Controllers/ImportControllerTest.cs index 9e4c93396..d0b2a61e5 100644 --- a/tests/api/Controllers/ImportControllerTest.cs +++ b/tests/api/Controllers/ImportControllerTest.cs @@ -23,6 +23,7 @@ public class ImportControllerTest private const int MaxLayerSeedId = 7029999; private BdmsContext context; + private BoreholeController boreholeController; private ImportController controller; private Mock httpClientFactoryMock; private Mock> loggerMock; @@ -56,6 +57,11 @@ 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); + 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 ImportController(context, loggerMock.Object, locationService, coordinateService, boreholeFileCloudService) { ControllerContext = GetControllerContextAdmin() }; } @@ -96,7 +102,6 @@ public async Task UploadJsonWithValidJsonShouldSaveData() var boreholeJsonFile = GetFormFileByExistingFile("json_import_valid.json"); ActionResult response = await controller.UploadJsonFileAsync(workgroupId: 1, boreholeJsonFile); - ActionResultAssert.IsOk(response.Result); OkObjectResult okResult = (OkObjectResult)response.Result!; Assert.AreEqual(2, okResult.Value); @@ -398,13 +403,15 @@ public async Task UploadJsonWithValidJsonShouldSaveData() Assert.AreEqual(22109020, sectionElement.DrillingMudSubtypeId, nameof(sectionElement.DrillingMudSubtypeId)); // Assert borehole's observations - Assert.AreEqual(2, borehole.Observations.Count, nameof(borehole.Observations.Count)); + Assert.AreEqual(6, borehole.Observations.Count, nameof(borehole.Observations.Count)); + + // Assert observation ObservationType.None (0) var observation = borehole.Observations.First(x => x.FromDepthM == 1900.0); Assert.IsNotNull(observation.Created, nameof(observation.Created).ShouldNotBeNullMessage()); Assert.IsNotNull(observation.CreatedById, nameof(observation.CreatedById).ShouldNotBeNullMessage()); Assert.IsNotNull(observation.Updated, nameof(observation.Updated).ShouldNotBeNullMessage()); Assert.IsNotNull(observation.UpdatedById, nameof(observation.UpdatedById).ShouldNotBeNullMessage()); - Assert.AreEqual((ObservationType)2, observation.Type, nameof(observation.Type)); + Assert.AreEqual((ObservationType)0, observation.Type, nameof(observation.Type)); Assert.AreEqual(DateTime.Parse("2021-10-05T17:41:48.389173Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), observation.StartTime, nameof(observation.StartTime)); Assert.AreEqual(DateTime.Parse("2021-09-21T20:42:21.785577Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), observation.EndTime, nameof(observation.EndTime)); Assert.AreEqual(1380.508568643829, observation.Duration, nameof(observation.Duration)); @@ -419,6 +426,34 @@ public async Task UploadJsonWithValidJsonShouldSaveData() Assert.IsNull(observation.Reliability, nameof(observation.Reliability).ShouldBeNullMessage()); Assert.IsNotNull(observation.Borehole, nameof(observation.Borehole).ShouldNotBeNullMessage()); + // Assert observation ObservationType.Wateringress (1) + var waterIngress = (WaterIngress)borehole.Observations.First(x => x.Type == ObservationType.WaterIngress); + Assert.IsNotNull(waterIngress.ConditionsId, nameof(waterIngress.ConditionsId).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, waterIngress.ConditionsId, nameof(waterIngress.ConditionsId)); + Assert.IsNotNull(waterIngress.QuantityId, nameof(waterIngress.QuantityId).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, waterIngress.QuantityId, nameof(waterIngress.QuantityId)); + + // Assert observation ObservationType.GroundwaterLevelMeasurement (2) + var groundwaterLevelMeasurement = (GroundwaterLevelMeasurement)borehole.Observations.First(x => x.Type == ObservationType.GroundwaterLevelMeasurement); + Assert.IsNotNull(groundwaterLevelMeasurement.KindId, nameof(groundwaterLevelMeasurement.KindId).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, groundwaterLevelMeasurement.KindId, nameof(groundwaterLevelMeasurement.KindId)); + + // Assert observation ObservationType.Hydrotest (3) + var hydrotest = (Hydrotest)borehole.Observations.First(x => x.Type == ObservationType.Hydrotest); + Assert.IsNotNull(hydrotest.KindCodelistIds, nameof(hydrotest.KindCodelistIds).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, hydrotest.KindCodelistIds.Count, nameof(hydrotest.KindCodelistIds)); + Assert.IsNotNull(hydrotest.FlowDirectionCodelistIds, nameof(hydrotest.FlowDirectionCodelistIds).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, hydrotest.FlowDirectionCodelistIds.Count, nameof(hydrotest.FlowDirectionCodelistIds)); + Assert.IsNotNull(hydrotest.EvaluationMethodCodelistIds, nameof(hydrotest.EvaluationMethodCodelistIds).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, hydrotest.EvaluationMethodCodelistIds.Count, nameof(hydrotest.EvaluationMethodCodelistIds)); + Assert.IsNotNull(hydrotest.HydrotestResults, nameof(hydrotest.HydrotestResults).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, hydrotest.HydrotestResults.Count, nameof(hydrotest.HydrotestResults)); + + // Assert observation ObservationType.FieldMeasurement (4) + var fieldMeasurement = (FieldMeasurement)borehole.Observations.First(x => x.Type == ObservationType.FieldMeasurement); + Assert.IsNotNull(fieldMeasurement.FieldMeasurementResults, nameof(fieldMeasurement.FieldMeasurementResults).ShouldNotBeNullMessage()); + Assert.AreNotEqual(0, fieldMeasurement.FieldMeasurementResults.Count, nameof(fieldMeasurement.FieldMeasurementResults)); + // Assert borehole's workflows Assert.AreEqual(1, borehole.Workflows.Count, nameof(borehole.Workflows.Count)); var workflow = borehole.Workflows.First(); diff --git a/tests/api/TestData/json_import_valid.json b/tests/api/TestData/json_import_valid.json index 5f243b7a9..04dd18dbc 100644 --- a/tests/api/TestData/json_import_valid.json +++ b/tests/api/TestData/json_import_valid.json @@ -6,12 +6,48 @@ "created": "2021-12-06T23:03:55.072565Z", "updated": "2021-10-24T21:44:07.044057Z", "updatedById": 5, - "updatedBy": null, + "UpdatedBy": { + "Id": 5, + "Name": "p. user", + "SubjectId": "sub_publisher", + "FirstName": "publisher", + "LastName": "user", + "IsAdmin": false, + "IsDisabled": false, + "DisabledAt": null, + "CreatedAt": "2024-12-12T14:00:27.13319Z", + "Settings": null, + "WorkgroupRoles": null, + "TermsAccepted": null, + "Deletable": null + }, "locked": null, "lockedById": 1, - "lockedBy": null, + "LockedBy": { + "Id": 5, + "Name": "p. user", + "SubjectId": "sub_publisher", + "FirstName": "publisher", + "LastName": "user", + "IsAdmin": false, + "IsDisabled": false, + "DisabledAt": null, + "CreatedAt": "2024-12-12T14:00:27.13319Z", + "Settings": null, + "WorkgroupRoles": null, + "TermsAccepted": null, + "Deletable": null + }, "workgroupId": 1, - "workgroup": null, + "Workgroup": { + "Id": 1, + "Name": "Default", + "CreatedAt": "2024-12-12T14:00:27.120984Z", + "DisabledAt": null, + "IsDisabled": false, + "Settings": "{}", + "BoreholeCount": 1 + }, "isPublic": true, "typeId": null, "type": null, @@ -1092,7 +1128,7 @@ "observations": [ { "id": 12000000, - "type": 2, + "type": 0, "startTime": "2021-10-05T17:41:48.389173Z", "endTime": "2021-09-21T20:42:21.785577Z", "duration": 1380.508568643829, @@ -1117,7 +1153,7 @@ }, { "id": 12000067, - "type": 2, + "type": 0, "startTime": "2021-03-18T13:35:45.875877Z", "endTime": "2021-09-14T15:16:08.77059Z", "duration": 4444.1706566532, @@ -1139,6 +1175,194 @@ "updatedById": 2, "updatedBy": null, "updated": "2021-12-22T13:26:28.067476Z" + }, + { + "KindCodelistIds": [ + 15203170, + 15203173, + 15203175 + ], + "KindCodelists": null, + "FlowDirectionCodelistIds": [ + 15203186, + 15203187 + ], + "FlowDirectionCodelists": null, + "EvaluationMethodCodelistIds": [ + 15203189, + 15203190 + ], + "EvaluationMethodCodelists": null, + "HydrotestResults": [ + { + "Id": 13001011, + "ParameterId": 15203194, + "Parameter": null, + "Value": -1345, + "MaxValue": 345, + "MinValue": 435, + "HydrotestId": 12000594, + "Hydrotest": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T08:28:32.449923Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T08:28:32.450057Z" + }, + { + "Id": 13001012, + "ParameterId": 15203196, + "Parameter": null, + "Value": 435, + "MaxValue": 345, + "MinValue": 435, + "HydrotestId": 12000594, + "Hydrotest": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T08:28:32.450236Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T08:28:32.450271Z" + } + ], + "Id": 12000594, + "Type": 3, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T08:28:32.449569Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T08:28:32.449763Z" + }, + { + "FieldMeasurementResults": [ + { + "Id": 14001051, + "SampleTypeId": 15203211, + "SampleType": null, + "ParameterId": 15203215, + "Parameter": null, + "Value": 546, + "FieldMeasurementId": 12000603, + "FieldMeasurement": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:00.087647Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:00.08788Z" + }, + { + "Id": 14001052, + "SampleTypeId": 15203212, + "SampleType": null, + "ParameterId": 15203215, + "Parameter": null, + "Value": 546, + "FieldMeasurementId": 12000603, + "FieldMeasurement": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:00.088139Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:00.088216Z" + } + ], + "Id": 12000603, + "Type": 4, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:00.087092Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:00.08738Z" + }, + { + "KindId": 15203206, + "Kind": null, + "LevelM": 456, + "LevelMasl": 455, + "Id": 12000604, + "Type": 2, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:18.335128Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:18.33522Z" + }, + { + "QuantityId": 15203162, + "Quantity": null, + "ConditionsId": 15203166, + "Conditions": null, + "Id": 12000605, + "Type": 1, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:31.217663Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:31.217772Z" } ], "workflows": [ @@ -2250,7 +2474,7 @@ "observations": [ { "id": 12000000, - "type": 2, + "type": 0, "startTime": "2021-10-05T17:41:48.389173Z", "endTime": "2021-09-21T20:42:21.785577Z", "duration": 1380.508568643829, @@ -2275,7 +2499,7 @@ }, { "id": 12000067, - "type": 2, + "type": 0, "startTime": "2021-03-18T13:35:45.875877Z", "endTime": "2021-09-14T15:16:08.77059Z", "duration": 4444.1706566532, @@ -2297,6 +2521,194 @@ "updatedById": 2, "updatedBy": null, "updated": "2021-12-22T13:26:28.067476Z" + }, + { + "KindCodelistIds": [ + 15203170, + 15203173, + 15203175 + ], + "KindCodelists": null, + "FlowDirectionCodelistIds": [ + 15203186, + 15203187 + ], + "FlowDirectionCodelists": null, + "EvaluationMethodCodelistIds": [ + 15203189, + 15203190 + ], + "EvaluationMethodCodelists": null, + "HydrotestResults": [ + { + "Id": 13001011, + "ParameterId": 15203194, + "Parameter": null, + "Value": -1345, + "MaxValue": 345, + "MinValue": 435, + "HydrotestId": 12000594, + "Hydrotest": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T08:28:32.449923Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T08:28:32.450057Z" + }, + { + "Id": 13001012, + "ParameterId": 15203196, + "Parameter": null, + "Value": 435, + "MaxValue": 345, + "MinValue": 435, + "HydrotestId": 12000594, + "Hydrotest": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T08:28:32.450236Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T08:28:32.450271Z" + } + ], + "Id": 12000594, + "Type": 3, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T08:28:32.449569Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T08:28:32.449763Z" + }, + { + "FieldMeasurementResults": [ + { + "Id": 14001051, + "SampleTypeId": 15203211, + "SampleType": null, + "ParameterId": 15203215, + "Parameter": null, + "Value": 546, + "FieldMeasurementId": 12000603, + "FieldMeasurement": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:00.087647Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:00.08788Z" + }, + { + "Id": 14001052, + "SampleTypeId": 15203212, + "SampleType": null, + "ParameterId": 15203215, + "Parameter": null, + "Value": 546, + "FieldMeasurementId": 12000603, + "FieldMeasurement": null, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:00.088139Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:00.088216Z" + } + ], + "Id": 12000603, + "Type": 4, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:00.087092Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:00.08738Z" + }, + { + "KindId": 15203206, + "Kind": null, + "LevelM": 456, + "LevelMasl": 455, + "Id": 12000604, + "Type": 2, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:18.335128Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:18.33522Z" + }, + { + "QuantityId": 15203162, + "Quantity": null, + "ConditionsId": 15203166, + "Conditions": null, + "Id": 12000605, + "Type": 1, + "StartTime": null, + "EndTime": null, + "Duration": null, + "FromDepthM": null, + "ToDepthM": null, + "FromDepthMasl": null, + "ToDepthMasl": null, + "CasingId": null, + "IsOpenBorehole": false, + "Casing": null, + "Comment": "", + "ReliabilityId": null, + "Reliability": null, + "BoreholeId": 1001140, + "CreatedById": 1, + "CreatedBy": null, + "Created": "2024-12-17T10:12:31.217663Z", + "UpdatedById": 1, + "UpdatedBy": null, + "Updated": "2024-12-17T10:12:31.217772Z" } ], "workflows": [ From a2ef53f8c9e404f0b94ade8e0c7bf0f8a1445ee3 Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Wed, 18 Dec 2024 13:13:46 +0100 Subject: [PATCH 04/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39691454c..700f6625c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - 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 0d60a70e9217d6b8b11d924d79a6ccebaaef44bb Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Wed, 18 Dec 2024 16:17:39 +0100 Subject: [PATCH 05/10] Update error message --- src/api/Controllers/ObservationConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Controllers/ObservationConverter.cs b/src/api/Controllers/ObservationConverter.cs index c14fad58e..817ad1774 100644 --- a/src/api/Controllers/ObservationConverter.cs +++ b/src/api/Controllers/ObservationConverter.cs @@ -53,7 +53,7 @@ public override void Write(Utf8JsonWriter writer, Observation value, JsonSeriali JsonSerializer.Serialize(writer, observation, options); break; default: - throw new NotSupportedException($"Observation type '{value.GetType().Name}' is not supported"); + throw new NotSupportedException($"Observation type is not supported"); } } } From 68481403b1ab6c478af1ce7359061e86bcdda4d6 Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Wed, 18 Dec 2024 16:25:16 +0100 Subject: [PATCH 06/10] Remove unused borehole controller --- tests/api/Controllers/ImportControllerTest.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/api/Controllers/ImportControllerTest.cs b/tests/api/Controllers/ImportControllerTest.cs index d0b2a61e5..bb8afbb1f 100644 --- a/tests/api/Controllers/ImportControllerTest.cs +++ b/tests/api/Controllers/ImportControllerTest.cs @@ -23,7 +23,6 @@ public class ImportControllerTest private const int MaxLayerSeedId = 7029999; private BdmsContext context; - private BoreholeController boreholeController; private ImportController controller; private Mock httpClientFactoryMock; private Mock> loggerMock; @@ -57,11 +56,6 @@ 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); - 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 ImportController(context, loggerMock.Object, locationService, coordinateService, boreholeFileCloudService) { ControllerContext = GetControllerContextAdmin() }; } From cad2b2425ce15d23d9a1de50a44a43093861a0f3 Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 09:54:17 +0100 Subject: [PATCH 07/10] 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 08/10] 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 3f6b5695b6ba71690ee5ef6f8e048e1a8cef84ed Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 10:47:39 +0100 Subject: [PATCH 09/10] Move export json endpoint to export controller --- src/api/BoreholeExtensions.cs | 36 +++++ src/api/Controllers/BoreholeController.cs | 126 +--------------- src/api/Controllers/ExportController.cs | 24 +++ .../api/Controllers/BoreholeControllerTest.cs | 85 ----------- tests/api/Controllers/ExportControllerTest.cs | 137 ++++++++++++++++++ 5 files changed, 200 insertions(+), 208 deletions(-) create mode 100644 src/api/BoreholeExtensions.cs diff --git a/src/api/BoreholeExtensions.cs b/src/api/BoreholeExtensions.cs new file mode 100644 index 000000000..9858bb418 --- /dev/null +++ b/src/api/BoreholeExtensions.cs @@ -0,0 +1,36 @@ +using BDMS.Models; +using Microsoft.EntityFrameworkCore; + +namespace BDMS; + +public static class BoreholeExtensions +{ + public static IQueryable GetAllWithIncludes(this DbSet boreholes) + { + return boreholes.Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerColorCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerDebrisCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainAngularityCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainShapeCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerOrganicComponentCodes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerUscs3Codes) + .Include(b => b.Stratigraphies).ThenInclude(s => s.LithologicalDescriptions) + .Include(b => b.Stratigraphies).ThenInclude(s => s.FaciesDescriptions) + .Include(b => b.Stratigraphies).ThenInclude(s => s.ChronostratigraphyLayers) + .Include(b => b.Stratigraphies).ThenInclude(s => s.LithostratigraphyLayers) + .Include(b => b.Completions).ThenInclude(c => c.Casings).ThenInclude(c => c.CasingElements) + .Include(b => b.Completions).ThenInclude(c => c.Instrumentations) + .Include(b => b.Completions).ThenInclude(c => c.Backfills) + .Include(b => b.Sections).ThenInclude(s => s.SectionElements) + .Include(b => b.Observations).ThenInclude(o => (o as FieldMeasurement)!.FieldMeasurementResults) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestResults) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestEvaluationMethodCodes) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestFlowDirectionCodes) + .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestKindCodes) + .Include(b => b.BoreholeCodelists) + .Include(b => b.Workflows) + .Include(b => b.BoreholeFiles) + .Include(b => b.BoreholeGeometry) + .Include(b => b.Workgroup) + .Include(b => b.UpdatedBy); + } +} diff --git a/src/api/Controllers/BoreholeController.cs b/src/api/Controllers/BoreholeController.cs index 3f63ac555..dc04e4db5 100644 --- a/src/api/Controllers/BoreholeController.cs +++ b/src/api/Controllers/BoreholeController.cs @@ -1,15 +1,10 @@ using BDMS.Authentication; using BDMS.Models; -using CsvHelper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NetTopologySuite.Geometries; using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; namespace BDMS.Controllers; @@ -87,7 +82,7 @@ public async Task> GetAllAsync([FromQuer pageSize = Math.Min(MaxPageSize, Math.Max(1, pageSize)); var skip = (pageNumber - 1) * pageSize; - var query = GetBoreholesWithIncludes().AsNoTracking(); + var query = Context.Boreholes.GetAllWithIncludes().AsNoTracking(); if (ids != null && ids.Any()) { @@ -109,7 +104,7 @@ public async Task> GetAllAsync([FromQuer [Authorize(Policy = PolicyNames.Viewer)] public async Task> GetByIdAsync(int id) { - var borehole = await GetBoreholesWithIncludes() + var borehole = await Context.Boreholes.GetAllWithIncludes() .AsNoTracking() .SingleOrDefaultAsync(l => l.Id == id) .ConfigureAwait(false); @@ -122,92 +117,6 @@ public async Task> GetByIdAsync(int id) return Ok(borehole); } - /// - /// Asynchronously gets all records filtered by ids. Additional data is included in the response. - /// - /// The required list of borehole ids to filter by. - [HttpGet("json")] - [Authorize(Policy = PolicyNames.Viewer)] - public async Task ExportJsonAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) - { - if (ids == null || !ids.Any()) return BadRequest("The list of IDs must not be empty."); - - var boreholes = await GetBoreholesWithIncludes().AsNoTracking().Where(borehole => ids.Contains(borehole.Id)).ToListAsync().ConfigureAwait(false); - - // Create a new JsonSerializerOptions for this specific endpoint - var options = new JsonSerializerOptions() - { - ReferenceHandler = ReferenceHandler.IgnoreCycles, - WriteIndented = true, - }; - - // Add the default converters from the global configuration - options.Converters.Add(new DateOnlyJsonConverter()); - options.Converters.Add(new LTreeJsonConverter()); - - // Add special converter for the 'Observations' collection - options.Converters.Add(new ObservationConverter()); - - return new JsonResult(boreholes, options); - } - - /// - /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. - /// - /// The list of IDs for the boreholes to be exported. - /// A CSV file containing the details specified boreholes. - [HttpGet("export-csv")] - [Authorize(Policy = PolicyNames.Viewer)] - public async Task DownloadCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) - { - ids = ids.Take(MaxPageSize).ToList(); - if (!ids.Any()) return BadRequest("The list of IDs must not be empty."); - - var boreholes = await Context.Boreholes - .Where(borehole => ids.Contains(borehole.Id)) - .Select(b => new - { - b.Id, - b.OriginalName, - b.ProjectName, - b.Name, - b.RestrictionId, - b.RestrictionUntil, - b.NationalInterest, - b.LocationX, - b.LocationY, - b.LocationPrecisionId, - b.ElevationZ, - b.ElevationPrecisionId, - b.ReferenceElevation, - b.ReferenceElevationTypeId, - b.ReferenceElevationPrecisionId, - b.HrsId, - b.TypeId, - b.PurposeId, - b.StatusId, - b.Remarks, - b.TotalDepth, - b.DepthPrecisionId, - b.TopBedrockFreshMd, - b.TopBedrockWeatheredMd, - b.HasGroundwater, - b.LithologyTopBedrockId, - b.ChronostratigraphyTopBedrockId, - b.LithostratigraphyTopBedrockId, - }) - .ToListAsync() - .ConfigureAwait(false); - - if (boreholes.Count == 0) return NotFound("No borehole(s) found for the provided id(s)."); - - using var stringWriter = new StringWriter(); - using var csvWriter = new CsvWriter(stringWriter, CultureInfo.InvariantCulture); - await csvWriter.WriteRecordsAsync(boreholes).ConfigureAwait(false); - - return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv"); - } - /// /// Asynchronously copies a . /// @@ -231,7 +140,7 @@ public async Task> CopyAsync([Required] int id, [Required] int return Unauthorized(); } - var borehole = await GetBoreholesWithIncludes() + var borehole = await Context.Boreholes.GetAllWithIncludes() .AsNoTracking() .SingleOrDefaultAsync(b => b.Id == id) .ConfigureAwait(false); @@ -343,33 +252,4 @@ public async Task> CopyAsync([Required] int id, [Required] int if (entity == null) return default; return await Task.FromResult(entity.Id).ConfigureAwait(false); } - - private IQueryable GetBoreholesWithIncludes() - { - return Context.Boreholes.Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerColorCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerDebrisCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainAngularityCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerGrainShapeCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerOrganicComponentCodes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.Layers).ThenInclude(l => l.LayerUscs3Codes) - .Include(b => b.Stratigraphies).ThenInclude(s => s.LithologicalDescriptions) - .Include(b => b.Stratigraphies).ThenInclude(s => s.FaciesDescriptions) - .Include(b => b.Stratigraphies).ThenInclude(s => s.ChronostratigraphyLayers) - .Include(b => b.Stratigraphies).ThenInclude(s => s.LithostratigraphyLayers) - .Include(b => b.Completions).ThenInclude(c => c.Casings).ThenInclude(c => c.CasingElements) - .Include(b => b.Completions).ThenInclude(c => c.Instrumentations) - .Include(b => b.Completions).ThenInclude(c => c.Backfills) - .Include(b => b.Sections).ThenInclude(s => s.SectionElements) - .Include(b => b.Observations).ThenInclude(o => (o as FieldMeasurement)!.FieldMeasurementResults) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestResults) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestEvaluationMethodCodes) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestFlowDirectionCodes) - .Include(b => b.Observations).ThenInclude(o => (o as Hydrotest)!.HydrotestKindCodes) - .Include(b => b.BoreholeCodelists) - .Include(b => b.Workflows) - .Include(b => b.BoreholeFiles) - .Include(b => b.BoreholeGeometry) - .Include(b => b.Workgroup) - .Include(b => b.UpdatedBy); - } } diff --git a/src/api/Controllers/ExportController.cs b/src/api/Controllers/ExportController.cs index 386eeb19a..a65ae23bd 100644 --- a/src/api/Controllers/ExportController.cs +++ b/src/api/Controllers/ExportController.cs @@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace BDMS.Controllers; @@ -18,11 +20,33 @@ public class ExportController : ControllerBase private const int MaxPageSize = 100; private readonly BdmsContext context; + private static readonly JsonSerializerOptions jsonExportOptions = new() + { + WriteIndented = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + Converters = { new DateOnlyJsonConverter(), new LTreeJsonConverter(), new ObservationConverter() }, + }; + public ExportController(BdmsContext context) { this.context = context; } + /// + /// Asynchronously gets all records filtered by ids. Additional data is included in the response. + /// + /// The required list of borehole ids to filter by. + [HttpGet("json")] + [Authorize(Policy = PolicyNames.Viewer)] + public async Task ExportJsonAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable ids) + { + if (ids == null || !ids.Any()) return BadRequest("The list of IDs must not be empty."); + + var boreholes = await context.Boreholes.GetAllWithIncludes().AsNoTracking().Where(borehole => ids.Contains(borehole.Id)).ToListAsync().ConfigureAwait(false); + + return new JsonResult(boreholes, jsonExportOptions); + } + /// /// Exports the details of up to boreholes as a CSV file. Filters the boreholes based on the provided list of IDs. /// diff --git a/tests/api/Controllers/BoreholeControllerTest.cs b/tests/api/Controllers/BoreholeControllerTest.cs index 06bd67fc0..22c17b6a5 100644 --- a/tests/api/Controllers/BoreholeControllerTest.cs +++ b/tests/api/Controllers/BoreholeControllerTest.cs @@ -390,91 +390,6 @@ public async Task CopyBoreholeWithHydrotests() Assert.AreEqual(waterIngress.ConditionsId, copiedWaterIngress.ConditionsId); } - [TestMethod] - public async Task ExportJson() - { - var newBorehole = GetBoreholeToAdd(); - - var fieldMeasurementResult = new FieldMeasurementResult - { - ParameterId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementParameterSchema).FirstAsync().ConfigureAwait(false)).Id, - SampleTypeId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementSampleTypeSchema).FirstAsync().ConfigureAwait(false)).Id, - Value = 10.0, - }; - - var fieldMeasurement = new FieldMeasurement - { - Borehole = newBorehole, - StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), - EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), - Type = ObservationType.FieldMeasurement, - Comment = "Field measurement observation for testing", - FieldMeasurementResults = new List { fieldMeasurementResult }, - }; - - var groundwaterLevelMeasurement = new GroundwaterLevelMeasurement - { - Borehole = newBorehole, - StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), - EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), - Type = ObservationType.GroundwaterLevelMeasurement, - Comment = "Groundwater level measurement observation for testing", - LevelM = 10.0, - LevelMasl = 11.0, - KindId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.GroundwaterLevelMeasurementKindSchema).FirstAsync().ConfigureAwait(false)).Id, - }; - - var waterIngress = new WaterIngress - { - Borehole = newBorehole, - IsOpenBorehole = true, - Type = ObservationType.WaterIngress, - Comment = "Water ingress observation for testing", - QuantityId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressQualitySchema).FirstAsync().ConfigureAwait(false)).Id, - ConditionsId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressConditionsSchema).FirstAsync().ConfigureAwait(false)).Id, - }; - - var hydroTestResult = new HydrotestResult - { - ParameterId = 15203191, - Value = 10.0, - MaxValue = 15.0, - MinValue = 5.0, - }; - - var kindCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.HydrotestKindSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); - var flowDirectionCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FlowdirectionSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); - var evaluationMethodCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.EvaluationMethodSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); - - var kindCodelists = await GetCodelists(context, kindCodelistIds).ConfigureAwait(false); - var flowDirectionCodelists = await GetCodelists(context, flowDirectionCodelistIds).ConfigureAwait(false); - var evaluationMethodCodelists = await GetCodelists(context, evaluationMethodCodelistIds).ConfigureAwait(false); - - var hydroTest = new Hydrotest - { - Borehole = newBorehole, - StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), - EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), - Type = ObservationType.Hydrotest, - Comment = "Hydrotest observation for testing", - HydrotestResults = new List() { hydroTestResult }, - HydrotestFlowDirectionCodes = new List { new() { CodelistId = flowDirectionCodelists[0].Id }, new() { CodelistId = flowDirectionCodelists[1].Id } }, - HydrotestKindCodes = new List { new() { CodelistId = kindCodelists[0].Id }, new() { CodelistId = kindCodelists[1].Id } }, - HydrotestEvaluationMethodCodes = new List { new() { CodelistId = evaluationMethodCodelists[0].Id }, new() { CodelistId = evaluationMethodCodelists[1].Id } }, - }; - - newBorehole.Observations = new List { hydroTest, fieldMeasurement, groundwaterLevelMeasurement, waterIngress }; - - context.Add(newBorehole); - await context.SaveChangesAsync().ConfigureAwait(false); - - var response = await controller.ExportJsonAsync(new List() { newBorehole.Id }).ConfigureAwait(false); - JsonResult jsonResult = (JsonResult)response!; - Assert.IsNotNull(jsonResult.Value); - List boreholes = (List)jsonResult.Value; - Assert.AreEqual(1, boreholes.Count); - } - [TestMethod] public async Task Copy() { diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index a18d32d41..ada15eebe 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -26,6 +26,91 @@ public void TestInitialize() controller = new ExportController(context) { ControllerContext = GetControllerContextAdmin() }; } + [TestMethod] + public async Task ExportJson() + { + var newBorehole = GetBoreholeToAdd(); + + var fieldMeasurementResult = new FieldMeasurementResult + { + ParameterId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementParameterSchema).FirstAsync().ConfigureAwait(false)).Id, + SampleTypeId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FieldMeasurementSampleTypeSchema).FirstAsync().ConfigureAwait(false)).Id, + Value = 10.0, + }; + + var fieldMeasurement = new FieldMeasurement + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.FieldMeasurement, + Comment = "Field measurement observation for testing", + FieldMeasurementResults = new List { fieldMeasurementResult }, + }; + + var groundwaterLevelMeasurement = new GroundwaterLevelMeasurement + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.GroundwaterLevelMeasurement, + Comment = "Groundwater level measurement observation for testing", + LevelM = 10.0, + LevelMasl = 11.0, + KindId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.GroundwaterLevelMeasurementKindSchema).FirstAsync().ConfigureAwait(false)).Id, + }; + + var waterIngress = new WaterIngress + { + Borehole = newBorehole, + IsOpenBorehole = true, + Type = ObservationType.WaterIngress, + Comment = "Water ingress observation for testing", + QuantityId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressQualitySchema).FirstAsync().ConfigureAwait(false)).Id, + ConditionsId = (await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.WateringressConditionsSchema).FirstAsync().ConfigureAwait(false)).Id, + }; + + var hydroTestResult = new HydrotestResult + { + ParameterId = 15203191, + Value = 10.0, + MaxValue = 15.0, + MinValue = 5.0, + }; + + var kindCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.HydrotestKindSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + var flowDirectionCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.FlowdirectionSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + var evaluationMethodCodelistIds = await context.Codelists.Where(c => c.Schema == HydrogeologySchemas.EvaluationMethodSchema).Take(2).Select(c => c.Id).ToListAsync().ConfigureAwait(false); + + var kindCodelists = await GetCodelists(context, kindCodelistIds).ConfigureAwait(false); + var flowDirectionCodelists = await GetCodelists(context, flowDirectionCodelistIds).ConfigureAwait(false); + var evaluationMethodCodelists = await GetCodelists(context, evaluationMethodCodelistIds).ConfigureAwait(false); + + var hydroTest = new Hydrotest + { + Borehole = newBorehole, + StartTime = new DateTime(2021, 01, 01, 01, 01, 01, DateTimeKind.Utc), + EndTime = new DateTime(2021, 01, 01, 13, 01, 01, DateTimeKind.Utc), + Type = ObservationType.Hydrotest, + Comment = "Hydrotest observation for testing", + HydrotestResults = new List() { hydroTestResult }, + HydrotestFlowDirectionCodes = new List { new() { CodelistId = flowDirectionCodelists[0].Id }, new() { CodelistId = flowDirectionCodelists[1].Id } }, + HydrotestKindCodes = new List { new() { CodelistId = kindCodelists[0].Id }, new() { CodelistId = kindCodelists[1].Id } }, + HydrotestEvaluationMethodCodes = new List { new() { CodelistId = evaluationMethodCodelists[0].Id }, new() { CodelistId = evaluationMethodCodelists[1].Id } }, + }; + + newBorehole.Observations = new List { hydroTest, fieldMeasurement, groundwaterLevelMeasurement, waterIngress }; + + context.Add(newBorehole); + await context.SaveChangesAsync().ConfigureAwait(false); + + var response = await controller.ExportJsonAsync(new List() { newBorehole.Id }).ConfigureAwait(false); + JsonResult jsonResult = (JsonResult)response!; + Assert.IsNotNull(jsonResult.Value); + List boreholes = (List)jsonResult.Value; + Assert.AreEqual(1, boreholes.Count); + } + [TestMethod] public async Task DownloadCsvWithValidIdsReturnsFileResultWithMax100Boreholes() { @@ -224,4 +309,56 @@ private static List GetRecordsFromFileContent(FileContentResult result) var csv = new CsvReader(reader, CsvConfigHelper.CsvWriteConfig); return csv.GetRecords().ToList(); } + + private Borehole GetBoreholeToAdd() + { + return new Borehole + { + CreatedById = 4, + UpdatedById = 4, + Locked = null, + LockedById = null, + WorkgroupId = 1, + IsPublic = true, + TypeId = 20101003, + LocationX = 2600000.0, + PrecisionLocationX = 5, + LocationY = 1200000.0, + PrecisionLocationY = 5, + LocationXLV03 = 600000.0, + PrecisionLocationXLV03 = 5, + LocationYLV03 = 200000.0, + PrecisionLocationYLV03 = 5, + OriginalReferenceSystem = ReferenceSystem.LV95, + ElevationZ = 450.5, + HrsId = 20106001, + TotalDepth = 100.0, + RestrictionId = 20111003, + RestrictionUntil = DateTime.UtcNow.AddYears(1), + NationalInterest = false, + OriginalName = "BH-257", + Name = "Borehole 257", + LocationPrecisionId = 20113002, + ElevationPrecisionId = null, + ProjectName = "Project Alpha", + Country = "CH", + Canton = "ZH", + Municipality = "Zurich", + PurposeId = 22103002, + StatusId = 22104001, + DepthPrecisionId = 22108005, + TopBedrockFreshMd = 10.5, + TopBedrockWeatheredMd = 8.0, + HasGroundwater = true, + Geometry = null, + Remarks = "Test borehole for project", + LithologyTopBedrockId = 15104934, + LithostratigraphyTopBedrockId = 15300259, + ChronostratigraphyTopBedrockId = 15001141, + ReferenceElevation = 500.0, + ReferenceElevationPrecisionId = 20114002, + ReferenceElevationTypeId = 20117003, + }; + } + } From 31024aafb7df7cc8251628613143ab09f3954a2f Mon Sep 17 00:00:00 2001 From: Frederic Stahel Date: Thu, 19 Dec 2024 11:19:10 +0100 Subject: [PATCH 10/10] Remove blank line --- tests/api/Controllers/ExportControllerTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/api/Controllers/ExportControllerTest.cs b/tests/api/Controllers/ExportControllerTest.cs index ada15eebe..0fd43e998 100644 --- a/tests/api/Controllers/ExportControllerTest.cs +++ b/tests/api/Controllers/ExportControllerTest.cs @@ -360,5 +360,4 @@ private Borehole GetBoreholeToAdd() ReferenceElevationTypeId = 20117003, }; } - }