From 6cc2e88bf442ec77f570f3a14ede2a6f75fd949e Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 9 May 2024 19:21:32 +0100 Subject: [PATCH 01/66] EES-5093 Set Public Data Processor function app ContentDb connection string setting. --- .../templates/public-api/deploy-stage-template.yml | 8 ++++++++ infrastructure/templates/public-api/main.bicep | 1 + 2 files changed, 9 insertions(+) diff --git a/infrastructure/templates/public-api/deploy-stage-template.yml b/infrastructure/templates/public-api/deploy-stage-template.yml index cb09b5d4133..7354fa929c7 100644 --- a/infrastructure/templates/public-api/deploy-stage-template.yml +++ b/infrastructure/templates/public-api/deploy-stage-template.yml @@ -89,6 +89,14 @@ stages: --settings \ "CoreStorage=@Microsoft.KeyVault(VaultName=$(keyVaultName); SecretName=$(coreStorageConnectionStringSecretKey))" + az webapp config connection-string set \ + --name '$(dataProcessorFunctionAppName)' \ + --resource-group '$(resourceGroupName)' \ + --slot 'staging' \ + --connection-string-type 'SQLAzure' \ + --settings \ + "ContentDb=@Microsoft.KeyVault(VaultName=$(keyVaultName); SecretName=$(dataProcessorContentDbConnectionStringSecretKey))" + az webapp config connection-string set \ --name '$(dataProcessorFunctionAppName)' \ --resource-group '$(resourceGroupName)' \ diff --git a/infrastructure/templates/public-api/main.bicep b/infrastructure/templates/public-api/main.bicep index 0d0df52c7da..bdee54b112a 100644 --- a/infrastructure/templates/public-api/main.bicep +++ b/infrastructure/templates/public-api/main.bicep @@ -337,6 +337,7 @@ module storeCoreStorageConnectionString 'components/keyVaultSecret.bicep' = { } } +output dataProcessorContentDbConnectionStringSecretKey string = 'ees-publicapi-data-processor-connectionstring-contentdb' output dataProcessorPsqlConnectionStringSecretKey string = dataProcessorPsqlConnectionStringSecretKey output coreStorageConnectionStringSecretKey string = coreStorageConnectionStringSecretKey output keyVaultName string = keyVaultName From ee73f3b5f27323bf2e3b12161115106c374a2cd4 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Fri, 10 May 2024 16:33:44 +0100 Subject: [PATCH 02/66] EES-5134 Add public API data sets for seeded release files Signed-off-by: Nicholas Tsim --- .../Commands/SeedDataCommand.cs | 4 +- .../Seeds/DataSetSeed.cs | 46 +++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Commands/SeedDataCommand.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Commands/SeedDataCommand.cs index a7c674a5a36..e4dfe2b2d99 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Commands/SeedDataCommand.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Commands/SeedDataCommand.cs @@ -49,9 +49,11 @@ public async ValueTask ExecuteAsync(IConsole console) var dataSetSeeds = new List { + DataSetSeed.AbsenceByCharacteristic2016, DataSetSeed.AbsenceRatesCharacteristic, DataSetSeed.AbsenceRatesGeographicLevel, DataSetSeed.AbsenceRatesGeographicLevelSchool, + DataSetSeed.ExclusionsByGeographicLevel, DataSetSeed.SpcEthnicityLanguage, DataSetSeed.SpcYearGroupGender, DataSetSeed.Nat01, @@ -309,7 +311,7 @@ ORDER BY time_period VersionMinor = 0, Status = DataSetVersionStatus.Published, Notes = string.Empty, - ReleaseFileId = Guid.NewGuid(), + ReleaseFileId = _seed.ReleaseFileId ?? Guid.NewGuid(), DataSetId = _seed.DataSet.Id, TotalResults = totalResults, MetaSummary = new DataSetVersionMetaSummary diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Seeds/DataSetSeed.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Seeds/DataSetSeed.cs index 740f5ede6e8..0c7037bf570 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Seeds/DataSetSeed.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts/Seeds/DataSetSeed.cs @@ -2,10 +2,11 @@ namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Scripts.Seeds; -public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Guid DataSetVersionId) +public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetVersionId, Guid? ReleaseFileId = null) { private static readonly Guid SpcPublicationId = new("a91d9e05-be82-474c-85ae-4913158406d0"); - private static readonly Guid PupilAbsencePublicationId = new("cbbd299f-8297-44bc-92ac-558bcf51f8ad"); + private static readonly Guid ExclusionsPublicationId = new("346fd6f2-3938-4006-9867-08dc1c5c66c3"); + private static readonly Guid PupilAbsencePublicationId = new("d40523f4-50ba-4896-9866-08dc1c5c66c3"); private static readonly Guid _16To18PerformancePublicationId = new("cbbd299f-8297-44bc-92ac-558bcf51f8ad"); public static DataSetSeed SpcEthnicityLanguage => new( @@ -22,7 +23,6 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-06-01T12:00:00+00:00"), Updated = DateTimeOffset.Parse("2023-06-15T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-06fc-7b7c-99d7-0849ec885917"), DataSetVersionId: new Guid("018c7db8-d7bb-77e8-8ee7-147a705c1e3e") ); @@ -40,10 +40,26 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-06-02T12:00:00+00:00"), Updated = DateTimeOffset.Parse("2023-06-16T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-2791-7209-bfcd-40cfb0addd00"), DataSetVersionId: new Guid("018c7db9-3418-7b58-aa76-6a55d0d7b146") ); + public static DataSetSeed AbsenceByCharacteristic2016 => new( + Filename: nameof(AbsenceByCharacteristic2016), + DataSet: new DataSet + { + Id = new Guid("018f6304-42aa-7731-af09-acfa105c6fca"), + Title = "Absence by characteristic", + Summary = "Absence by characteristic data guidance content", + Status = DataSetStatus.Published, + PublicationId = PupilAbsencePublicationId, + Published = DateTimeOffset.Parse("2024-01-25T09:30:00+00:00"), + Created = DateTimeOffset.Parse("2024-01-24T12:00+00:00"), + Updated = DateTimeOffset.Parse("2024-01-24T09:30:00+00:00"), + }, + DataSetVersionId: new Guid("018f6306-cb44-7fcd-80cc-bfdb9e1ce5a1"), + ReleaseFileId: new Guid("41f18583-3b28-4399-c082-08dc1c5c7ea7") + ); + public static DataSetSeed AbsenceRatesCharacteristic => new( Filename: nameof(AbsenceRatesCharacteristic), DataSet: new DataSet @@ -58,7 +74,6 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-08-15T12:00+00:00"), Updated = DateTimeOffset.Parse("2023-09-01T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-8a93-7bb6-82b0-9d53fcf028a3"), DataSetVersionId: new Guid("018c7db9-6a8f-77e1-a789-2eb0f071ef33") ); @@ -76,7 +91,6 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-08-16T12:00+00:00"), Updated = DateTimeOffset.Parse("2023-09-02T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-a962-7124-af9e-b71ad621b89e"), DataSetVersionId: new Guid("018c7db9-c6e5-70b6-869f-2b177b8ad324") ); @@ -94,10 +108,26 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-08-17T12:00+00:00"), Updated = DateTimeOffset.Parse("2023-09-03T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-bc77-7b52-b8b8-c22a5b14c953"), DataSetVersionId: new Guid("018c7db9-f885-7897-af0a-8f62ac55d0f4") ); + public static DataSetSeed ExclusionsByGeographicLevel => new( + Filename: nameof(ExclusionsByGeographicLevel), + DataSet: new DataSet + { + Id = new Guid("018f630d-f1f7-7268-8af3-7e8eba892947"), + Title = "Exclusions by geographic level", + Summary = "Exclusions by geographic level data guidance content", + Status = DataSetStatus.Published, + PublicationId = ExclusionsPublicationId, + Published = DateTimeOffset.Parse("2024-01-25T09:30:00+00:00"), + Created = DateTimeOffset.Parse("2024-01-24T12:00+00:00"), + Updated = DateTimeOffset.Parse("2024-01-24T09:30:00+00:00"), + }, + DataSetVersionId: new Guid("018f630e-bd6c-7ea5-94e6-d12c361ef2b8"), + ReleaseFileId: new Guid("7d602917-2c16-4745-c086-08dc1c5c7ea7") + ); + public static DataSetSeed Qua01 => new( Filename: nameof(Qua01), DataSet: new DataSet @@ -112,7 +142,6 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-11-01T09:30:00+00:00"), Updated = DateTimeOffset.Parse("2023-12-01T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-d84f-7fb4-8979-d214d3270c46"), DataSetVersionId: new Guid("018c7dba-1821-747b-9a7c-c91169543a81") ); @@ -130,7 +159,6 @@ public record DataSetSeed(string Filename, DataSet DataSet, Guid DataSetMetaId, Created = DateTimeOffset.Parse("2023-11-02T09:30:00+00:00"), Updated = DateTimeOffset.Parse("2023-12-02T09:30:00+00:00"), }, - DataSetMetaId: new Guid("018c8da0-ed73-7b18-b10b-c6407c63e96a"), DataSetVersionId: new Guid("018c7dba-582f-7a22-8e01-67e0a6b43603") ); } From 27ea05e075de0e686e9a82cb6cef1fc66f6f1bd8 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Sat, 11 May 2024 11:18:49 +0100 Subject: [PATCH 03/66] EES-5135 Move `ApiDataSetViewModel` into correct file --- ...aSetCandidateViewModels.cs => ApiDataSetCandidateViewModel.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/{ReleaseApiDataSetCandidateViewModels.cs => ApiDataSetCandidateViewModel.cs} (100%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/ReleaseApiDataSetCandidateViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/ApiDataSetCandidateViewModel.cs similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/ReleaseApiDataSetCandidateViewModels.cs rename to src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/ApiDataSetCandidateViewModel.cs From a5eb87dda0965bef50a5ab3b46b0a430f774b3b7 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Sat, 11 May 2024 11:16:54 +0100 Subject: [PATCH 04/66] EES-5135 Move `DataSetVersion` view models into separate files --- .../Public.Data/DataSetVersionViewModels.cs | 43 ++++++ .../Public.Data/DataSetViewModels.cs | 36 ----- .../ViewModels/DataSetVersionViewModels.cs | 125 ++++++++++++++++++ .../ViewModels/DataSetViewModels.cs | 122 +---------------- 4 files changed, 169 insertions(+), 157 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetVersionViewModels.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs new file mode 100644 index 00000000000..42f207fc1a6 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs @@ -0,0 +1,43 @@ +#nullable enable +using System; +using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Public.Data; + +public record DataSetVersionViewModel +{ + public required Guid Id { get; init; } + + public required string Version { get; init; } + + [JsonConverter(typeof(StringEnumConverter))] + public required DataSetVersionStatus Status { get; init; } + + [JsonConverter(typeof(StringEnumConverter))] + public required DataSetVersionType Type { get; init; } +} + +public record DataSetLiveVersionViewModel : DataSetVersionViewModel +{ + public required DateTimeOffset Published { get; init; } +} + +public record DataSetVersionSummaryViewModel +{ + public required Guid Id { get; init; } + + public required string Version { get; init; } + + [JsonConverter(typeof(StringEnumConverter))] + public required DataSetVersionStatus Status { get; init; } + + [JsonConverter(typeof(StringEnumConverter))] + public required DataSetVersionType Type { get; init; } + + public required Guid DataSetFileId { get; init; } + + public required IdTitleViewModel ReleaseVersion { get; init; } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs index 7f834621bb2..de059267199 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs @@ -1,6 +1,5 @@ #nullable enable using System; -using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -25,24 +24,6 @@ public record DataSetViewModel public Guid? SupersedingDataSetId { get; init; } } -public record DataSetVersionViewModel -{ - public required Guid Id { get; init; } - - public required string Version { get; init; } - - [JsonConverter(typeof(StringEnumConverter))] - public required DataSetVersionStatus Status { get; init; } - - [JsonConverter(typeof(StringEnumConverter))] - public required DataSetVersionType Type { get; init; } -} - -public record DataSetLiveVersionViewModel : DataSetVersionViewModel -{ - public required DateTimeOffset Published { get; init; } -} - public record DataSetSummaryViewModel { public required Guid Id { get; init; } @@ -58,20 +39,3 @@ public record DataSetSummaryViewModel public required DataSetVersionSummaryViewModel? LatestLiveVersion { get; init; } } - -public record DataSetVersionSummaryViewModel -{ - public required Guid Id { get; init; } - - public required string Version { get; init; } - - [JsonConverter(typeof(StringEnumConverter))] - public required DataSetVersionStatus Status { get; init; } - - [JsonConverter(typeof(StringEnumConverter))] - public required DataSetVersionType Type { get; init; } - - public required Guid DataSetFileId { get; init; } - - public required IdTitleViewModel ReleaseVersion { get; init; } -} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetVersionViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetVersionViewModels.cs new file mode 100644 index 00000000000..1b0d9cd6d61 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetVersionViewModels.cs @@ -0,0 +1,125 @@ +using System.Text.Json.Serialization; +using GovUk.Education.ExploreEducationStatistics.Common.Converters.SystemJson; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.ViewModels; + +public class DataSetVersionViewModel +{ + /// + /// The version number. Follows semantic versioning e.g. 2.0 (major), 1.1 (minor). + /// + public required string Version { get; init; } + + /// + /// The version type. Can be one of the following: + /// + /// - `Major` - backwards incompatible changes are being introduced + /// - `Minor` - backwards compatible changes are being introduced + /// + /// Major versions typically indicate that some action may be required + /// to ensure code that consumes the data set continues to work. + /// + /// Minor versions should not cause issues in the functionality of existing code. + /// + public required DataSetVersionType Type { get; init; } + + /// + /// The version’s status. Can be one of the following: + /// + /// - `Published` - the version is published and can be used + /// - `Deprecated` - the version is being deprecated and will not be usable in the future + /// - `Withdrawn` - the version has been withdrawn and can no longer be used + /// + public required DataSetVersionStatus Status { get; init; } + + /// + /// When the version was published. + /// + public required DateTimeOffset Published { get; init; } + + /// + /// When the version was withdrawn. + /// + public DateTimeOffset? Withdrawn { get; init; } + + /// + /// Any notes about this version and its changes. + /// + public required string Notes { get; init; } + + /// + /// The total number of results available to query in the data set. + /// + public required long TotalResults { get; init; } + + /// + /// The time period range covered by the data set. + /// + public required TimePeriodRangeViewModel TimePeriods { get; init; } + + /// + /// The geographic levels available in the data set. + /// + [JsonConverter(typeof(ListJsonConverter>))] + public required IReadOnlyList GeographicLevels { get; init; } + + /// + /// The filters available in the data set. + /// + public required IReadOnlyList Filters { get; init; } + + /// + /// The indicators available in the data set. + /// + public required IReadOnlyList Indicators { get; init; } +} + +/// +/// A paginated list of data set versions. +/// +public record DataSetVersionPaginatedListViewModel : PaginatedListViewModel; + +/// +/// Provides high-level information about the latest version of a data set. +/// +public record DataSetLatestVersionViewModel +{ + /// + /// The version number. Follows semantic versioning e.g. 2.0 (major), 1.1 (minor). + /// + public required string Version { get; init; } + + /// + /// When the version was published. + /// + public required DateTimeOffset Published { get; init; } + + /// + /// The total number of results available to query in the data set. + /// + public required long TotalResults { get; init; } + + /// + /// The time period range covered by the data set. + /// + public required TimePeriodRangeViewModel TimePeriods { get; init; } + + /// + /// The geographic levels available in the data set. + /// + [JsonConverter(typeof(ListJsonConverter>))] + public required IReadOnlyList GeographicLevels { get; init; } + + /// + /// The filters available in the data set. + /// + public required IReadOnlyList Filters { get; init; } + + /// + /// The indicators available in the data set. + /// + public required IReadOnlyList Indicators { get; init; } +} + diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetViewModels.cs index 256788b7544..96bb8923ec2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetViewModels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/ViewModels/DataSetViewModels.cs @@ -1,7 +1,4 @@ -using GovUk.Education.ExploreEducationStatistics.Common.Converters.SystemJson; -using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; -using System.Text.Json.Serialization; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.ViewModels; @@ -29,7 +26,7 @@ public record DataSetViewModel /// The status of the data set. Can be one of the following: /// /// - `Published` - the data set has been published and will receive updates - /// - `Deprecated` - the data set is being discontinued and will no receive updates + /// - `Deprecated` - the data set is being discontinued and will no longer receive updates /// - `Withdrawn` - the data set has been withdrawn and can no longer be used /// public required DataSetStatus Status { get; init; } @@ -45,125 +42,8 @@ public record DataSetViewModel public Guid? SupersedingDataSetId { get; init; } } -/// -/// Provides high-level information about the latest version of a data set. -/// -public record DataSetLatestVersionViewModel -{ - /// - /// The version number. Follows semantic versioning e.g. 2.0 (major), 1.1 (minor). - /// - public required string Version { get; init; } - - /// - /// When the version was published. - /// - public required DateTimeOffset Published { get; init; } - - /// - /// The total number of results available to query in the data set. - /// - public required long TotalResults { get; init; } - - /// - /// The time period range covered by the data set. - /// - public required TimePeriodRangeViewModel TimePeriods { get; init; } - - /// - /// The geographic levels available in the data set. - /// - [JsonConverter(typeof(ListJsonConverter>))] - public required IReadOnlyList GeographicLevels { get; init; } - - /// - /// The filters available in the data set. - /// - public required IReadOnlyList Filters { get; init; } - - /// - /// The indicators available in the data set. - /// - public required IReadOnlyList Indicators { get; init; } -} - /// /// A paginated list of data sets. /// public record DataSetPaginatedListViewModel : PaginatedListViewModel; -public class DataSetVersionViewModel -{ - /// - /// The version number. Follows semantic versioning e.g. 2.0 (major), 1.1 (minor). - /// - public required string Version { get; init; } - - /// - /// The version type. Can be one of the following: - /// - /// - `Major` - backwards incompatible changes are being introduced - /// - `Minor` - backwards compatible changes are being introduced - /// - /// Major versions typically indicate that some action may be required - /// to ensure code that consumes the data set continues to work. - /// - /// Minor versions should not cause issues in the functionality of existing code. - /// - public required DataSetVersionType Type { get; init; } - - /// - /// The version’s status. Can be one of the following: - /// - /// - `Published` - the version is published and can be used - /// - `Deprecated` - the version is being deprecated and will not be usable in the future - /// - `Withdrawn` - the version has been withdrawn and can no longer be used - /// - public required DataSetVersionStatus Status { get; init; } - - /// - /// When the version was published. - /// - public required DateTimeOffset Published { get; init; } - - /// - /// When the version was withdrawn. - /// - public DateTimeOffset? Withdrawn { get; init; } - - /// - /// Any notes about this version and its changes. - /// - public required string Notes { get; init; } - - /// - /// The total number of results available to query in the data set. - /// - public required long TotalResults { get; init; } - - /// - /// The time period range covered by the data set. - /// - public required TimePeriodRangeViewModel TimePeriods { get; init; } - - /// - /// The geographic levels available in the data set. - /// - [JsonConverter(typeof(ListJsonConverter>))] - public required IReadOnlyList GeographicLevels { get; init; } - - /// - /// The filters available in the data set. - /// - public required IReadOnlyList Filters { get; init; } - - /// - /// The indicators available in the data set. - /// - public required IReadOnlyList Indicators { get; init; } -} - -/// -/// A paginated list of data set versions. -/// -public record DataSetVersionPaginatedListViewModel : PaginatedListViewModel; From 948ec9e999ce8bb5acf543c735712cf0c8f4707f Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Sat, 11 May 2024 12:39:00 +0100 Subject: [PATCH 05/66] EES-5135 Add meta summaries to data set version view models and various fixes This change also fixes incorrect naming of: - `DataSetViewModel` <-> `DataSetSummaryViewModel` - `DataSetVersionViewModel` <-> `DataSetVersionSummaryViewModel` These have been essentially flipped around as the `ListDataSets` endpoint should provide the summary and not the other way round. --- .../Public.Data/DataSetsControllerTests.cs | 350 ++++++++---------- .../Api/Public.Data/DataSetsController.cs | 4 +- .../Interfaces/Public.Data/IDataSetService.cs | 4 +- .../Services/Public.Data/DataSetService.cs | 99 +++-- .../Public.Data/DataSetVersionViewModels.cs | 32 +- .../Public.Data/DataSetViewModels.cs | 8 +- .../Public.Data/TimePeriodRangeViewModel.cs | 21 ++ 7 files changed, 280 insertions(+), 238 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/TimePeriodRangeViewModel.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs index c5badec106c..3147f92942c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs @@ -30,8 +30,7 @@ public class ListDataSetsTests(TestApplicationFactory testApp) : DataSetsControl [Fact] public async Task PublicationHasSingleDataSet_Success_CorrectViewModel() { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); DataSet dataSet = DataFixture .DefaultDataSet() @@ -42,20 +41,14 @@ public async Task PublicationHasSingleDataSet_Success_CorrectViewModel() await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion draftDataSetVersion = DataFixture - .DefaultDataSetVersion(filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithVersionNumber(1, 1) .WithStatusDraft() .WithDataSet(dataSet) .FinishWith(dsv => dataSet.LatestDraftVersion = dsv); DataSetVersion liveDataSetVersion = DataFixture - .DefaultDataSetVersion(filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusPublished() .WithDataSet(dataSet) .FinishWith(dsv => dataSet.LatestLiveVersion = dsv); @@ -68,7 +61,7 @@ await TestApp.AddTestData(context => var response = await ListPublicationDataSets(publication.Id); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasExpectedPagingAndResultCount(expectedTotalResults: 1); @@ -105,8 +98,7 @@ public async Task PublicationHasMultipleDataSets_Success_CorrectPaging( int pageSize, int numberOfAvailableDataSets) { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); var dataSets = DataFixture .DefaultDataSet() @@ -118,17 +110,12 @@ public async Task PublicationHasMultipleDataSets_Success_CorrectPaging( await TestApp.AddTestData(context => context.DataSets.AddRange(dataSets)); var dataSetVersions = dataSets - .Select(ds => - DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) - .WithStatusPublished() - .WithDataSet(ds) - .FinishWith(dsv => ds.LatestLiveVersion = dsv) - .Generate()) + .Select(ds => DataFixture + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) + .WithStatusPublished() + .WithDataSet(ds) + .FinishWith(dsv => ds.LatestLiveVersion = dsv) + .Generate()) .ToList(); await TestApp.AddTestData(context => @@ -146,7 +133,7 @@ await TestApp.AddTestData(context => var response = await ListPublicationDataSets(publication.Id, page: page, pageSize: pageSize); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasExpectedPagingAndResultCount(expectedTotalResults: numberOfAvailableDataSets, expectedPage: page, @@ -159,8 +146,7 @@ await TestApp.AddTestData(context => [Fact] public async Task PublicationHasMultipleDataSets_Success_CorrectOrdering() { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); var dataSets = DataFixture .DefaultDataSet() @@ -174,11 +160,7 @@ public async Task PublicationHasMultipleDataSets_Success_CorrectOrdering() await TestApp.AddTestData(context => context.DataSets.AddRange(dataSets)); var dataSetVersions = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .ForIndex(0, s => { // Associate data set 0 with a live version published 3 days ago @@ -247,7 +229,7 @@ await TestApp.AddTestData(context => var response = await ListPublicationDataSets(publication.Id); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasExpectedPagingAndResultCount(expectedTotalResults: dataSets.Count); @@ -272,8 +254,7 @@ await TestApp.AddTestData(context => public async Task PublicationHasSingleDataSetWithoutLiveVersion_LatestLiveVersionIsEmpty( DataSetVersionStatus dataSetVersionStatus) { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); DataSet dataSet = DataFixture .DefaultDataSet() @@ -284,10 +265,7 @@ public async Task PublicationHasSingleDataSetWithoutLiveVersion_LatestLiveVersio await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion dataSetVersion = DataFixture - .DefaultDataSetVersion(filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatus(dataSetVersionStatus) .WithDataSet(dataSet) .FinishWith(dsv => dataSet.LatestDraftVersion = dsv); @@ -300,7 +278,7 @@ await TestApp.AddTestData(context => var response = await ListPublicationDataSets(publication.Id); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasExpectedPagingAndResultCount(expectedTotalResults: 1); @@ -315,8 +293,7 @@ await TestApp.AddTestData(context => public async Task PublicationHasSingleDataSetWithoutDraftVersion_DraftVersionIsEmpty( DataSetVersionStatus dataSetVersionStatus) { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); DataSet dataSet = DataFixture .DefaultDataSet() @@ -327,10 +304,7 @@ public async Task PublicationHasSingleDataSetWithoutDraftVersion_DraftVersionIsE await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion dataSetVersion = DataFixture - .DefaultDataSetVersion(filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithPublished(DateTimeOffset.UtcNow) .WithStatus(dataSetVersionStatus) .WithDataSet(dataSet) @@ -344,7 +318,7 @@ await TestApp.AddTestData(context => var response = await ListPublicationDataSets(publication.Id); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasExpectedPagingAndResultCount(expectedTotalResults: 1); @@ -356,14 +330,13 @@ await TestApp.AddTestData(context => [Fact] public async Task PublicationHasNoDataSets_ReturnsEmpty() { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); await TestApp.AddTestData(context => context.Publications.Add(publication)); var response = await ListPublicationDataSets(publication.Id); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasPagingConsistentWithEmptyResults(); } @@ -381,8 +354,7 @@ public async Task NoPublication_ReturnsNotFound() [Fact] public async Task UserHasNoAccessToPublication_ReturnsForbidden() { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); await TestApp.AddTestData(context => context.Publications.Add(publication)); @@ -414,14 +386,13 @@ public async Task PageBelowMinimumThreshold_ReturnsValidationError(int page) [InlineData(9999)] public async Task PageAboveMinimumThreshold_Success(int page) { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); await TestApp.AddTestData(context => context.Publications.Add(publication)); var response = await ListPublicationDataSets(publication.Id, page: page); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasPagingConsistentWithEmptyResults(expectedPage: page); } @@ -446,14 +417,13 @@ public async Task PageSizeOutsideAllowedRange_ReturnsValidationError(int pageSiz [InlineData(100)] public async Task PageSizeInAllowedRange_Success(int pageSize) { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); await TestApp.AddTestData(context => context.Publications.Add(publication)); var response = await ListPublicationDataSets(publication.Id, pageSize: pageSize); - var pagedResult = response.AssertOk>(); + var pagedResult = response.AssertOk>(); pagedResult.AssertHasPagingConsistentWithEmptyResults(expectedPageSize: pageSize); } @@ -500,24 +470,18 @@ public async Task Success() .Generate(1) ); - File liveFile = DataFixture - .DefaultFile(); - - File draftFile = DataFixture - .DefaultFile(); - var liveReleaseVersion = publication.ReleaseVersions.Single(rv => rv.Published is not null); var draftReleaseVersion = publication.ReleaseVersions.Single(rv => rv.Published is null); ReleaseFile liveReleaseFile = DataFixture .DefaultReleaseFile() - .WithFile(liveFile) + .WithFile(DataFixture.DefaultFile()) .WithReleaseVersion(liveReleaseVersion); ReleaseFile draftReleaseFile = DataFixture .DefaultReleaseFile() - .WithFile(draftFile) + .WithFile(DataFixture.DefaultFile()) .WithReleaseVersion(draftReleaseVersion); await TestApp.AddTestData(context => @@ -534,24 +498,18 @@ await TestApp.AddTestData(context => await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion liveDataSetVersion = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusPublished() + .WithTotalResults(5000) .WithReleaseFileId(liveReleaseFile.Id) .WithDataSet(dataSet) .FinishWith(dsv => dsv.DataSet.LatestLiveVersion = dsv); DataSetVersion draftDataSetVersion = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusDraft() .WithVersionNumber(1, 1) + .WithTotalResults(6000) .WithReleaseFileId(draftReleaseFile.Id) .WithDataSet(dataSet) .FinishWith(dsv => dsv.DataSet.LatestDraftVersion = dsv); @@ -564,31 +522,62 @@ await TestApp.AddTestData(context => var response = await GetDataSet(dataSet.Id); - var content = response.AssertOk(); - - Assert.NotNull(content); - Assert.Equal(dataSet.Id, content.Id); - Assert.Equal(dataSet.Title, content.Title); - Assert.Equal(dataSet.Summary, content.Summary); - Assert.Equal(dataSet.Status, content.Status); - Assert.Equal(liveDataSetVersion.Id, content.LatestLiveVersion!.Id); - Assert.Equal(liveDataSetVersion.Version, content.LatestLiveVersion.Version); - Assert.Equal(liveDataSetVersion.Status, content.LatestLiveVersion.Status); - Assert.Equal(liveDataSetVersion.VersionType, content.LatestLiveVersion.Type); - Assert.Equal(liveFile.DataSetFileId, content.LatestLiveVersion.DataSetFileId); - Assert.Equal(liveReleaseVersion.Id, content.LatestLiveVersion.ReleaseVersion.Id); - Assert.Equal(liveReleaseVersion.Title, content.LatestLiveVersion.ReleaseVersion.Title); - Assert.Equal(draftDataSetVersion.Id, content.DraftVersion!.Id); - Assert.Equal(draftDataSetVersion.Version, content.DraftVersion.Version); - Assert.Equal(draftDataSetVersion.Status, content.DraftVersion.Status); - Assert.Equal(draftDataSetVersion.VersionType, content.DraftVersion.Type); - Assert.Equal(draftFile.DataSetFileId, content.DraftVersion.DataSetFileId); - Assert.Equal(draftReleaseVersion.Id, content.DraftVersion.ReleaseVersion.Id); - Assert.Equal(draftReleaseVersion.Title, content.DraftVersion.ReleaseVersion.Title); + var viewModel = response.AssertOk(); + + Assert.Equal(dataSet.Id, viewModel.Id); + Assert.Equal(dataSet.Title, viewModel.Title); + Assert.Equal(dataSet.Summary, viewModel.Summary); + Assert.Equal(dataSet.Status, viewModel.Status); + + Assert.Equal(liveDataSetVersion.Id, viewModel.LatestLiveVersion!.Id); + Assert.Equal(liveDataSetVersion.Version, viewModel.LatestLiveVersion.Version); + Assert.Equal(liveDataSetVersion.Status, viewModel.LatestLiveVersion.Status); + Assert.Equal(liveDataSetVersion.VersionType, viewModel.LatestLiveVersion.Type); + Assert.Equal(liveDataSetVersion.TotalResults, viewModel.LatestLiveVersion.TotalResults); + Assert.Equal(liveDataSetVersion.Published.TruncateNanoseconds(), viewModel.LatestLiveVersion.Published); + Assert.Equal(liveReleaseFile.File.DataSetFileId, viewModel.LatestLiveVersion.DataSetFileId); + + Assert.Equal( + liveDataSetVersion.MetaSummary!.GeographicLevels.Select(l => l.GetEnumLabel()), + viewModel.LatestLiveVersion.GeographicLevels); + Assert.Equal( + liveDataSetVersion.MetaSummary!.Filters, + viewModel.LatestLiveVersion.Filters); + Assert.Equal( + TimePeriodRangeViewModel.Create(liveDataSetVersion.MetaSummary!.TimePeriodRange), + viewModel.LatestLiveVersion.TimePeriods); + Assert.Equal( + liveDataSetVersion.MetaSummary!.Indicators, + viewModel.LatestLiveVersion.Indicators); + + Assert.Equal(liveReleaseVersion.Id, viewModel.LatestLiveVersion.ReleaseVersion.Id); + Assert.Equal(liveReleaseVersion.Title, viewModel.LatestLiveVersion.ReleaseVersion.Title); + + Assert.Equal(draftDataSetVersion.Id, viewModel.DraftVersion!.Id); + Assert.Equal(draftDataSetVersion.Version, viewModel.DraftVersion.Version); + Assert.Equal(draftDataSetVersion.Status, viewModel.DraftVersion.Status); + Assert.Equal(draftDataSetVersion.VersionType, viewModel.DraftVersion.Type); + Assert.Equal(draftDataSetVersion.TotalResults, viewModel.DraftVersion.TotalResults); + Assert.Equal(draftReleaseFile.File.DataSetFileId, viewModel.DraftVersion.DataSetFileId); + + Assert.Equal(draftReleaseVersion.Id, viewModel.DraftVersion.ReleaseVersion.Id); + Assert.Equal(draftReleaseVersion.Title, viewModel.DraftVersion.ReleaseVersion.Title); + Assert.Equal( + draftDataSetVersion.MetaSummary!.GeographicLevels.Select(l => l.GetEnumLabel()), + viewModel.DraftVersion.GeographicLevels); + Assert.Equal( + draftDataSetVersion.MetaSummary!.Filters, + viewModel.DraftVersion.Filters); + Assert.Equal( + TimePeriodRangeViewModel.Create(draftDataSetVersion.MetaSummary!.TimePeriodRange), + viewModel.DraftVersion.TimePeriods); + Assert.Equal( + draftDataSetVersion.MetaSummary!.Indicators, + viewModel.DraftVersion.Indicators); } [Fact] - public async Task ExistsReleaseFilesWithSameFileId_Returns200_CorrectViewModel() + public async Task ReleaseFilesWithSameFileId_Returns200_SameDataSetFileId() { Publication publication = DataFixture .DefaultPublication() @@ -598,8 +587,7 @@ public async Task ExistsReleaseFilesWithSameFileId_Returns200_CorrectViewModel() .Generate(1) ); - File file = DataFixture - .DefaultFile(); + File file = DataFixture.DefaultFile(); var liveReleaseVersion = publication.ReleaseVersions.Single(rv => rv.Published is not null); @@ -629,22 +617,14 @@ await TestApp.AddTestData(context => await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion liveDataSetVersion = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusPublished() .WithReleaseFileId(liveReleaseFile.Id) .WithDataSet(dataSet) .FinishWith(dsv => dsv.DataSet.LatestLiveVersion = dsv); DataSetVersion draftDataSetVersion = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusDraft() .WithVersionNumber(1, 1) .WithReleaseFileId(draftReleaseFile.Id) @@ -659,27 +639,12 @@ await TestApp.AddTestData(context => var response = await GetDataSet(dataSet.Id); - var content = response.AssertOk(); - - Assert.NotNull(content); - Assert.Equal(dataSet.Id, content.Id); - Assert.Equal(dataSet.Title, content.Title); - Assert.Equal(dataSet.Summary, content.Summary); - Assert.Equal(dataSet.Status, content.Status); - Assert.Equal(liveDataSetVersion.Id, content.LatestLiveVersion!.Id); - Assert.Equal(liveDataSetVersion.Version, content.LatestLiveVersion.Version); - Assert.Equal(liveDataSetVersion.Status, content.LatestLiveVersion.Status); - Assert.Equal(liveDataSetVersion.VersionType, content.LatestLiveVersion.Type); - Assert.Equal(file.DataSetFileId, content.LatestLiveVersion.DataSetFileId); - Assert.Equal(liveReleaseVersion.Id, content.LatestLiveVersion.ReleaseVersion.Id); - Assert.Equal(liveReleaseVersion.Title, content.LatestLiveVersion.ReleaseVersion.Title); - Assert.Equal(draftDataSetVersion.Id, content.DraftVersion!.Id); - Assert.Equal(draftDataSetVersion.Version, content.DraftVersion.Version); - Assert.Equal(draftDataSetVersion.Status, content.DraftVersion.Status); - Assert.Equal(draftDataSetVersion.VersionType, content.DraftVersion.Type); - Assert.Equal(file.DataSetFileId, content.DraftVersion.DataSetFileId); - Assert.Equal(draftReleaseVersion.Id, content.DraftVersion.ReleaseVersion.Id); - Assert.Equal(draftReleaseVersion.Title, content.DraftVersion.ReleaseVersion.Title); + var viewModel = response.AssertOk(); + + Assert.Equal(dataSet.Id, viewModel.Id); + + Assert.Equal(file.DataSetFileId, viewModel.LatestLiveVersion!.DataSetFileId); + Assert.Equal(file.DataSetFileId, viewModel.DraftVersion!.DataSetFileId); } [Fact] @@ -689,18 +654,15 @@ public async Task RequestedDataSetHasNoDraftVersion_Returns200_NoDraftVersion() .DefaultPublication() .WithReleases( DataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1) + .DefaultRelease(publishedVersions: 1) + .Generate(1) ); - File file = DataFixture - .DefaultFile(); - var releaseVersion = publication.ReleaseVersions.Single(); ReleaseFile releaseFile = DataFixture .DefaultReleaseFile() - .WithFile(file) + .WithFile(DataFixture.DefaultFile()) .WithReleaseVersion(releaseVersion); await TestApp.AddTestData(context => @@ -717,11 +679,7 @@ await TestApp.AddTestData(context => await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion dataSetVersion = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusPublished() .WithReleaseFileId(releaseFile.Id) .WithDataSet(dataSet) @@ -735,21 +693,20 @@ await TestApp.AddTestData(context => var response = await GetDataSet(dataSet.Id); - var content = response.AssertOk(); - - Assert.NotNull(content); - Assert.Equal(dataSet.Id, content.Id); - Assert.Equal(dataSet.Title, content.Title); - Assert.Equal(dataSet.Summary, content.Summary); - Assert.Equal(dataSet.Status, content.Status); - Assert.Equal(dataSetVersion.Id, content.LatestLiveVersion!.Id); - Assert.Equal(dataSetVersion.Version, content.LatestLiveVersion.Version); - Assert.Equal(dataSetVersion.Status, content.LatestLiveVersion.Status); - Assert.Equal(dataSetVersion.VersionType, content.LatestLiveVersion.Type); - Assert.Equal(file.DataSetFileId, content.LatestLiveVersion.DataSetFileId); - Assert.Equal(releaseVersion.Id, content.LatestLiveVersion.ReleaseVersion.Id); - Assert.Equal(releaseVersion.Title, content.LatestLiveVersion.ReleaseVersion.Title); - Assert.Null(content.DraftVersion); + var viewModel = response.AssertOk(); + + Assert.Equal(dataSet.Id, viewModel.Id); + Assert.Equal(dataSet.Title, viewModel.Title); + Assert.Equal(dataSet.Summary, viewModel.Summary); + Assert.Equal(dataSet.Status, viewModel.Status); + + Assert.Equal(dataSetVersion.Id, viewModel.LatestLiveVersion!.Id); + Assert.Equal(dataSetVersion.Version, viewModel.LatestLiveVersion.Version); + Assert.Equal(dataSetVersion.Status, viewModel.LatestLiveVersion.Status); + Assert.Equal(dataSetVersion.VersionType, viewModel.LatestLiveVersion.Type); + Assert.Equal(releaseFile.File.DataSetFileId, viewModel.LatestLiveVersion.DataSetFileId); + + Assert.Null(viewModel.DraftVersion); } [Fact] @@ -759,18 +716,15 @@ public async Task RequestedDataSetHasNoLiveVersion_Returns200_NoLiveVersion() .DefaultPublication() .WithReleases( DataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1) + .DefaultRelease(publishedVersions: 0, draftVersion: true) + .Generate(1) ); - File file = DataFixture - .DefaultFile(); - var releaseVersion = publication.ReleaseVersions.Single(); ReleaseFile releaseFile = DataFixture .DefaultReleaseFile() - .WithFile(file) + .WithFile(DataFixture.DefaultFile()) .WithReleaseVersion(releaseVersion); await TestApp.AddTestData(context => @@ -787,11 +741,7 @@ await TestApp.AddTestData(context => await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); DataSetVersion dataSetVersion = DataFixture - .DefaultDataSetVersion( - filters: 1, - indicators: 1, - locations: 1, - timePeriods: 2) + .DefaultDataSetVersion(filters: 1, indicators: 1, locations: 1, timePeriods: 2) .WithStatusDraft() .WithReleaseFileId(releaseFile.Id) .WithDataSet(dataSet) @@ -805,28 +755,26 @@ await TestApp.AddTestData(context => var response = await GetDataSet(dataSet.Id); - var content = response.AssertOk(); - - Assert.NotNull(content); - Assert.Equal(dataSet.Id, content.Id); - Assert.Equal(dataSet.Title, content.Title); - Assert.Equal(dataSet.Summary, content.Summary); - Assert.Equal(dataSet.Status, content.Status); - Assert.Equal(dataSetVersion.Id, content.DraftVersion!.Id); - Assert.Equal(dataSetVersion.Version, content.DraftVersion.Version); - Assert.Equal(dataSetVersion.Status, content.DraftVersion.Status); - Assert.Equal(dataSetVersion.VersionType, content.DraftVersion.Type); - Assert.Equal(file.DataSetFileId, content.DraftVersion.DataSetFileId); - Assert.Equal(releaseVersion.Id, content.DraftVersion.ReleaseVersion.Id); - Assert.Equal(releaseVersion.Title, content.DraftVersion.ReleaseVersion.Title); - Assert.Null(content.LatestLiveVersion); + var viewModel = response.AssertOk(); + + Assert.Equal(dataSet.Id, viewModel.Id); + Assert.Equal(dataSet.Title, viewModel.Title); + Assert.Equal(dataSet.Summary, viewModel.Summary); + Assert.Equal(dataSet.Status, viewModel.Status); + + Assert.Equal(dataSetVersion.Id, viewModel.DraftVersion!.Id); + Assert.Equal(dataSetVersion.Version, viewModel.DraftVersion.Version); + Assert.Equal(dataSetVersion.Status, viewModel.DraftVersion.Status); + Assert.Equal(dataSetVersion.VersionType, viewModel.DraftVersion.Type); + Assert.Equal(releaseFile.File.DataSetFileId, viewModel.DraftVersion.DataSetFileId); + + Assert.Null(viewModel.LatestLiveVersion); } [Fact] public async Task RequestedDataSetHasNoVersions_Returns200_NoLatestLiveVersionOrDraftVersion() { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); await TestApp.AddTestData(context => context.Publications.Add(publication)); @@ -839,22 +787,21 @@ public async Task RequestedDataSetHasNoVersions_Returns200_NoLatestLiveVersionOr var response = await GetDataSet(dataSet.Id); - var content = response.AssertOk(); + var viewModel = response.AssertOk(); + + Assert.Equal(dataSet.Id, viewModel.Id); + Assert.Equal(dataSet.Title, viewModel.Title); + Assert.Equal(dataSet.Summary, viewModel.Summary); + Assert.Equal(dataSet.Status, viewModel.Status); - Assert.NotNull(content); - Assert.Equal(dataSet.Id, content.Id); - Assert.Equal(dataSet.Title, content.Title); - Assert.Equal(dataSet.Summary, content.Summary); - Assert.Equal(dataSet.Status, content.Status); - Assert.Null(content.DraftVersion); - Assert.Null(content.LatestLiveVersion); + Assert.Null(viewModel.DraftVersion); + Assert.Null(viewModel.LatestLiveVersion); } [Fact] public async Task NoPermissionsToViewPublication_Returns403() { - Publication publication = DataFixture - .DefaultPublication(); + Publication publication = DataFixture.DefaultPublication(); await TestApp.AddTestData(context => context.Publications.Add(publication)); @@ -881,18 +828,15 @@ public async Task DataSetDoesNotExist_Returns404() .DefaultPublication() .WithReleases( DataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1) + .DefaultRelease(publishedVersions: 0, draftVersion: true) + .Generate(1) ); - File file = DataFixture - .DefaultFile(); - var releaseVersion = publication.ReleaseVersions.Single(); ReleaseFile releaseFile = DataFixture .DefaultReleaseFile() - .WithFile(file) + .WithFile(DataFixture.DefaultFile()) .WithReleaseVersion(releaseVersion); await TestApp.AddTestData(context => @@ -913,8 +857,8 @@ public async Task PublicationDoesNotExist_Returns404() .DefaultPublication() .WithReleases( DataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1) + .DefaultRelease(publishedVersions: 0, draftVersion: true) + .Generate(1) ); File file = DataFixture diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs index 3c683f9d546..f4d7986a385 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs @@ -19,7 +19,7 @@ public class DataSetsController(IDataSetService dataSetService) : ControllerBase { [HttpGet] [Produces("application/json")] - public async Task>> ListDataSets( + public async Task>> ListDataSets( [FromQuery] DataSetListRequest request, CancellationToken cancellationToken) { @@ -34,7 +34,7 @@ public async Task>> ListDa [HttpGet("{dataSetId:guid}")] [Produces("application/json")] - public async Task> GetDataSet( + public async Task> GetDataSet( Guid dataSetId, CancellationToken cancellationToken) { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs index 62d8718d388..17309018f04 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs @@ -11,13 +11,13 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.P public interface IDataSetService { - Task>> ListDataSets( + Task>> ListDataSets( int page, int pageSize, Guid publicationId, CancellationToken cancellationToken = default); - Task> GetDataSet( + Task> GetDataSet( Guid dataSetId, CancellationToken cancellationToken = default); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs index a4b94fb5b79..f8866a91d5d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs @@ -26,7 +26,7 @@ public class DataSetService( IUserService userService) : IDataSetService { - public async Task>> ListDataSets( + public async Task>> ListDataSets( int page, int pageSize, Guid publicationId, @@ -50,10 +50,10 @@ public async Task> .Paginate(page: page, pageSize: pageSize) .ToListAsync(cancellationToken: cancellationToken) ) - .Select(MapDataSet) + .Select(MapDataSetSummary) .ToList(); - return new PaginatedListViewModel( + return new PaginatedListViewModel( dataSets, totalResults: await dataSetsQueryable.CountAsync(cancellationToken: cancellationToken), page: page, @@ -61,41 +61,41 @@ public async Task> }); } - public async Task> GetDataSet( + public async Task> GetDataSet( Guid dataSetId, CancellationToken cancellationToken = default) { return await CheckDataSetExists(dataSetId, cancellationToken) .OnSuccessDo(dataSet => CheckPublicationExists(dataSet.PublicationId, cancellationToken) - .OnSuccess(userService.CheckCanViewPublication) + .OnSuccess(userService.CheckCanViewPublication) ) - .OnSuccessCombineWith(async dataSet => await GetReleaseFilesByDataSetVersionId(dataSet, cancellationToken)) - .OnSuccess(combinedDataSetAndReleaseFiles => + .OnSuccess(async dataSet => { - var (dataSet, releaseFilesByDataSetVersionId) = combinedDataSetAndReleaseFiles; + var releaseFilesByDataSetVersionId = + await GetReleaseFilesByDataSetVersionId(dataSet, cancellationToken); return MapDataSet(dataSet, releaseFilesByDataSetVersionId); }); } - private static DataSetViewModel MapDataSet(DataSet dataSet) + private static DataSetSummaryViewModel MapDataSetSummary(DataSet dataSet) { - return new DataSetViewModel + return new DataSetSummaryViewModel { Id = dataSet.Id, Title = dataSet.Title, Summary = dataSet.Summary, Status = dataSet.Status, - DraftVersion = MapDraftVersion(dataSet.LatestDraftVersion), - LatestLiveVersion = MapLiveVersion(dataSet.LatestLiveVersion), SupersedingDataSetId = dataSet.SupersedingDataSetId, + DraftVersion = MapDraftSummaryVersion(dataSet.LatestDraftVersion), + LatestLiveVersion = MapLiveSummaryVersion(dataSet.LatestLiveVersion), }; } - private static DataSetVersionViewModel? MapDraftVersion(DataSetVersion? dataSetVersion) + private static DataSetVersionSummaryViewModel? MapDraftSummaryVersion(DataSetVersion? dataSetVersion) { return dataSetVersion != null - ? new DataSetVersionViewModel + ? new DataSetVersionSummaryViewModel { Id = dataSetVersion.Id, Version = dataSetVersion.Version, @@ -105,10 +105,10 @@ private static DataSetViewModel MapDataSet(DataSet dataSet) : null; } - private static DataSetLiveVersionViewModel? MapLiveVersion(DataSetVersion? dataSetVersion) + private static DataSetLiveVersionSummaryViewModel? MapLiveSummaryVersion(DataSetVersion? dataSetVersion) { return dataSetVersion != null - ? new DataSetLiveVersionViewModel + ? new DataSetLiveVersionSummaryViewModel { Id = dataSetVersion.Id, Version = dataSetVersion.Version, @@ -119,37 +119,80 @@ private static DataSetViewModel MapDataSet(DataSet dataSet) : null; } - private static DataSetSummaryViewModel MapDataSet(DataSet dataSet, IReadOnlyDictionary releaseFilesByDataSetVersionId) + private static DataSetViewModel MapDataSet( + DataSet dataSet, + IReadOnlyDictionary releaseFilesByDataSetVersionId) { var draftVersion = dataSet.LatestDraftVersion is null ? null - : MapDataSetVersion(dataSet.LatestDraftVersion, releaseFilesByDataSetVersionId[dataSet.LatestDraftVersionId!.Value]); + : MapDraftVersion( + dataSet.LatestDraftVersion, + releaseFilesByDataSetVersionId[dataSet.LatestDraftVersionId!.Value] + ); var latestLiveVersion = dataSet.LatestLiveVersion is null ? null - : MapDataSetVersion(dataSet.LatestLiveVersion, releaseFilesByDataSetVersionId[dataSet.LatestLiveVersionId!.Value]); + : MapLiveVersion( + dataSet.LatestLiveVersion, + releaseFilesByDataSetVersionId[dataSet.LatestLiveVersionId!.Value] + ); - return new DataSetSummaryViewModel + return new DataSetViewModel { Id = dataSet.Id, Title = dataSet.Title, Summary = dataSet.Summary, Status = dataSet.Status, + SupersedingDataSetId = dataSet.SupersedingDataSetId, DraftVersion = draftVersion, LatestLiveVersion = latestLiveVersion, }; } - private static DataSetVersionSummaryViewModel MapDataSetVersion(DataSetVersion dataSetVersion, ReleaseFile releaseFile) + private static DataSetVersionViewModel MapDraftVersion( + DataSetVersion dataSetVersion, + ReleaseFile releaseFile) + { + return new DataSetVersionViewModel + { + Id = dataSetVersion.Id, + Version = dataSetVersion.Version, + Status = dataSetVersion.Status, + Type = dataSetVersion.VersionType, + DataSetFileId = releaseFile.File.DataSetFileId!.Value, + ReleaseVersion = MapReleaseVersion(releaseFile.ReleaseVersion), + TotalResults = dataSetVersion.TotalResults, + GeographicLevels = dataSetVersion.MetaSummary?.GeographicLevels + .Select(l => l.GetEnumLabel()) + .ToList() ?? null, + TimePeriods = dataSetVersion.MetaSummary?.TimePeriodRange is not null + ? TimePeriodRangeViewModel.Create(dataSetVersion.MetaSummary.TimePeriodRange) + : null, + Filters = dataSetVersion.MetaSummary?.Filters ?? null, + Indicators = dataSetVersion.MetaSummary?.Indicators ?? null, + }; + } + + private static DataSetLiveVersionViewModel MapLiveVersion( + DataSetVersion dataSetVersion, + ReleaseFile releaseFile) { - return new DataSetVersionSummaryViewModel + return new DataSetLiveVersionViewModel { Id = dataSetVersion.Id, Version = dataSetVersion.Version, Status = dataSetVersion.Status, Type = dataSetVersion.VersionType, DataSetFileId = releaseFile.File.DataSetFileId!.Value, + Published = dataSetVersion.Published!.Value, + TotalResults = dataSetVersion.TotalResults, ReleaseVersion = MapReleaseVersion(releaseFile.ReleaseVersion), + GeographicLevels = dataSetVersion.MetaSummary!.GeographicLevels + .Select(l => l.GetEnumLabel()) + .ToList(), + TimePeriods = TimePeriodRangeViewModel.Create(dataSetVersion.MetaSummary.TimePeriodRange), + Filters = dataSetVersion.MetaSummary.Filters, + Indicators = dataSetVersion.MetaSummary.Indicators, }; } @@ -182,7 +225,7 @@ private async Task> CheckDataSetExists( .SingleOrNotFoundAsync(cancellationToken); } - private async Task>> GetReleaseFilesByDataSetVersionId( + private async Task> GetReleaseFilesByDataSetVersionId( DataSet dataSet, CancellationToken cancellationToken) { @@ -195,12 +238,18 @@ private async Task>> if (dataSet.LatestDraftVersion is not null) { - dataSetVersionIdsByReleaseFileId.Add(dataSet.LatestDraftVersion.ReleaseFileId, dataSet.LatestDraftVersionId!.Value); + dataSetVersionIdsByReleaseFileId.Add( + dataSet.LatestDraftVersion.ReleaseFileId, + dataSet.LatestDraftVersionId!.Value + ); } if (dataSet.LatestLiveVersion is not null) { - dataSetVersionIdsByReleaseFileId.Add(dataSet.LatestLiveVersion.ReleaseFileId, dataSet.LatestLiveVersionId!.Value); + dataSetVersionIdsByReleaseFileId.Add( + dataSet.LatestLiveVersion.ReleaseFileId, + dataSet.LatestLiveVersionId!.Value + ); } return await contentDbContext.ReleaseFiles diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs index 42f207fc1a6..ddb8074bdb0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetVersionViewModels.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; using Newtonsoft.Json; @@ -18,11 +19,35 @@ public record DataSetVersionViewModel [JsonConverter(typeof(StringEnumConverter))] public required DataSetVersionType Type { get; init; } + + public required Guid DataSetFileId { get; init; } + + public required IdTitleViewModel ReleaseVersion { get; init; } + + public long TotalResults { get; init; } + + public TimePeriodRangeViewModel? TimePeriods { get; init; } + + public IReadOnlyList? GeographicLevels { get; init; } + + public IReadOnlyList? Filters { get; init; } + + public IReadOnlyList? Indicators { get; init; } } public record DataSetLiveVersionViewModel : DataSetVersionViewModel { public required DateTimeOffset Published { get; init; } + + public new required long TotalResults { get; init; } + + public new required TimePeriodRangeViewModel TimePeriods { get; init; } + + public new required IReadOnlyList GeographicLevels { get; init; } = []; + + public new required IReadOnlyList Filters { get; init; } = []; + + public new required IReadOnlyList Indicators { get; init; } = []; } public record DataSetVersionSummaryViewModel @@ -36,8 +61,9 @@ public record DataSetVersionSummaryViewModel [JsonConverter(typeof(StringEnumConverter))] public required DataSetVersionType Type { get; init; } +} - public required Guid DataSetFileId { get; init; } - - public required IdTitleViewModel ReleaseVersion { get; init; } +public record DataSetLiveVersionSummaryViewModel : DataSetVersionSummaryViewModel +{ + public required DateTimeOffset Published { get; init; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs index de059267199..a6ab3474aed 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/DataSetViewModels.cs @@ -17,11 +17,11 @@ public record DataSetViewModel [JsonConverter(typeof(StringEnumConverter))] public required DataSetStatus Status { get; init; } + public Guid? SupersedingDataSetId { get; init; } + public required DataSetVersionViewModel? DraftVersion { get; init; } public required DataSetLiveVersionViewModel? LatestLiveVersion { get; init; } - - public Guid? SupersedingDataSetId { get; init; } } public record DataSetSummaryViewModel @@ -35,7 +35,9 @@ public record DataSetSummaryViewModel [JsonConverter(typeof(StringEnumConverter))] public required DataSetStatus Status { get; init; } + public Guid? SupersedingDataSetId { get; init; } + public required DataSetVersionSummaryViewModel? DraftVersion { get; init; } - public required DataSetVersionSummaryViewModel? LatestLiveVersion { get; init; } + public required DataSetLiveVersionSummaryViewModel? LatestLiveVersion { get; init; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/TimePeriodRangeViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/TimePeriodRangeViewModel.cs new file mode 100644 index 00000000000..834415cb8bf --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Public.Data/TimePeriodRangeViewModel.cs @@ -0,0 +1,21 @@ +#nullable enable +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Utils; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Public.Data; + +public record TimePeriodRangeViewModel +{ + public required string Start { get; set; } + + public required string End { get; set; } + + public static TimePeriodRangeViewModel Create(TimePeriodRange timePeriodRange) + { + return new TimePeriodRangeViewModel + { + Start = TimePeriodFormatter.FormatLabel(timePeriodRange.Start.Period, timePeriodRange.Start.Code), + End = TimePeriodFormatter.FormatLabel(timePeriodRange.End.Period, timePeriodRange.End.Code), + }; + } +} From 4eec2ea6bb394f690c9ed2f64b06a52d0dc0c8d1 Mon Sep 17 00:00:00 2001 From: SaicharanMuthyapwar Date: Mon, 13 May 2024 16:41:23 +0100 Subject: [PATCH 06/66] Fixing ui test failure of admin and public tests --- .../playwright-tests/general-public/pages/FindStatisticsPage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts b/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts index e8e7d2860d2..c2dfd577767 100644 --- a/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts +++ b/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts @@ -12,5 +12,6 @@ export default class FindStatisticsPage { async navigateToPublicReleasePage(publicationName: string) { await this.releaseLink(publicationName).click(); + await this.page.waitForTimeout(2000); } } From 50316c7eaad936adeb223036220a2e605bea8822 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Thu, 2 May 2024 00:19:55 +0100 Subject: [PATCH 07/66] EES-5133 Refactor `DataSetControllerGetQueryTests` to run faster --- ....cs => DataSetsControllerGetQueryTests.cs} | 2754 +++++++++-------- 1 file changed, 1402 insertions(+), 1352 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/{DataSetsControllerQueryTests.cs => DataSetsControllerGetQueryTests.cs} (91%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerQueryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs similarity index 91% rename from src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerQueryTests.cs rename to src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs index df2d995db79..ab8bd925952 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerQueryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs @@ -17,23 +17,40 @@ namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Controllers; -public abstract class DataSetsControllerQueryTests(TestApplicationFactory testApp) : IntegrationTestFixture(testApp) +public abstract class DataSetsControllerGetQueryTests(TestApplicationFactory testApp) : IntegrationTestFixture(testApp) { private const string BaseUrl = "api/v1/data-sets"; - private readonly TestParquetPathResolver _parquetPathResolver = new(); + private readonly TestParquetPathResolver _parquetPathResolver = new() + { + Directory = "AbsenceSchool" + }; - public class QueryDataSetsGetTests : DataSetsControllerQueryTests + public class AccessTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) { - public QueryDataSetsGetTests(TestApplicationFactory testApp) : base(testApp) + [Theory] + [InlineData(DataSetVersionStatus.Processing)] + [InlineData(DataSetVersionStatus.Failed)] + [InlineData(DataSetVersionStatus.Draft)] + [InlineData(DataSetVersionStatus.Mapping)] + [InlineData(DataSetVersionStatus.Withdrawn)] + [InlineData(DataSetVersionStatus.Cancelled)] + public async Task VersionNotAvailable_Returns403(DataSetVersionStatus versionStatus) { - _parquetPathResolver.Directory = "AbsenceSchool"; + var dataSetVersion = await SetupDefaultDataSetVersion(versionStatus); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"] + ); + + response.AssertForbidden(); } [Theory] [InlineData(DataSetVersionStatus.Published)] [InlineData(DataSetVersionStatus.Deprecated)] - public async Task VersionAvailable_Returns200_CorrectViewModel(DataSetVersionStatus versionStatus) + public async Task VersionAvailable_Returns200(DataSetVersionStatus versionStatus) { var dataSetVersion = await SetupDefaultDataSetVersion(versionStatus); @@ -51,815 +68,1005 @@ public async Task VersionAvailable_Returns200_CorrectViewModel(DataSetVersionSta Assert.Empty(viewModel.Warnings); Assert.Equal(216, viewModel.Results.Count); + } - var result = viewModel.Results[0]; - - Assert.Equal(2, result.Filters.Count); - Assert.Equal("pTSoj", result.Filters["ncyear"]); - Assert.Equal("0kT5D", result.Filters["school_type"]); - - Assert.Equal(GeographicLevel.LocalAuthority, result.GeographicLevel); - - Assert.Equal(3, result.Locations.Count); - Assert.Equal("dP0Zw", result.Locations[GeographicLevel.LocalAuthority.GetEnumValue()]); - Assert.Equal("pTSoj", result.Locations[GeographicLevel.Country.GetEnumValue()]); - Assert.Equal("it6Xr", result.Locations[GeographicLevel.Region.GetEnumValue()]); - - Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); - Assert.Equal("2022/2023", result.TimePeriod.Period); + [Fact] + public async Task DataSetDoesNotExist_Returns404() + { + var response = await QueryDataSet( + dataSetId: Guid.NewGuid(), + indicators: ["sess_authorised"] + ); - Assert.Single(result.Values); - Assert.Equal("4064499", result.Values["sess_authorised"]); + response.AssertNotFound(); } - [Theory] - [InlineData(1, 50, 5)] - [InlineData(2, 50, 5)] - [InlineData(3, 50, 5)] - [InlineData(1, 150, 2)] - [InlineData(2, 150, 2)] - [InlineData(1, 216, 1)] - public async Task MultiplePages_Returns200_PaginatedCorrectly(int page, int pageSize, int totalPages) + [Fact] + public async Task VersionDoesNotExist_Returns404() { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - page: page, - pageSize: pageSize + dataSetVersion: "2.0", + indicators: ["sess_authorised"] ); - var viewModel = response.AssertOk(useSystemJson: true); - - Assert.Equal(page, viewModel.Paging.Page); - Assert.Equal(pageSize, viewModel.Paging.PageSize); - Assert.Equal(totalPages, viewModel.Paging.TotalPages); - Assert.Equal(216, viewModel.Paging.TotalResults); + response.AssertNotFound(); } + } + public class IndicatorValidationTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { [Fact] - public async Task NoResults_Returns200_HasWarning() + public async Task Empty_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - "locations.eq", "LA|id|9U4vZ" - }, - { - "geographicLevels.eq", "NAT" - } - } - ); + var client = BuildApp().CreateClient(); - var viewModel = response.AssertOk(useSystemJson: true); + var response = await client.GetAsync($"{BaseUrl}/{dataSetVersion.DataSetId}/query?indicators[]="); - Assert.Equal(1, viewModel.Paging.Page); - Assert.Equal(1000, viewModel.Paging.PageSize); - Assert.Equal(1, viewModel.Paging.TotalPages); - Assert.Equal(0, viewModel.Paging.TotalResults); + var validationProblem = response.AssertValidationProblem(); - var warning = Assert.Single(viewModel.Warnings); + Assert.Single(validationProblem.Errors); - Assert.Equal(ValidationMessages.QueryNoResults.Code, warning.Code); - Assert.Equal(ValidationMessages.QueryNoResults.Message, warning.Message); + validationProblem.AssertHasNotEmptyError("indicators"); } [Fact] - public async Task DebugEnabled_Returns200_HasWarning() + public async Task Blank_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - debug: true + indicators: ["", " ", " "] ); - var viewModel = response.AssertOk(useSystemJson: true); + var validationProblem = response.AssertValidationProblem(); - var warning = Assert.Single(viewModel.Warnings); + Assert.Equal(3, validationProblem.Errors.Count); - Assert.Equal(ValidationMessages.DebugEnabled.Code, warning.Code); - Assert.Equal(ValidationMessages.DebugEnabled.Message, warning.Message); + validationProblem.AssertHasNotEmptyError("indicators[0]"); + validationProblem.AssertHasNotEmptyError("indicators[1]"); + validationProblem.AssertHasNotEmptyError("indicators[2]"); } [Fact] - public async Task AllIndicators_Returns200_ResultValuesInAllowedRanges() + public async Task TooLong_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: - [ - "enrolments", - "sess_authorised", - "sess_possible", - "sess_unauthorised", - "sess_unauthorised_percent", - ] + indicators: [new string('a', 41), new string('a', 42)] ); - var viewModel = response.AssertOk(useSystemJson: true); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(216, viewModel.Results.Count); + Assert.Equal(2, validationProblem.Errors.Count); - var values = viewModel.Results - .SelectMany(result => result.Values) - .GroupBy(kv => kv.Key, kv => kv.Value) - .ToDictionary(kv => kv.Key, kv => kv.ToList()); + validationProblem.AssertHasMaximumLengthError("indicators[0]", maxLength: 40); + validationProblem.AssertHasMaximumLengthError("indicators[1]", maxLength: 40); + } - var enrolments = values["enrolments"].Select(int.Parse).ToList(); + [Fact] + public async Task MissingParam_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); - Assert.Equal(216, enrolments.Count); - Assert.Equal(999598, enrolments.Max()); - Assert.Equal(1072, enrolments.Min()); + var client = BuildApp().CreateClient(); - var sessAuthorised = values["sess_authorised"].Select(int.Parse).ToList(); + var response = await client.GetAsync($"{BaseUrl}/{dataSetVersion.DataSetId}/query"); - Assert.Equal(216, sessAuthorised.Count); - Assert.Equal(4967515, sessAuthorised.Max()); - Assert.Equal(22441, sessAuthorised.Min()); + var validationProblem = response.AssertValidationProblem(); - var sessPossible = values["sess_possible"].Select(int.Parse).ToList(); + Assert.Single(validationProblem.Errors); - Assert.Equal(216, sessPossible.Count); - Assert.Equal(9934276, sessPossible.Max()); - Assert.Equal(18306, sessPossible.Min()); + validationProblem.AssertHasNotEmptyError("indicators"); + } - var sessUnauthorised = values["sess_unauthorised"].Select(int.Parse).ToList(); + [Fact] + public async Task NotFound_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); - Assert.Equal(216, sessUnauthorised.Count); - Assert.Equal(494993, sessUnauthorised.Max()); - Assert.Equal(2883, sessUnauthorised.Min()); + string[] notFoundIndicators = ["invalid1", "invalid2", "invalid3"]; - var sessUnauthorisedPercent = values["sess_unauthorised_percent"].Select(float.Parse).ToList(); + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: notFoundIndicators + ); - Assert.Equal(216, sessUnauthorisedPercent.Count); - Assert.Equal(14.8837004f, sessUnauthorisedPercent.Max()); - Assert.Equal(0.241600007f, sessUnauthorisedPercent.Min()); + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasIndicatorsNotFoundError("indicators", notFoundIndicators); } + } - [Fact] - public async Task AllIndicators_Returns200_CorrectResultIds() + public class FiltersValidationTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Theory] + [InlineData("filters.in")] + [InlineData("filters.notIn")] + public async Task Empty_Returns400(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: - [ - "enrolments", - "sess_authorised", - "sess_possible", - "sess_unauthorised", - "sess_unauthorised_percent", - ] + indicators: ["sess_authorised"], + queryParameters: new Dictionary + { + { + $"{path}[]", "" + } + } ); - var viewModel = response.AssertOk(useSystemJson: true); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(216, viewModel.Results.Count); + Assert.Single(validationProblem.Errors); - var meta = GatherQueryResultsMeta(viewModel); + validationProblem.AssertHasNotEmptyError(path); + } - Assert.Equal(3, meta.Filters.Count); + [Theory] + [InlineData("filters.in")] + [InlineData("filters.notIn")] + public async Task InvalidMix_Returns400(string path) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); - Assert.Equal(3, meta.Filters["academy_type"].Count); + string[] invalidFilters = + [ + "", + " ", + " ", + new string('a', 11), + new string('a', 12), + ]; - Assert.Contains("dP0Zw", meta.Filters["academy_type"]); - Assert.Contains("9U4vZ", meta.Filters["academy_type"]); - Assert.Contains("O7CLF", meta.Filters["academy_type"]); + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + queryParameters: new Dictionary + { + { path, invalidFilters } + } + ); - Assert.Equal(4, meta.Filters["ncyear"].Count); - Assert.Contains("IzBzg", meta.Filters["ncyear"]); - Assert.Contains("it6Xr", meta.Filters["ncyear"]); - Assert.Contains("7zXob", meta.Filters["ncyear"]); - Assert.Contains("pTSoj", meta.Filters["ncyear"]); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(3, meta.Filters["school_type"].Count); - Assert.Contains("LxWjE", meta.Filters["school_type"]); - Assert.Contains("6jrfe", meta.Filters["school_type"]); - Assert.Contains("0kT5D", meta.Filters["school_type"]); + Assert.Equal(5, validationProblem.Errors.Count); - Assert.Equal(4, meta.Locations.Count); + validationProblem.AssertHasNotEmptyError($"{path}[0]"); + validationProblem.AssertHasNotEmptyError($"{path}[1]"); + validationProblem.AssertHasNotEmptyError($"{path}[2]"); + validationProblem.AssertHasMaximumLengthError($"{path}[3]", maxLength: 10); + validationProblem.AssertHasMaximumLengthError($"{path}[4]", maxLength: 10); + } - Assert.Single(meta.Locations["NAT"]); - Assert.Contains("pTSoj", meta.Locations["NAT"]); + [Fact] + public async Task AllComparatorsInvalid_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); - Assert.Equal(2, meta.Locations["REG"].Count); - Assert.Contains("it6Xr", meta.Locations["REG"]); - Assert.Contains("IzBzg", meta.Locations["REG"]); + string[] invalidFilters = + [ + new string('a', 11), + "" + ]; - Assert.Equal(4, meta.Locations["LA"].Count); - Assert.Contains("9U4vZ", meta.Locations["LA"]); - Assert.Contains("O7CLF", meta.Locations["LA"]); - Assert.Contains("dP0Zw", meta.Locations["LA"]); - Assert.Contains("7zXob", meta.Locations["LA"]); - - Assert.Equal(8, meta.Locations["SCH"].Count); - Assert.Contains("qFjG7", meta.Locations["SCH"]); - Assert.Contains("0kT5D", meta.Locations["SCH"]); - Assert.Contains("arLPb", meta.Locations["SCH"]); - Assert.Contains("6jrfe", meta.Locations["SCH"]); - Assert.Contains("HTzLj", meta.Locations["SCH"]); - Assert.Contains("LxWjE", meta.Locations["SCH"]); - Assert.Contains("CpId1", meta.Locations["SCH"]); - Assert.Contains("YPHKM", meta.Locations["SCH"]); + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + queryParameters: new Dictionary + { + { + "filters.eq", new string('a', 11) + }, + { + "filters.notEq", new string('a', 12) + }, + { + "filters.in[]", "" + }, + { + "filters.notIn", invalidFilters + }, + } + ); - Assert.Equal(4, meta.GeographicLevels.Count); - Assert.Contains(GeographicLevel.Country, meta.GeographicLevels); - Assert.Contains(GeographicLevel.Region, meta.GeographicLevels); - Assert.Contains(GeographicLevel.LocalAuthority, meta.GeographicLevels); - Assert.Contains(GeographicLevel.School, meta.GeographicLevels); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(3, meta.TimePeriods.Count); - Assert.Contains( - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2020/2021" }, - meta.TimePeriods - ); - Assert.Contains( - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, - meta.TimePeriods - ); - Assert.Contains( - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" }, - meta.TimePeriods - ); + Assert.Equal(5, validationProblem.Errors.Count); - Assert.Equal(5, meta.Indicators.Count); - Assert.Contains("enrolments", meta.Indicators); - Assert.Contains("sess_authorised", meta.Indicators); - Assert.Contains("sess_possible", meta.Indicators); - Assert.Contains("sess_unauthorised", meta.Indicators); - Assert.Contains("sess_unauthorised_percent", meta.Indicators); + validationProblem.AssertHasMaximumLengthError("filters.eq", maxLength: 10); + validationProblem.AssertHasMaximumLengthError("filters.notEq", maxLength: 10); + validationProblem.AssertHasNotEmptyError("filters.in"); + validationProblem.AssertHasMaximumLengthError("filters.notIn[0]", maxLength: 10); + validationProblem.AssertHasNotEmptyError("filters.notIn[1]"); } - [Fact] - public async Task AllIndicators_Returns200_CorrectDebuggedResultLabels() + [Theory] + [InlineData("filters.in")] + [InlineData("filters.notIn")] + public async Task NotFound_Returns200_HasWarning(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); + string[] notFoundFilters = + [ + "invalid", + "9999999" + ]; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: - [ - "enrolments", - "sess_authorised", - "sess_possible", - "sess_unauthorised", - "sess_unauthorised_percent", - ], - debug: true + indicators: ["sess_authorised"], + queryParameters: new Dictionary + { + { + path, new StringValues(["IzBzg", ..notFoundFilters]) + } + } ); var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(216, viewModel.Results.Count); - - var meta = GatherQueryResultsMeta(viewModel); - - Assert.Equal(3, meta.Filters.Count); - - Assert.Equal(3, meta.Filters["academy_type"].Count); - - Assert.Contains("dP0Zw :: Primary sponsor led academy", meta.Filters["academy_type"]); - Assert.Contains("9U4vZ :: Secondary free school", meta.Filters["academy_type"]); - Assert.Contains("O7CLF :: Secondary sponsor led academy", meta.Filters["academy_type"]); - - Assert.Equal(4, meta.Filters["ncyear"].Count); - Assert.Contains("IzBzg :: Year 4", meta.Filters["ncyear"]); - Assert.Contains("it6Xr :: Year 6", meta.Filters["ncyear"]); - Assert.Contains("7zXob :: Year 8", meta.Filters["ncyear"]); - Assert.Contains("pTSoj :: Year 10", meta.Filters["ncyear"]); - - Assert.Equal(3, meta.Filters["school_type"].Count); - Assert.Contains("LxWjE :: State-funded primary", meta.Filters["school_type"]); - Assert.Contains("6jrfe :: State-funded secondary", meta.Filters["school_type"]); - Assert.Contains("0kT5D :: Total", meta.Filters["school_type"]); - - Assert.Equal(4, meta.Locations.Count); - - Assert.Single(meta.Locations["NAT"]); - Assert.Contains("pTSoj :: England (code = E92000001)", meta.Locations["NAT"]); + Assert.Single(viewModel.Warnings); - Assert.Equal(2, meta.Locations["REG"].Count); - Assert.Contains("it6Xr :: Outer London (code = E13000002)", meta.Locations["REG"]); - Assert.Contains("IzBzg :: Yorkshire and The Humber (code = E12000003)", meta.Locations["REG"]); + viewModel.AssertHasFiltersNotFoundWarning(path, notFoundFilters); + } + } - Assert.Equal(4, meta.Locations["LA"].Count); - Assert.Contains("9U4vZ :: Barnet (code = E09000003, oldCode = 302)", meta.Locations["LA"]); - Assert.Contains("O7CLF :: Barnsley (code = E08000016, oldCode = 370)", meta.Locations["LA"]); - Assert.Contains( - "dP0Zw :: Kingston upon Thames / Richmond upon Thames (code = E09000021 / E09000027, oldCode = 314)", - meta.Locations["LA"] - ); - Assert.Contains("7zXob :: Sheffield (code = E08000019, oldCode = 373)", meta.Locations["LA"]); + public class GeographicLevelsValidationTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Theory] + [InlineData("geographicLevels.in")] + [InlineData("geographicLevels.notIn")] + public async Task Empty_Returns400(string path) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); - Assert.Equal(8, meta.Locations["SCH"].Count); - Assert.Contains("qFjG7 :: Colindale Primary School (urn = 101269, laEstab = 3022014)", meta.Locations["SCH"]); - Assert.Contains("0kT5D :: Greenhill Primary School (urn = 145374, laEstab = 3732341)", meta.Locations["SCH"]); - Assert.Contains( - "arLPb :: Hoyland Springwood Primary School (urn = 141973, laEstab = 3702039)", - meta.Locations["SCH"] - ); - Assert.Contains( - "6jrfe :: King Athelstan Primary School (urn = 102579, laEstab = 3142032)", - meta.Locations["SCH"] + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + queryParameters: new Dictionary + { + { + $"{path}[]", "" + } + } ); - Assert.Contains("HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", meta.Locations["SCH"]); - Assert.Contains("LxWjE :: Penistone Grammar School (urn = 106653, laEstab = 3704027)", meta.Locations["SCH"]); - Assert.Contains("CpId1 :: The Kingston Academy (urn = 141862, laEstab = 3144001)", meta.Locations["SCH"]); - Assert.Contains("YPHKM :: Wren Academy Finchley (urn = 135507, laEstab = 3026906)", meta.Locations["SCH"]); - Assert.Equal(4, meta.GeographicLevels.Count); - Assert.Contains(GeographicLevel.Country, meta.GeographicLevels); - Assert.Contains(GeographicLevel.Region, meta.GeographicLevels); - Assert.Contains(GeographicLevel.LocalAuthority, meta.GeographicLevels); - Assert.Contains(GeographicLevel.School, meta.GeographicLevels); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(3, meta.TimePeriods.Count); - Assert.Contains( - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2020/2021" }, - meta.TimePeriods - ); - Assert.Contains( - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, - meta.TimePeriods - ); - Assert.Contains( - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" }, - meta.TimePeriods - ); + Assert.Single(validationProblem.Errors); - Assert.Equal(5, meta.Indicators.Count); - Assert.Contains("enrolments", meta.Indicators); - Assert.Contains("sess_authorised", meta.Indicators); - Assert.Contains("sess_possible", meta.Indicators); - Assert.Contains("sess_unauthorised", meta.Indicators); - Assert.Contains("sess_unauthorised_percent", meta.Indicators); + validationProblem.AssertHasNotEmptyError(path); } [Theory] - [InlineData("filters.eq", 54)] - [InlineData("filters.notEq", 162)] - [InlineData("filters.in", 54)] - [InlineData("filters.notIn", 162)] - public async Task Filters_SingleOption_Returns200(string path, int expectedResults) + [InlineData("geographicLevels.in")] + [InlineData("geographicLevels.notIn")] + public async Task InvalidMix_Returns400(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Year 4 - const string filterOptionId = "IzBzg"; + string[] invalidLevels = + [ + "", + " ", + "LADD", + "NATT", + "National", + "Local authority", + "LocalAuthority" + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, filterOptionId } + { + path, invalidLevels + } } ); - var viewModel = response.AssertOk(useSystemJson: true); + var allowed = GeographicLevelUtils.OrderedCodes; - Assert.Equal(expectedResults, viewModel.Results.Count); + var validationProblem = response.AssertValidationProblem(); - var meta = GatherQueryResultsMeta(viewModel); + Assert.Equal(7, validationProblem.Errors.Count); - switch (path) - { - case "filters.eq": - case "filters.in": - Assert.Single(meta.Filters["ncyear"]); - Assert.Contains(filterOptionId, meta.Filters["ncyear"]); - break; - case "filters.notEq": - case "filters.notIn": - Assert.Equal(3, meta.Filters["ncyear"].Count); - Assert.DoesNotContain(filterOptionId, meta.Filters["ncyear"]); - break; - } + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[0]", value: null, allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[1]", value: null, allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[2]", value: invalidLevels[2], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[3]", value: invalidLevels[3], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[4]", value: invalidLevels[4], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[5]", value: invalidLevels[5], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[6]", value: invalidLevels[6], allowed); } [Theory] - [InlineData("filters.in", 108)] - [InlineData("filters.notIn", 108)] - public async Task Filters_MultipleOptionsInSameFilter_Returns200(string path, int expectedResults) + [InlineData("geographicLevels.in")] + [InlineData("geographicLevels.notIn")] + public async Task NotFound_Returns200_HasWarning(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Year 4 and 8 - string[] filterOptionIds = ["IzBzg", "7zXob"]; + string[] notFoundGeographicLevels = + [ + GeographicLevel.Ward.GetEnumValue(), + GeographicLevel.OpportunityArea.GetEnumValue(), + GeographicLevel.PlanningArea.GetEnumValue(), + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, filterOptionIds } - } - ); - - var viewModel = response.AssertOk(useSystemJson: true); - - Assert.Equal(expectedResults, viewModel.Results.Count); + { + path, new StringValues( + [ + "LA", + ..notFoundGeographicLevels + ] + ) + } + } + ); - var meta = GatherQueryResultsMeta(viewModel); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(2, meta.Indicators.Count); - Assert.Contains("enrolments", meta.Indicators); - Assert.Contains("sess_authorised", meta.Indicators); + Assert.Single(viewModel.Warnings); - switch (path) - { - case "filters.in": - Assert.Equal(2, meta.Filters["ncyear"].Count); - Assert.Contains(filterOptionIds[0], meta.Filters["ncyear"]); - Assert.Contains(filterOptionIds[1], meta.Filters["ncyear"]); - break; - case "filters.notIn": - Assert.Equal(2, meta.Filters["ncyear"].Count); - Assert.DoesNotContain(filterOptionIds[0], meta.Filters["ncyear"]); - Assert.DoesNotContain(filterOptionIds[1], meta.Filters["ncyear"]); - break; - } + viewModel.AssertHasGeographicLevelsNotFoundWarning(path, notFoundGeographicLevels); } [Fact] - public async Task Filters_CommaSeparatedOptionsInSameFilter_Returns200() + public async Task AllComparatorsInvalid_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Year 4 and 8 - string[] filterOptionIds = ["IzBzg", "7zXob"]; + string[] invalidLevels = + [ + " ", + "National", + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { "filters.in", filterOptionIds.JoinToString(',') } + { + "geographicLevels.eq", "NATT" + }, + { + "geographicLevels.notEq", "LADD" + }, + { + "geographicLevels.in", invalidLevels + }, + { + "geographicLevels.notIn[]", "" + }, } ); - var viewModel = response.AssertOk(useSystemJson: true); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(108, viewModel.Results.Count); + var allowed = GeographicLevelUtils.OrderedCodes; - var meta = GatherQueryResultsMeta(viewModel); + Assert.Equal(5, validationProblem.Errors.Count); - Assert.Equal(2, meta.Filters["ncyear"].Count); - Assert.Contains(filterOptionIds[0], meta.Filters["ncyear"]); - Assert.Contains(filterOptionIds[1], meta.Filters["ncyear"]); + validationProblem.AssertHasAllowedValueError( + expectedPath: "geographicLevels.eq", + value: "NATT", + allowed: allowed + ); + validationProblem.AssertHasAllowedValueError( + expectedPath: "geographicLevels.notEq", + value: "LADD", + allowed: allowed + ); + validationProblem.AssertHasAllowedValueError( + expectedPath: "geographicLevels.in[0]", + value: null, + allowed: allowed + ); + validationProblem.AssertHasAllowedValueError( + expectedPath: "geographicLevels.in[1]", + value: invalidLevels[1], + allowed: allowed + ); + validationProblem.AssertHasNotEmptyError("geographicLevels.notIn"); } + } + public class LocationsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerGetQueryTests(testApp) + { [Theory] - [InlineData("filters.in", 150)] - [InlineData("filters.notIn", 66)] - public async Task Filters_MultipleOptionsInDifferentFilters_Returns200(string path, int expectedResults) + [InlineData("locations.in")] + [InlineData("locations.notIn")] + public async Task Empty_Returns400(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Total and secondary school type - // Secondary free school and secondary sponsor led academy types - string[] filterOptionIds = ["0kT5D", "6jrfe", "9U4vZ", "O7CLF"]; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, filterOptionIds } + { + $"{path}[]", "" + } } ); - var viewModel = response.AssertOk(useSystemJson: true); - - Assert.Equal(expectedResults, viewModel.Results.Count); - - var meta = GatherQueryResultsMeta(viewModel); - - switch (path) - { - case "filters.in": - Assert.Equal(2, meta.Filters["school_type"].Count); - Assert.Contains(filterOptionIds[0], meta.Filters["school_type"]); - Assert.Contains(filterOptionIds[1], meta.Filters["school_type"]); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(2, meta.Filters["academy_type"].Count); - Assert.Contains(filterOptionIds[2], meta.Filters["academy_type"]); - Assert.Contains(filterOptionIds[3], meta.Filters["academy_type"]); - break; - case "filters.notIn": - Assert.Single(meta.Filters["school_type"]); - Assert.DoesNotContain(filterOptionIds[0], meta.Filters["school_type"]); - Assert.DoesNotContain(filterOptionIds[1], meta.Filters["school_type"]); + Assert.Single(validationProblem.Errors); - Assert.Single(meta.Filters["academy_type"]); - Assert.DoesNotContain(filterOptionIds[2], meta.Filters["academy_type"]); - Assert.DoesNotContain(filterOptionIds[3], meta.Filters["academy_type"]); - break; - } + validationProblem.AssertHasNotEmptyError(path); } [Theory] - [InlineData("geographicLevels.eq", 132)] - [InlineData("geographicLevels.notEq", 84)] - [InlineData("geographicLevels.in", 132)] - [InlineData("geographicLevels.notIn", 84)] - public async Task GeographicLevels_SingleOption_Returns200(string path, int expectedResults) + [InlineData("locations.in")] + [InlineData("locations.notIn")] + public async Task InvalidMix_Returns400(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - const GeographicLevel geographicLevel = GeographicLevel.LocalAuthority; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, geographicLevel.GetEnumValue() } + { + path, new StringValues( + [ + "", + "invalid", + "||", + "LADD|code|12345", + "NATT|code|12345", + "NAT|invalid|12345", + "LA|urn|12345", + "SCH|code|12345", + "PROV|oldCode|12345", + "RSC|code|12345", + "NAT|id| ", + "LA|code| ", + $"NAT|id|{new string('a', 11)}", + $"LA|code|{new string('a', 26)}", + $"SCH|urn|{new string('a', 7)}", + $"PROV|ukprn|{new string('a', 9)}", + $"RSC|id|{new string('a', 11)}", + ] + ) + } } ); - var viewModel = response.AssertOk(useSystemJson: true); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(expectedResults, viewModel.Results.Count); + Assert.Equal(17, validationProblem.Errors.Count); - var meta = GatherQueryResultsMeta(viewModel); + validationProblem.AssertHasNotEmptyError(expectedPath: $"{path}[0]"); + validationProblem.AssertHasLocationFormatError(expectedPath: $"{path}[1]", value: "invalid"); + validationProblem.AssertHasLocationFormatError(expectedPath: $"{path}[2]", value: "||"); - switch (path) - { - case "geographicLevels.eq": - case "geographicLevels.in": - Assert.Single(meta.GeographicLevels); - Assert.Contains(geographicLevel, meta.GeographicLevels); - break; - case "geographicLevels.notEq": - case "geographicLevels.notIn": - Assert.Equal(3, meta.GeographicLevels.Count); - Assert.DoesNotContain(geographicLevel, meta.GeographicLevels); - break; - } + validationProblem.AssertHasLocationAllowedLevelError(expectedPath: $"{path}[3]", level: "LADD"); + validationProblem.AssertHasLocationAllowedLevelError(expectedPath: $"{path}[4]", level: "NATT"); + + validationProblem.AssertHasLocationAllowedPropertyError( + expectedPath: $"{path}[5]", + property: "invalid", + allowedProperties: ["id", "code"] + ); + validationProblem.AssertHasLocationAllowedPropertyError( + expectedPath: $"{path}[6]", + property: "urn", + allowedProperties: ["id", "code", "oldCode"] + ); + validationProblem.AssertHasLocationAllowedPropertyError( + expectedPath: $"{path}[7]", + property: "code", + allowedProperties: ["id", "urn", "laEstab"] + ); + validationProblem.AssertHasLocationAllowedPropertyError( + expectedPath: $"{path}[8]", + property: "oldCode", + allowedProperties: ["id", "ukprn"] + ); + validationProblem.AssertHasLocationAllowedPropertyError( + expectedPath: $"{path}[9]", + property: "code", + allowedProperties: ["id"] + ); + validationProblem.AssertHasLocationValueNotEmptyError(expectedPath: $"{path}[10]", property: "id"); + validationProblem.AssertHasLocationValueNotEmptyError(expectedPath: $"{path}[11]", property: "code"); + + validationProblem.AssertHasLocationValueMaxLengthError( + expectedPath: $"{path}[12]", + property: "id", + maxLength: 10 + ); + validationProblem.AssertHasLocationValueMaxLengthError( + expectedPath: $"{path}[13]", + property: "code", + maxLength: 25 + ); + validationProblem.AssertHasLocationValueMaxLengthError( + expectedPath: $"{path}[14]", + property: "urn", + maxLength: 6 + ); + validationProblem.AssertHasLocationValueMaxLengthError( + expectedPath: $"{path}[15]", + property: "ukprn", + maxLength: 8 + ); + validationProblem.AssertHasLocationValueMaxLengthError( + expectedPath: $"{path}[16]", + property: "id", + maxLength: 10 + ); } - [Theory] - [InlineData("geographicLevels.in", 180)] - [InlineData("geographicLevels.notIn", 36)] - public async Task GeographicLevels_MultipleOptions_Returns200(string path, int expectedResults) + [Fact] + public async Task AllComparatorsInvalid_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); - GeographicLevel[] geographicLevels = [GeographicLevel.Region, GeographicLevel.LocalAuthority]; + string[] invalidLocations = + [ + "", + "||", + "NAT|id| ", + $"NAT|id|{new string('a', 11)}", + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, geographicLevels.Select(l => l.GetEnumValue()).ToArray() } - } - ); - - var viewModel = response.AssertOk(useSystemJson: true); + { + "locations.eq", "LADD|code|12345" + }, + { + "locations.notEq", "LA|urn|12345" + }, + { + "locations.in", invalidLocations + }, + { + "locations.notIn[]", "" + }, + } + ); - Assert.Equal(expectedResults, viewModel.Results.Count); + var validationProblem = response.AssertValidationProblem(); - var meta = GatherQueryResultsMeta(viewModel); + Assert.Equal(7, validationProblem.Errors.Count); - switch (path) - { - case "geographicLevels.in": - Assert.Equal(2, meta.GeographicLevels.Count); - Assert.Contains(geographicLevels[0], meta.GeographicLevels); - Assert.Contains(geographicLevels[1], meta.GeographicLevels); - break; - case "geographicLevels.notIn": - Assert.Equal(2, meta.GeographicLevels.Count); - Assert.DoesNotContain(geographicLevels[0], meta.GeographicLevels); - Assert.DoesNotContain(geographicLevels[1], meta.GeographicLevels); - break; - } + validationProblem.AssertHasLocationAllowedLevelError(expectedPath: "locations.eq", level: "LADD"); + validationProblem.AssertHasLocationAllowedPropertyError( + expectedPath: "locations.notEq", + property: "urn", + allowedProperties: ["id", "code", "oldCode"] + ); + validationProblem.AssertHasNotEmptyError(expectedPath: "locations.in[0]"); + validationProblem.AssertHasLocationFormatError(expectedPath: "locations.in[1]", value: "||"); + validationProblem.AssertHasLocationValueNotEmptyError( + expectedPath: "locations.in[2]", + property: "id" + ); + validationProblem.AssertHasLocationValueMaxLengthError( + expectedPath: "locations.in[3]", + property: "id", + maxLength: 10 + ); + validationProblem.AssertHasNotEmptyError("locations.notIn"); } - [Fact] - public async Task GeographicLevels_CommaSeparatedOptions_Returns200() + [Theory] + [InlineData("locations.in")] + [InlineData("locations.notIn")] + public async Task NotFound_Returns200_HasWarning(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - GeographicLevel[] geographicLevels = [GeographicLevel.Region, GeographicLevel.LocalAuthority]; + string[] notFoundLocations = + [ + "NAT|id|11111111", + "NAT|code|11111111", + "REG|id|22222222", + "LA|id|33333333", + "LA|code|4444444", + "LA|oldCode|999", + "SCH|id|55555555", + "SCH|urn|666666", + "SCH|laEstab|7777777", + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { "geographicLevels.in", geographicLevels.Select(l => l.GetEnumValue()).JoinToString(',') } + { + path, new StringValues(["LA|code|E08000016", ..notFoundLocations]) + } } ); var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(180, viewModel.Results.Count); - - var meta = GatherQueryResultsMeta(viewModel); + Assert.Single(viewModel.Warnings); - Assert.Equal(2, meta.GeographicLevels.Count); - Assert.Contains(geographicLevels[0], meta.GeographicLevels); - Assert.Contains(geographicLevels[1], meta.GeographicLevels); + viewModel.AssertHasLocationsNotFoundWarning(path, notFoundLocations); } + } - [Theory] - [InlineData("locations.eq", 36)] - [InlineData("locations.notEq", 180)] - [InlineData("locations.in", 36)] - [InlineData("locations.notIn", 180)] - public async Task Locations_SingleOption_Returns200(string path, int expectedResults) + public class TimePeriodsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerGetQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Sheffield - const string locationStrings = "LA|code|E08000019"; - const string locationId = "7zXob"; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, locationStrings } + { + "timePeriods.in[]", "" + } } ); - var viewModel = response.AssertOk(useSystemJson: true); - - Assert.Equal(expectedResults, viewModel.Results.Count); + var validationProblem = response.AssertValidationProblem(); - var meta = GatherQueryResultsMeta(viewModel); + Assert.Single(validationProblem.Errors); - switch (path) - { - case "locations.eq": - case "locations.in": - Assert.Single(meta.Locations["LA"]); - Assert.Contains(locationId, meta.Locations["LA"]); - break; - case "locations.notEq": - case "locations.notIn": - Assert.Equal(3, meta.Locations["LA"].Count); - Assert.DoesNotContain(locationId, meta.Locations["LA"]); - break; - } + validationProblem.AssertHasNotEmptyError("timePeriods.in"); } [Theory] - [InlineData("locations.in", 72)] - [InlineData("locations.notIn", 144)] - public async Task Locations_MultipleOptionsInSameLevel_Returns200(string path, int expectedResults) + [InlineData("timePeriods.in")] + [InlineData("timePeriods.notIn")] + public async Task InvalidMix_Returns400(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Sheffield and Barnsley - string[] locationStrings = ["LA|code|E08000019", "LA|id|O7CLF"]; - string[] locationIds = ["7zXob", "O7CLF"]; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, locationStrings } + { + path, new StringValues( + [ + "", + "invalid", + "|", + "2020/2019|AY", + "2020/2022|AY", + "2020|INVALID", + "2020/2021|CY", + "2020/2021|CYQ2", + "2020/2021|RY", + "2020/2021|W10", + "2020/2021|M5", + ] + ) + } } ); - var viewModel = response.AssertOk(useSystemJson: true); + var validationProblem = response.AssertValidationProblem(); - Assert.Equal(expectedResults, viewModel.Results.Count); + Assert.Equal(11, validationProblem.Errors.Count); - var meta = GatherQueryResultsMeta(viewModel); + validationProblem.AssertHasNotEmptyError($"{path}[0]"); + validationProblem.AssertHasTimePeriodFormatError(expectedPath: $"{path}[1]", value: "invalid"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[2]", code: ""); - switch (path) - { - case "locations.in": - Assert.Equal(2, meta.Locations["LA"].Count); - Assert.Contains(locationIds[0], meta.Locations["LA"]); - Assert.Contains(locationIds[1], meta.Locations["LA"]); - break; - case "locations.notIn": - Assert.Equal(2, meta.Locations["LA"].Count); - Assert.DoesNotContain(locationIds[0], meta.Locations["LA"]); - Assert.DoesNotContain(locationIds[1], meta.Locations["LA"]); - break; - } + validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{path}[3]", period: "2020/2019"); + validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{path}[4]", period: "2020/2022"); + + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[5]", code: "INVALID"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[6]", code: "CY"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[7]", code: "CYQ2"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[8]", code: "RY"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[9]", code: "W10"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[10]", code: "M5"); } [Fact] - public async Task Locations_CommaSeparatedOptionsInSameLevel_Returns200() + public async Task AllComparatorsInvalid_Returns400() { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Sheffield and Barnsley - string[] locationStrings = ["LA|code|E08000019", "LA|id|O7CLF"]; - string[] locationIds = ["7zXob", "O7CLF"]; + string[] invalidTimePeriods = + [ + "", + "invalid", + "|" + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { "locations.in", locationStrings.JoinToString(',') } + { + "timePeriods.eq", "2020/2019|AY" + }, + { + "timePeriods.notEq", "2020/2021|W10" + }, + { + "timePeriods.in", invalidTimePeriods + }, + { + "timePeriods.notIn[]", "" + } } ); - var viewModel = response.AssertOk(useSystemJson: true); - - Assert.Equal(72, viewModel.Results.Count); + var validationProblem = response.AssertValidationProblem(); - var meta = GatherQueryResultsMeta(viewModel); + Assert.Equal(6, validationProblem.Errors.Count); - Assert.Equal(2, meta.Locations["LA"].Count); - Assert.Contains(locationIds[0], meta.Locations["LA"]); - Assert.Contains(locationIds[1], meta.Locations["LA"]); + validationProblem.AssertHasTimePeriodYearRangeError("timePeriods.eq", period: "2020/2019"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: "timePeriods.notEq", code: "W10"); + validationProblem.AssertHasNotEmptyError(expectedPath: "timePeriods.in[0]"); + validationProblem.AssertHasTimePeriodFormatError(expectedPath: "timePeriods.in[1]", value: "invalid"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: "timePeriods.in[2]", code: ""); + validationProblem.AssertHasNotEmptyError(expectedPath: "timePeriods.notIn"); } [Theory] - [InlineData("locations.in", 84)] - [InlineData("locations.notIn", 132)] - public async Task Locations_MultipleOptionsInDifferentLevels_Returns200(string path, int expectedResults) + [InlineData("timePeriods.in")] + [InlineData("timePeriods.notIn")] + public async Task NotFound_Returns200_HasWarning(string path) { var dataSetVersion = await SetupDefaultDataSetVersion(); - // Sheffield and Barnsley - // THe Kingston Academy and King Athelstan Primary School - string[] locationStrings = ["LA|code|E08000019", "LA|id|O7CLF", "SCH|laEstab|3144001", "SCH|urn|102579"]; - string[] locationIds = ["7zXob", "O7CLF", "0kT5D", "arLPb"]; + string[] notFoundTimePeriods = + [ + "2021|CY", + "2022|CY", + "2030|CY", + "2023/2024|AY", + "2018/2019|AY", + ]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["enrolments", "sess_authorised"], + indicators: ["sess_authorised"], queryParameters: new Dictionary { - { path, locationStrings } + { + path, new StringValues(["2020/2021|AY", ..notFoundTimePeriods]) + } } ); var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(expectedResults, viewModel.Results.Count); + Assert.Single(viewModel.Warnings); - var meta = GatherQueryResultsMeta(viewModel); + viewModel.AssertHasTimePeriodsNotFoundWarning(path, notFoundTimePeriods); + } + } - switch (path) - { - case "locations.in": - Assert.Equal(3, meta.Locations["LA"].Count); - Assert.Contains(locationIds[0], meta.Locations["LA"]); - Assert.Contains(locationIds[1], meta.Locations["LA"]); + public class SortsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerGetQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); - Assert.Equal(6, meta.Locations["SCH"].Count); - Assert.Contains(locationIds[2], meta.Locations["SCH"]); - Assert.Contains(locationIds[3], meta.Locations["SCH"]); - break; - case "locations.notIn": - Assert.Equal(2, meta.Locations["LA"].Count); - Assert.DoesNotContain(locationIds[0], meta.Locations["LA"]); - Assert.DoesNotContain(locationIds[1], meta.Locations["LA"]); + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + queryParameters: new Dictionary + { + { + "sorts[]", "" + } + } + ); - Assert.Equal(2, meta.Locations["SCH"].Count); - Assert.DoesNotContain(locationIds[2], meta.Locations["SCH"]); - Assert.DoesNotContain(locationIds[3], meta.Locations["SCH"]); - break; - } + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError("sorts"); + } + + [Fact] + public async Task InvalidMix_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + sorts: + [ + "", + "invalid", + "|", + "test|", + "test|invalid", + "test|asc", + "test|desc", + $"{new string('a', 41)}|Asc", + $"{new string('b', 41)}|Desc", + "missing1|Asc", + "missing2|Desc", + ] + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(10, validationProblem.Errors.Count); + + validationProblem.AssertHasNotEmptyError(expectedPath: "sorts[0]"); + validationProblem.AssertHasSortFormatError(expectedPath: "sorts[1]", value: "invalid"); + + validationProblem.AssertHasSortFieldNotEmptyError(expectedPath: "sorts[2]"); + + validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[2]", direction: ""); + validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[3]", direction: ""); + validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[4]", direction: "invalid"); + validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[5]", direction: "asc"); + validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[6]", direction: "desc"); + + validationProblem.AssertHasSortFieldMaxLengthError( + expectedPath: "sorts[7]", + field: new string('a', 41) + ); + validationProblem.AssertHasSortFieldMaxLengthError( + expectedPath: "sorts[8]", + field: new string('b', 41) + ); + } + + [Fact] + public async Task FieldsNotFound_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] notFoundSorts = + [ + "invalid1|Asc", + "invalid2|Desc", + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + sorts: ["timePeriod|Asc", ..notFoundSorts] + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasSortFieldsNotFoundError("sorts", notFoundSorts); } + } + public class PaginationTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { [Theory] - [InlineData("timePeriods.eq", 72)] - [InlineData("timePeriods.notEq", 144)] - [InlineData("timePeriods.in", 72)] - [InlineData("timePeriods.notIn", 144)] - [InlineData("timePeriods.gt", 72)] - [InlineData("timePeriods.gte", 144)] - [InlineData("timePeriods.lt", 72)] - [InlineData("timePeriods.lte", 144)] - public async Task TimePeriods_SingleOption_Returns200(string path, int expectedResults) + [InlineData(-1)] + [InlineData(0)] + public async Task PageTooSmall_Returns400(int page) { var dataSetVersion = await SetupDefaultDataSetVersion(); - const string timePeriodString = "2021/2022|AY"; + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + page: page + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasGreaterThanOrEqualError("page", comparisonValue: 1); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(10001)] + public async Task PageSizeOutOfBounds_Returns400(int pageSize) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + pageSize: pageSize + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasInclusiveBetweenError("pageSize", from: 1, to: 10000); + } + + [Theory] + [InlineData(1, 50, 5)] + [InlineData(2, 50, 5)] + [InlineData(3, 50, 5)] + [InlineData(1, 150, 2)] + [InlineData(2, 150, 2)] + [InlineData(1, 216, 1)] + public async Task MultiplePages_Returns200_PaginatedCorrectly(int page, int pageSize, int totalPages) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + indicators: ["sess_authorised"], + page: page, + pageSize: pageSize + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(page, viewModel.Paging.Page); + Assert.Equal(pageSize, viewModel.Paging.PageSize); + Assert.Equal(totalPages, viewModel.Paging.TotalPages); + Assert.Equal(216, viewModel.Paging.TotalResults); + } + } + + public class FiltersQueryTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Theory] + [InlineData("filters.eq", 54)] + [InlineData("filters.notEq", 162)] + [InlineData("filters.in", 54)] + [InlineData("filters.notIn", 162)] + public async Task SingleOption_Returns200(string path, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Year 4 + const string filterOptionId = "IzBzg"; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { path, timePeriodString } + { path, filterOptionId } } ); @@ -869,52 +1076,37 @@ public async Task TimePeriods_SingleOption_Returns200(string path, int expectedR var meta = GatherQueryResultsMeta(viewModel); - var timePeriod = new TimePeriodViewModel - { - Code = TimeIdentifier.AcademicYear, - Period = "2021/2022" - }; - switch (path) { - case "timePeriods.eq": - case "timePeriods.in": - Assert.Single(meta.TimePeriods); - Assert.Contains(timePeriod, meta.TimePeriods); - break; - case "timePeriods.notEq": - case "timePeriods.notIn": - Assert.Equal(2, meta.TimePeriods.Count); - Assert.DoesNotContain(timePeriod, meta.TimePeriods); - break; - case "timePeriods.lt": - case "timePeriods.gt": - Assert.Single(meta.TimePeriods); - Assert.DoesNotContain(timePeriod, meta.TimePeriods); + case "filters.eq": + case "filters.in": + Assert.Single(meta.Filters["ncyear"]); + Assert.Contains(filterOptionId, meta.Filters["ncyear"]); break; - case "timePeriods.lte": - case "timePeriods.gte": - Assert.Equal(2, meta.TimePeriods.Count); - Assert.Contains(timePeriod, meta.TimePeriods); + case "filters.notEq": + case "filters.notIn": + Assert.Equal(3, meta.Filters["ncyear"].Count); + Assert.DoesNotContain(filterOptionId, meta.Filters["ncyear"]); break; } } [Theory] - [InlineData("timePeriods.in", 144)] - [InlineData("timePeriods.notIn", 72)] - public async Task TimePeriods_MultipleOptions_Returns200(string path, int expectedResults) + [InlineData("filters.in", 108)] + [InlineData("filters.notIn", 108)] + public async Task MultipleOptionsInSameFilter_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] timePeriodStrings = ["2021|AY", "2022/2023|AY"]; + // Year 4 and 8 + string[] filterOptionIds = ["IzBzg", "7zXob"]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { path, timePeriodStrings } + { path, filterOptionIds } } ); @@ -924,1117 +1116,975 @@ public async Task TimePeriods_MultipleOptions_Returns200(string path, int expect var meta = GatherQueryResultsMeta(viewModel); - TimePeriodViewModel[] timePeriods = - [ - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" } - ]; + Assert.Equal(2, meta.Indicators.Count); + Assert.Contains("enrolments", meta.Indicators); + Assert.Contains("sess_authorised", meta.Indicators); switch (path) { - case "timePeriods.in": - Assert.Equal(2, meta.TimePeriods.Count); - Assert.Contains(timePeriods[0], meta.TimePeriods); - Assert.Contains(timePeriods[1], meta.TimePeriods); + case "filters.in": + Assert.Equal(2, meta.Filters["ncyear"].Count); + Assert.Contains(filterOptionIds[0], meta.Filters["ncyear"]); + Assert.Contains(filterOptionIds[1], meta.Filters["ncyear"]); break; - case "timePeriods.notIn": - Assert.Single(meta.TimePeriods); - Assert.DoesNotContain(timePeriods[0], meta.TimePeriods); - Assert.DoesNotContain(timePeriods[1], meta.TimePeriods); + case "filters.notIn": + Assert.Equal(2, meta.Filters["ncyear"].Count); + Assert.DoesNotContain(filterOptionIds[0], meta.Filters["ncyear"]); + Assert.DoesNotContain(filterOptionIds[1], meta.Filters["ncyear"]); break; } } [Fact] - public async Task TimePeriods_CommaSeparatedOptions_Returns200() + public async Task CommaSeparatedOptionsInSameFilter_Returns200() { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] timePeriodStrings = ["2021|AY", "2022/2023|AY"]; + // Year 4 and 8 + string[] filterOptionIds = ["IzBzg", "7zXob"]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { "timePeriods.in", timePeriodStrings } + { "filters.in", filterOptionIds.JoinToString(',') } } ); var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(144, viewModel.Results.Count); + Assert.Equal(108, viewModel.Results.Count); var meta = GatherQueryResultsMeta(viewModel); - TimePeriodViewModel[] timePeriods = - [ - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, - new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" } - ]; - - Assert.Equal(2, meta.TimePeriods.Count); - Assert.Contains(timePeriods[0], meta.TimePeriods); - Assert.Contains(timePeriods[1], meta.TimePeriods); + Assert.Equal(2, meta.Filters["ncyear"].Count); + Assert.Contains(filterOptionIds[0], meta.Filters["ncyear"]); + Assert.Contains(filterOptionIds[1], meta.Filters["ncyear"]); } - - [Fact] - public async Task AllFacets_MixOfConditions_Returns200() + + [Theory] + [InlineData("filters.in", 150)] + [InlineData("filters.notIn", 66)] + public async Task MultipleOptionsInDifferentFilters_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); + // Total and secondary school type + // Secondary free school and secondary sponsor led academy types + string[] filterOptionIds = ["0kT5D", "6jrfe", "9U4vZ", "O7CLF"]; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, indicators: ["enrolments", "sess_authorised"], - debug: true, queryParameters: new Dictionary { - // Year 8 - { "filters.notEq", "7zXob" }, - // Secondary free school and Secondary sponsor led academy types - { "filters.in", new StringValues(["9U4vZ", "O7CLF"]) }, - { "geographicLevels.notEq", "NAT" }, - // England - { "locations.eq", "NAT|id|pTSoj" }, - // Outer London, Barnsley - { "locations.notIn", new StringValues(["REG|code|E13000002", "LA|oldCode|370"]) }, - { "timePeriods.gt", "2020/2021|AY" }, - { "timePeriods.lt", "2022/2023|AY" } + { path, filterOptionIds } } ); var viewModel = response.AssertOk(useSystemJson: true); - var result = Assert.Single(viewModel.Results); - - Assert.Equal(3, result.Filters.Count); - Assert.Equal("pTSoj :: Year 10", result.Filters["ncyear"]); - Assert.Equal("6jrfe :: State-funded secondary", result.Filters["school_type"]); - Assert.Equal("O7CLF :: Secondary sponsor led academy", result.Filters["academy_type"]); + Assert.Equal(expectedResults, viewModel.Results.Count); - Assert.Equal(GeographicLevel.School, result.GeographicLevel); + var meta = GatherQueryResultsMeta(viewModel); - Assert.Equal(4, result.Locations.Count); - Assert.Equal( - "pTSoj :: England (code = E92000001)", - result.Locations[GeographicLevel.Country.GetEnumValue()] - ); - Assert.Equal( - "IzBzg :: Yorkshire and The Humber (code = E12000003)", - result.Locations[GeographicLevel.Region.GetEnumValue()] - ); - Assert.Equal( - "7zXob :: Sheffield (code = E08000019, oldCode = 373)", - result.Locations[GeographicLevel.LocalAuthority.GetEnumValue()] - ); - Assert.Equal( - "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", - result.Locations[GeographicLevel.School.GetEnumValue()] - ); + switch (path) + { + case "filters.in": + Assert.Equal(2, meta.Filters["school_type"].Count); + Assert.Contains(filterOptionIds[0], meta.Filters["school_type"]); + Assert.Contains(filterOptionIds[1], meta.Filters["school_type"]); - Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); - Assert.Equal("2021/2022", result.TimePeriod.Period); + Assert.Equal(2, meta.Filters["academy_type"].Count); + Assert.Contains(filterOptionIds[2], meta.Filters["academy_type"]); + Assert.Contains(filterOptionIds[3], meta.Filters["academy_type"]); + break; + case "filters.notIn": + Assert.Single(meta.Filters["school_type"]); + Assert.DoesNotContain(filterOptionIds[0], meta.Filters["school_type"]); + Assert.DoesNotContain(filterOptionIds[1], meta.Filters["school_type"]); - Assert.Equal(2, result.Values.Count); - Assert.Equal("752009", result.Values["enrolments"]); - Assert.Equal("262396", result.Values["sess_authorised"]); + Assert.Single(meta.Filters["academy_type"]); + Assert.DoesNotContain(filterOptionIds[2], meta.Filters["academy_type"]); + Assert.DoesNotContain(filterOptionIds[3], meta.Filters["academy_type"]); + break; + } } + } - [Theory] - [InlineData(DataSetVersionStatus.Processing)] - [InlineData(DataSetVersionStatus.Failed)] - [InlineData(DataSetVersionStatus.Draft)] - [InlineData(DataSetVersionStatus.Mapping)] - [InlineData(DataSetVersionStatus.Withdrawn)] - public async Task VersionNotAvailable_Returns403(DataSetVersionStatus versionStatus) + public class GeographicLevelsQueryTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Theory] + [InlineData("geographicLevels.eq", 132)] + [InlineData("geographicLevels.notEq", 84)] + [InlineData("geographicLevels.in", 132)] + [InlineData("geographicLevels.notIn", 84)] + public async Task SingleOption_Returns200(string path, int expectedResults) { - var dataSetVersion = await SetupDefaultDataSetVersion(versionStatus); + var dataSetVersion = await SetupDefaultDataSetVersion(); + + const GeographicLevel geographicLevel = GeographicLevel.LocalAuthority; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"] + indicators: ["enrolments", "sess_authorised"], + queryParameters: new Dictionary + { + { path, geographicLevel.GetEnumValue() } + } ); - response.AssertForbidden(); - } + var viewModel = response.AssertOk(useSystemJson: true); - [Fact] - public async Task DataSetDoesNotExist_Returns404() - { - var response = await QueryDataSet( - dataSetId: Guid.NewGuid(), - indicators: ["sess_authorised"] - ); + Assert.Equal(expectedResults, viewModel.Results.Count); - response.AssertNotFound(); + var meta = GatherQueryResultsMeta(viewModel); + + switch (path) + { + case "geographicLevels.eq": + case "geographicLevels.in": + Assert.Single(meta.GeographicLevels); + Assert.Contains(geographicLevel, meta.GeographicLevels); + break; + case "geographicLevels.notEq": + case "geographicLevels.notIn": + Assert.Equal(3, meta.GeographicLevels.Count); + Assert.DoesNotContain(geographicLevel, meta.GeographicLevels); + break; + } } - [Fact] - public async Task VersionDoesNotExist_Returns404() + [Theory] + [InlineData("geographicLevels.in", 180)] + [InlineData("geographicLevels.notIn", 36)] + public async Task MultipleOptions_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); + GeographicLevel[] geographicLevels = [GeographicLevel.Region, GeographicLevel.LocalAuthority]; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - dataSetVersion: "2.0", - indicators: ["sess_authorised"] + indicators: ["enrolments", "sess_authorised"], + queryParameters: new Dictionary + { + { path, geographicLevels.Select(l => l.GetEnumValue()).ToArray() } + } ); - response.AssertNotFound(); - } - - [Fact] - public async Task Indicators_Empty_Returns400() - { - var dataSetVersion = await SetupDefaultDataSetVersion(); - - var client = BuildApp().CreateClient(); - - var response = await client.GetAsync($"{BaseUrl}/{dataSetVersion.DataSetId}/query?indicators[]="); + var viewModel = response.AssertOk(useSystemJson: true); - var validationProblem = response.AssertValidationProblem(); + Assert.Equal(expectedResults, viewModel.Results.Count); - Assert.Single(validationProblem.Errors); + var meta = GatherQueryResultsMeta(viewModel); - validationProblem.AssertHasNotEmptyError("indicators"); + switch (path) + { + case "geographicLevels.in": + Assert.Equal(2, meta.GeographicLevels.Count); + Assert.Contains(geographicLevels[0], meta.GeographicLevels); + Assert.Contains(geographicLevels[1], meta.GeographicLevels); + break; + case "geographicLevels.notIn": + Assert.Equal(2, meta.GeographicLevels.Count); + Assert.DoesNotContain(geographicLevels[0], meta.GeographicLevels); + Assert.DoesNotContain(geographicLevels[1], meta.GeographicLevels); + break; + } } [Fact] - public async Task Indicators_Blank_Returns400() + public async Task CommaSeparatedOptions_Returns200() { var dataSetVersion = await SetupDefaultDataSetVersion(); + GeographicLevel[] geographicLevels = [GeographicLevel.Region, GeographicLevel.LocalAuthority]; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["", " ", " "] + indicators: ["enrolments", "sess_authorised"], + queryParameters: new Dictionary + { + { "geographicLevels.in", geographicLevels.Select(l => l.GetEnumValue()).JoinToString(',') } + } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(3, validationProblem.Errors.Count); + Assert.Equal(180, viewModel.Results.Count); - validationProblem.AssertHasNotEmptyError("indicators[0]"); - validationProblem.AssertHasNotEmptyError("indicators[1]"); - validationProblem.AssertHasNotEmptyError("indicators[2]"); + var meta = GatherQueryResultsMeta(viewModel); + + Assert.Equal(2, meta.GeographicLevels.Count); + Assert.Contains(geographicLevels[0], meta.GeographicLevels); + Assert.Contains(geographicLevels[1], meta.GeographicLevels); } + } - [Fact] - public async Task Indicators_TooLong_Returns400() + public class LocationsQueryTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Theory] + [InlineData("locations.eq", 36)] + [InlineData("locations.notEq", 180)] + [InlineData("locations.in", 36)] + [InlineData("locations.notIn", 180)] + public async Task SingleOption_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); + // Sheffield + const string locationStrings = "LA|code|E08000019"; + const string locationId = "7zXob"; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: [new string('a', 41), new string('a', 42)] + indicators: ["enrolments", "sess_authorised"], + queryParameters: new Dictionary + { + { path, locationStrings } + } ); - var validationProblem = response.AssertValidationProblem(); - - Assert.Equal(2, validationProblem.Errors.Count); - - validationProblem.AssertHasMaximumLengthError("indicators[0]", maxLength: 40); - validationProblem.AssertHasMaximumLengthError("indicators[1]", maxLength: 40); - } - - [Fact] - public async Task Indicators_MissingParam_Returns400() - { - var dataSetVersion = await SetupDefaultDataSetVersion(); - - var client = BuildApp().CreateClient(); - - var response = await client.GetAsync($"{BaseUrl}/{dataSetVersion.DataSetId}/query"); + var viewModel = response.AssertOk(useSystemJson: true); - var validationProblem = response.AssertValidationProblem(); + Assert.Equal(expectedResults, viewModel.Results.Count); - Assert.Single(validationProblem.Errors); + var meta = GatherQueryResultsMeta(viewModel); - validationProblem.AssertHasNotEmptyError("indicators"); + switch (path) + { + case "locations.eq": + case "locations.in": + Assert.Single(meta.Locations["LA"]); + Assert.Contains(locationId, meta.Locations["LA"]); + break; + case "locations.notEq": + case "locations.notIn": + Assert.Equal(3, meta.Locations["LA"].Count); + Assert.DoesNotContain(locationId, meta.Locations["LA"]); + break; + } } - [Fact] - public async Task Indicators_NotFound_Returns400() + [Theory] + [InlineData("locations.in", 72)] + [InlineData("locations.notIn", 144)] + public async Task MultipleOptionsInSameLevel_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] notFoundIndicators = ["invalid1", "invalid2", "invalid3"]; + // Sheffield and Barnsley + string[] locationStrings = ["LA|code|E08000019", "LA|id|O7CLF"]; + string[] locationIds = ["7zXob", "O7CLF"]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: notFoundIndicators + indicators: ["enrolments", "sess_authorised"], + queryParameters: new Dictionary + { + { path, locationStrings } + } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(validationProblem.Errors); + Assert.Equal(expectedResults, viewModel.Results.Count); - validationProblem.AssertHasIndicatorsNotFoundError("indicators", notFoundIndicators); + var meta = GatherQueryResultsMeta(viewModel); + + switch (path) + { + case "locations.in": + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.Contains(locationIds[0], meta.Locations["LA"]); + Assert.Contains(locationIds[1], meta.Locations["LA"]); + break; + case "locations.notIn": + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.DoesNotContain(locationIds[0], meta.Locations["LA"]); + Assert.DoesNotContain(locationIds[1], meta.Locations["LA"]); + break; + } } - [Theory] - [InlineData("filters.in")] - [InlineData("filters.notIn")] - public async Task Filters_Empty_Returns400(string path) + [Fact] + public async Task CommaSeparatedOptionsInSameLevel_Returns200() { var dataSetVersion = await SetupDefaultDataSetVersion(); + // Sheffield and Barnsley + string[] locationStrings = ["LA|code|E08000019", "LA|id|O7CLF"]; + string[] locationIds = ["7zXob", "O7CLF"]; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], + indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { - $"{path}[]", "" - } + { "locations.in", locationStrings.JoinToString(',') } } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(validationProblem.Errors); + Assert.Equal(72, viewModel.Results.Count); - validationProblem.AssertHasNotEmptyError(path); + var meta = GatherQueryResultsMeta(viewModel); + + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.Contains(locationIds[0], meta.Locations["LA"]); + Assert.Contains(locationIds[1], meta.Locations["LA"]); } [Theory] - [InlineData("filters.in")] - [InlineData("filters.notIn")] - public async Task Filters_InvalidMix_Returns400(string path) + [InlineData("locations.in", 84)] + [InlineData("locations.notIn", 132)] + public async Task MultipleOptionsInDifferentLevels_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] invalidFilters = - [ - "", - " ", - " ", - new string('a', 11), - new string('a', 12), - ]; + // Sheffield and Barnsley + // THe Kingston Academy and King Athelstan Primary School + string[] locationStrings = ["LA|code|E08000019", "LA|id|O7CLF", "SCH|laEstab|3144001", "SCH|urn|102579"]; + string[] locationIds = ["7zXob", "O7CLF", "0kT5D", "arLPb"]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], + indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { path, invalidFilters } + { path, locationStrings } } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(5, validationProblem.Errors.Count); + Assert.Equal(expectedResults, viewModel.Results.Count); - validationProblem.AssertHasNotEmptyError($"{path}[0]"); - validationProblem.AssertHasNotEmptyError($"{path}[1]"); - validationProblem.AssertHasNotEmptyError($"{path}[2]"); - validationProblem.AssertHasMaximumLengthError($"{path}[3]", maxLength: 10); - validationProblem.AssertHasMaximumLengthError($"{path}[4]", maxLength: 10); + var meta = GatherQueryResultsMeta(viewModel); + + switch (path) + { + case "locations.in": + Assert.Equal(3, meta.Locations["LA"].Count); + Assert.Contains(locationIds[0], meta.Locations["LA"]); + Assert.Contains(locationIds[1], meta.Locations["LA"]); + + Assert.Equal(6, meta.Locations["SCH"].Count); + Assert.Contains(locationIds[2], meta.Locations["SCH"]); + Assert.Contains(locationIds[3], meta.Locations["SCH"]); + break; + case "locations.notIn": + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.DoesNotContain(locationIds[0], meta.Locations["LA"]); + Assert.DoesNotContain(locationIds[1], meta.Locations["LA"]); + + Assert.Equal(2, meta.Locations["SCH"].Count); + Assert.DoesNotContain(locationIds[2], meta.Locations["SCH"]); + Assert.DoesNotContain(locationIds[3], meta.Locations["SCH"]); + break; + } } + } - [Fact] - public async Task Filters_AllCriteriaInvalid_Returns400() + public class TimePeriodsQueryTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Theory] + [InlineData("timePeriods.eq", 72)] + [InlineData("timePeriods.notEq", 144)] + [InlineData("timePeriods.in", 72)] + [InlineData("timePeriods.notIn", 144)] + [InlineData("timePeriods.gt", 72)] + [InlineData("timePeriods.gte", 144)] + [InlineData("timePeriods.lt", 72)] + [InlineData("timePeriods.lte", 144)] + public async Task SingleOption_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] invalidFilters = - [ - new string('a', 11), - "" - ]; + const string timePeriodString = "2021/2022|AY"; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], + indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { - "filters.eq", new string('a', 11) - }, - { - "filters.notEq", new string('a', 12) - }, - { - "filters.in[]", "" - }, - { - "filters.notIn", invalidFilters - }, + { path, timePeriodString } } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(5, validationProblem.Errors.Count); + Assert.Equal(expectedResults, viewModel.Results.Count); - validationProblem.AssertHasMaximumLengthError("filters.eq", maxLength: 10); - validationProblem.AssertHasMaximumLengthError("filters.notEq", maxLength: 10); - validationProblem.AssertHasNotEmptyError("filters.in"); - validationProblem.AssertHasMaximumLengthError("filters.notIn[0]", maxLength: 10); - validationProblem.AssertHasNotEmptyError("filters.notIn[1]"); + var meta = GatherQueryResultsMeta(viewModel); + + var timePeriod = new TimePeriodViewModel + { + Code = TimeIdentifier.AcademicYear, + Period = "2021/2022" + }; + + switch (path) + { + case "timePeriods.eq": + case "timePeriods.in": + Assert.Single(meta.TimePeriods); + Assert.Contains(timePeriod, meta.TimePeriods); + break; + case "timePeriods.notEq": + case "timePeriods.notIn": + Assert.Equal(2, meta.TimePeriods.Count); + Assert.DoesNotContain(timePeriod, meta.TimePeriods); + break; + case "timePeriods.lt": + case "timePeriods.gt": + Assert.Single(meta.TimePeriods); + Assert.DoesNotContain(timePeriod, meta.TimePeriods); + break; + case "timePeriods.lte": + case "timePeriods.gte": + Assert.Equal(2, meta.TimePeriods.Count); + Assert.Contains(timePeriod, meta.TimePeriods); + break; + } } [Theory] - [InlineData("filters.in")] - [InlineData("filters.notIn")] - public async Task Filters_NotFound_Returns200_HasWarning(string path) + [InlineData("timePeriods.in", 144)] + [InlineData("timePeriods.notIn", 72)] + public async Task MultipleOptions_Returns200(string path, int expectedResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] notFoundFilters = - [ - "invalid", - "9999999" - ]; + string[] timePeriodStrings = ["2021|AY", "2022/2023|AY"]; var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], + indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { - path, new StringValues(["IzBzg", ..notFoundFilters]) - } + { path, timePeriodStrings } } ); var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(viewModel.Warnings); + Assert.Equal(expectedResults, viewModel.Results.Count); - viewModel.AssertHasFiltersNotFoundWarning(path, notFoundFilters); + var meta = GatherQueryResultsMeta(viewModel); + + TimePeriodViewModel[] timePeriods = + [ + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" } + ]; + + switch (path) + { + case "timePeriods.in": + Assert.Equal(2, meta.TimePeriods.Count); + Assert.Contains(timePeriods[0], meta.TimePeriods); + Assert.Contains(timePeriods[1], meta.TimePeriods); + break; + case "timePeriods.notIn": + Assert.Single(meta.TimePeriods); + Assert.DoesNotContain(timePeriods[0], meta.TimePeriods); + Assert.DoesNotContain(timePeriods[1], meta.TimePeriods); + break; + } } - [Theory] - [InlineData("geographicLevels.in")] - [InlineData("geographicLevels.notIn")] - public async Task GeographicLevels_Empty_Returns400(string path) + [Fact] + public async Task CommaSeparatedOptions_Returns200() { var dataSetVersion = await SetupDefaultDataSetVersion(); + string[] timePeriodStrings = ["2021|AY", "2022/2023|AY"]; + var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], + indicators: ["enrolments", "sess_authorised"], queryParameters: new Dictionary { - { - $"{path}[]", "" - } + { "timePeriods.in", timePeriodStrings } } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(validationProblem.Errors); + Assert.Equal(144, viewModel.Results.Count); - validationProblem.AssertHasNotEmptyError(path); + var meta = GatherQueryResultsMeta(viewModel); + + TimePeriodViewModel[] timePeriods = + [ + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" } + ]; + + Assert.Equal(2, meta.TimePeriods.Count); + Assert.Contains(timePeriods[0], meta.TimePeriods); + Assert.Contains(timePeriods[1], meta.TimePeriods); } + } - [Theory] - [InlineData("geographicLevels.in")] - [InlineData("geographicLevels.notIn")] - public async Task GeographicLevels_InvalidMix_Returns400(string path) + public class ResultsTests(TestApplicationFactory testApp) : DataSetsControllerGetQueryTests(testApp) + { + [Fact] + public async Task NoResults_Returns200_HasWarning() { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] invalidLevels = - [ - "", - " ", - "LADD", - "NATT", - "National", - "Local authority", - "LocalAuthority" - ]; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, indicators: ["sess_authorised"], queryParameters: new Dictionary { { - path, invalidLevels - } + "locations.eq", "LA|id|9U4vZ" + }, + { + "geographicLevels.eq", "NAT" + } } ); - var allowed = GeographicLevelUtils.OrderedCodes; + var viewModel = response.AssertOk(useSystemJson: true); - var validationProblem = response.AssertValidationProblem(); + Assert.Equal(1, viewModel.Paging.Page); + Assert.Equal(1000, viewModel.Paging.PageSize); + Assert.Equal(1, viewModel.Paging.TotalPages); + Assert.Equal(0, viewModel.Paging.TotalResults); - Assert.Equal(7, validationProblem.Errors.Count); + var warning = Assert.Single(viewModel.Warnings); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[0]", value: null, allowed); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[1]", value: null, allowed); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[2]", value: invalidLevels[2], allowed); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[3]", value: invalidLevels[3], allowed); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[4]", value: invalidLevels[4], allowed); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[5]", value: invalidLevels[5], allowed); - validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[6]", value: invalidLevels[6], allowed); + Assert.Equal(ValidationMessages.QueryNoResults.Code, warning.Code); + Assert.Equal(ValidationMessages.QueryNoResults.Message, warning.Message); } - [Theory] - [InlineData("geographicLevels.in")] - [InlineData("geographicLevels.notIn")] - public async Task GeographicLevels_NotFound_Returns200_HasWarning(string path) + [Fact] + public async Task DebugEnabled_Returns200_HasWarning() { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] notFoundGeographicLevels = - [ - GeographicLevel.Ward.GetEnumValue(), - GeographicLevel.OpportunityArea.GetEnumValue(), - GeographicLevel.PlanningArea.GetEnumValue(), - ]; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - path, new StringValues( - [ - GeographicLevel.LocalAuthority.GetEnumValue(), - ..notFoundGeographicLevels - ] - ) - } - } + debug: true ); var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(viewModel.Warnings); + var warning = Assert.Single(viewModel.Warnings); - viewModel.AssertHasGeographicLevelsNotFoundWarning(path, notFoundGeographicLevels); + Assert.Equal(ValidationMessages.DebugEnabled.Code, warning.Code); + Assert.Equal(ValidationMessages.DebugEnabled.Message, warning.Message); } - + [Fact] - public async Task GeographicLevels_AllCriteriaInvalid_Returns400() + public async Task SingleIndicator_Returns200_CorrectViewModel() { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] invalidLevels = - [ - " ", - "National", - ]; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - "geographicLevels.eq", "NATT" - }, - { - "geographicLevels.notEq", "LADD" - }, - { - "geographicLevels.in", invalidLevels - }, - { - "geographicLevels.notIn[]", "" - }, - } + indicators: ["sess_authorised"] ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - var allowed = GeographicLevelUtils.OrderedCodes; + Assert.Equal(1, viewModel.Paging.Page); + Assert.Equal(1, viewModel.Paging.TotalPages); + Assert.Equal(216, viewModel.Paging.TotalResults); - Assert.Equal(5, validationProblem.Errors.Count); + Assert.Empty(viewModel.Warnings); - validationProblem.AssertHasAllowedValueError( - expectedPath: "geographicLevels.eq", - value: "NATT", - allowed: allowed - ); - validationProblem.AssertHasAllowedValueError( - expectedPath: "geographicLevels.notEq", - value: "LADD", - allowed: allowed - ); - validationProblem.AssertHasAllowedValueError( - expectedPath: "geographicLevels.in[0]", - value: null, - allowed: allowed - ); - validationProblem.AssertHasAllowedValueError( - expectedPath: "geographicLevels.in[1]", - value: invalidLevels[1], - allowed: allowed - ); - validationProblem.AssertHasNotEmptyError("geographicLevels.notIn"); - } + Assert.Equal(216, viewModel.Results.Count); - [Fact] - public async Task Sorts_Empty_Returns400() - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + var result = viewModel.Results[0]; - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - "sorts[]", "" - } - } - ); + Assert.Equal(2, result.Filters.Count); + Assert.Equal("pTSoj", result.Filters["ncyear"]); + Assert.Equal("0kT5D", result.Filters["school_type"]); - var validationProblem = response.AssertValidationProblem(); + Assert.Equal(GeographicLevel.LocalAuthority, result.GeographicLevel); - Assert.Single(validationProblem.Errors); + Assert.Equal(3, result.Locations.Count); + Assert.Equal("dP0Zw", result.Locations["LA"]); + Assert.Equal("pTSoj", result.Locations["NAT"]); + Assert.Equal("it6Xr", result.Locations["REG"]); - validationProblem.AssertHasNotEmptyError("sorts"); + Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); + Assert.Equal("2022/2023", result.TimePeriod.Period); + + Assert.Single(result.Values); + Assert.Equal("4064499", result.Values["sess_authorised"]); } [Fact] - public async Task Sorts_InvalidMix_Returns400() + public async Task AllIndicators_Returns200_ResultValuesInAllowedRanges() { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - sorts: + indicators: [ - "", - "invalid", - "|", - "test|", - "test|invalid", - "test|asc", - "test|desc", - $"{new string('a', 41)}|Asc", - $"{new string('b', 41)}|Desc", - "missing1|Asc", - "missing2|Desc", + "enrolments", + "sess_authorised", + "sess_possible", + "sess_unauthorised", + "sess_unauthorised_percent", ] ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(10, validationProblem.Errors.Count); + Assert.Equal(216, viewModel.Results.Count); - validationProblem.AssertHasNotEmptyError(expectedPath: "sorts[0]"); - validationProblem.AssertHasSortFormatError(expectedPath: "sorts[1]", value: "invalid"); + var values = viewModel.Results + .SelectMany(result => result.Values) + .GroupBy(kv => kv.Key, kv => kv.Value) + .ToDictionary(kv => kv.Key, kv => kv.ToList()); - validationProblem.AssertHasSortFieldNotEmptyError(expectedPath: "sorts[2]"); + var enrolments = values["enrolments"].Select(int.Parse).ToList(); - validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[2]", direction: ""); - validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[3]", direction: ""); - validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[4]", direction: "invalid"); - validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[5]", direction: "asc"); - validationProblem.AssertHasSortDirectionError(expectedPath: "sorts[6]", direction: "desc"); + Assert.Equal(216, enrolments.Count); + Assert.Equal(999598, enrolments.Max()); + Assert.Equal(1072, enrolments.Min()); - validationProblem.AssertHasSortFieldMaxLengthError( - expectedPath: "sorts[7]", - field: new string('a', 41) - ); - validationProblem.AssertHasSortFieldMaxLengthError( - expectedPath: "sorts[8]", - field: new string('b', 41) - ); - } + var sessAuthorised = values["sess_authorised"].Select(int.Parse).ToList(); - [Fact] - public async Task Sorts_FieldsNotFound_Returns400() - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + Assert.Equal(216, sessAuthorised.Count); + Assert.Equal(4967515, sessAuthorised.Max()); + Assert.Equal(22441, sessAuthorised.Min()); - string[] notFoundSorts = - [ - "invalid1|Asc", - "invalid2|Desc", - ]; + var sessPossible = values["sess_possible"].Select(int.Parse).ToList(); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - sorts: ["timePeriod|Asc", ..notFoundSorts] - ); + Assert.Equal(216, sessPossible.Count); + Assert.Equal(9934276, sessPossible.Max()); + Assert.Equal(18306, sessPossible.Min()); - var validationProblem = response.AssertValidationProblem(); + var sessUnauthorised = values["sess_unauthorised"].Select(int.Parse).ToList(); - Assert.Single(validationProblem.Errors); + Assert.Equal(216, sessUnauthorised.Count); + Assert.Equal(494993, sessUnauthorised.Max()); + Assert.Equal(2883, sessUnauthorised.Min()); - validationProblem.AssertHasSortFieldsNotFoundError("sorts", notFoundSorts); + var sessUnauthorisedPercent = values["sess_unauthorised_percent"].Select(float.Parse).ToList(); + + Assert.Equal(216, sessUnauthorisedPercent.Count); + Assert.Equal(14.8837004f, sessUnauthorisedPercent.Max()); + Assert.Equal(0.241600007f, sessUnauthorisedPercent.Min()); } - [Theory] - [InlineData("locations.in")] - [InlineData("locations.notIn")] - public async Task Locations_Empty_Returns400(string path) + [Fact] + public async Task AllIndicators_Returns200_CorrectResultIds() { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - $"{path}[]", "" - } - } + indicators: + [ + "enrolments", + "sess_authorised", + "sess_possible", + "sess_unauthorised", + "sess_unauthorised_percent", + ] ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(validationProblem.Errors); + Assert.Equal(216, viewModel.Results.Count); - validationProblem.AssertHasNotEmptyError(path); - } + var meta = GatherQueryResultsMeta(viewModel); - [Theory] - [InlineData("locations.in")] - [InlineData("locations.notIn")] - public async Task Locations_InvalidMix_Returns400(string path) - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + Assert.Equal(3, meta.Filters.Count); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - path, new StringValues( - [ - "", - "invalid", - "||", - "LADD|code|12345", - "NATT|code|12345", - "NAT|invalid|12345", - "LA|urn|12345", - "SCH|code|12345", - "PROV|oldCode|12345", - "RSC|code|12345", - "NAT|id| ", - "LA|code| ", - $"NAT|id|{new string('a', 11)}", - $"LA|code|{new string('a', 26)}", - $"SCH|urn|{new string('a', 7)}", - $"PROV|ukprn|{new string('a', 9)}", - $"RSC|id|{new string('a', 11)}", - ] - ) - } - } - ); - - var validationProblem = response.AssertValidationProblem(); - - Assert.Equal(17, validationProblem.Errors.Count); - - validationProblem.AssertHasNotEmptyError(expectedPath: $"{path}[0]"); - validationProblem.AssertHasLocationFormatError(expectedPath: $"{path}[1]", value: "invalid"); - validationProblem.AssertHasLocationFormatError(expectedPath: $"{path}[2]", value: "||"); - - validationProblem.AssertHasLocationAllowedLevelError(expectedPath: $"{path}[3]", level: "LADD"); - validationProblem.AssertHasLocationAllowedLevelError(expectedPath: $"{path}[4]", level: "NATT"); - - validationProblem.AssertHasLocationAllowedPropertyError( - expectedPath: $"{path}[5]", - property: "invalid", - allowedProperties: ["id", "code"] - ); - validationProblem.AssertHasLocationAllowedPropertyError( - expectedPath: $"{path}[6]", - property: "urn", - allowedProperties: ["id", "code", "oldCode"] - ); - validationProblem.AssertHasLocationAllowedPropertyError( - expectedPath: $"{path}[7]", - property: "code", - allowedProperties: ["id", "urn", "laEstab"] - ); - validationProblem.AssertHasLocationAllowedPropertyError( - expectedPath: $"{path}[8]", - property: "oldCode", - allowedProperties: ["id", "ukprn"] - ); - validationProblem.AssertHasLocationAllowedPropertyError( - expectedPath: $"{path}[9]", - property: "code", - allowedProperties: ["id"] - ); - validationProblem.AssertHasLocationValueNotEmptyError(expectedPath: $"{path}[10]", property: "id"); - validationProblem.AssertHasLocationValueNotEmptyError(expectedPath: $"{path}[11]", property: "code"); + Assert.Equal(3, meta.Filters["academy_type"].Count); - validationProblem.AssertHasLocationValueMaxLengthError( - expectedPath: $"{path}[12]", - property: "id", - maxLength: 10 - ); - validationProblem.AssertHasLocationValueMaxLengthError( - expectedPath: $"{path}[13]", - property: "code", - maxLength: 25 - ); - validationProblem.AssertHasLocationValueMaxLengthError( - expectedPath: $"{path}[14]", - property: "urn", - maxLength: 6 - ); - validationProblem.AssertHasLocationValueMaxLengthError( - expectedPath: $"{path}[15]", - property: "ukprn", - maxLength: 8 - ); - validationProblem.AssertHasLocationValueMaxLengthError( - expectedPath: $"{path}[16]", - property: "id", - maxLength: 10 - ); - } + Assert.Contains("dP0Zw", meta.Filters["academy_type"]); + Assert.Contains("9U4vZ", meta.Filters["academy_type"]); + Assert.Contains("O7CLF", meta.Filters["academy_type"]); - [Fact] - public async Task Locations_AllCriteriaInvalid_Returns400() - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + Assert.Equal(4, meta.Filters["ncyear"].Count); + Assert.Contains("IzBzg", meta.Filters["ncyear"]); + Assert.Contains("it6Xr", meta.Filters["ncyear"]); + Assert.Contains("7zXob", meta.Filters["ncyear"]); + Assert.Contains("pTSoj", meta.Filters["ncyear"]); - string[] invalidLocations = - [ - "", - "||", - "NAT|id| ", - $"NAT|id|{new string('a', 11)}", - ]; + Assert.Equal(3, meta.Filters["school_type"].Count); + Assert.Contains("LxWjE", meta.Filters["school_type"]); + Assert.Contains("6jrfe", meta.Filters["school_type"]); + Assert.Contains("0kT5D", meta.Filters["school_type"]); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - "locations.eq", "LADD|code|12345" - }, - { - "locations.notEq", "LA|urn|12345" - }, - { - "locations.in", invalidLocations - }, - { - "locations.notIn[]", "" - }, - } - ); + Assert.Equal(4, meta.Locations.Count); - var validationProblem = response.AssertValidationProblem(); + Assert.Single(meta.Locations["NAT"]); + Assert.Contains("pTSoj", meta.Locations["NAT"]); - Assert.Equal(7, validationProblem.Errors.Count); + Assert.Equal(2, meta.Locations["REG"].Count); + Assert.Contains("it6Xr", meta.Locations["REG"]); + Assert.Contains("IzBzg", meta.Locations["REG"]); - validationProblem.AssertHasLocationAllowedLevelError(expectedPath: "locations.eq", level: "LADD"); - validationProblem.AssertHasLocationAllowedPropertyError( - expectedPath: "locations.notEq", - property: "urn", - allowedProperties: ["id", "code", "oldCode"] - ); - validationProblem.AssertHasNotEmptyError(expectedPath: "locations.in[0]"); - validationProblem.AssertHasLocationFormatError(expectedPath: "locations.in[1]", value: "||"); - validationProblem.AssertHasLocationValueNotEmptyError( - expectedPath: "locations.in[2]", - property: "id" - ); - validationProblem.AssertHasLocationValueMaxLengthError( - expectedPath: "locations.in[3]", - property: "id", - maxLength: 10 - ); - validationProblem.AssertHasNotEmptyError("locations.notIn"); - } + Assert.Equal(4, meta.Locations["LA"].Count); + Assert.Contains("9U4vZ", meta.Locations["LA"]); + Assert.Contains("O7CLF", meta.Locations["LA"]); + Assert.Contains("dP0Zw", meta.Locations["LA"]); + Assert.Contains("7zXob", meta.Locations["LA"]); - [Theory] - [InlineData("locations.in")] - [InlineData("locations.notIn")] - public async Task Locations_NotFound_Returns200_HasWarning(string path) - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + Assert.Equal(8, meta.Locations["SCH"].Count); + Assert.Contains("qFjG7", meta.Locations["SCH"]); + Assert.Contains("0kT5D", meta.Locations["SCH"]); + Assert.Contains("arLPb", meta.Locations["SCH"]); + Assert.Contains("6jrfe", meta.Locations["SCH"]); + Assert.Contains("HTzLj", meta.Locations["SCH"]); + Assert.Contains("LxWjE", meta.Locations["SCH"]); + Assert.Contains("CpId1", meta.Locations["SCH"]); + Assert.Contains("YPHKM", meta.Locations["SCH"]); - string[] notFoundLocations = - [ - "NAT|id|11111111", - "NAT|code|11111111", - "REG|id|22222222", - "LA|id|33333333", - "LA|code|4444444", - "LA|oldCode|999", - "SCH|id|55555555", - "SCH|urn|666666", - "SCH|laEstab|7777777", - ]; + Assert.Equal(4, meta.GeographicLevels.Count); + Assert.Contains(GeographicLevel.Country, meta.GeographicLevels); + Assert.Contains(GeographicLevel.Region, meta.GeographicLevels); + Assert.Contains(GeographicLevel.LocalAuthority, meta.GeographicLevels); + Assert.Contains(GeographicLevel.School, meta.GeographicLevels); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - path, new StringValues(["LA|code|E08000016", ..notFoundLocations]) - } - } + Assert.Equal(3, meta.TimePeriods.Count); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2020/2021" }, + meta.TimePeriods ); - - var viewModel = response.AssertOk(useSystemJson: true); - - Assert.Single(viewModel.Warnings); - - viewModel.AssertHasLocationsNotFoundWarning(path, notFoundLocations); - } - - [Fact] - public async Task TimePeriods_Empty_Returns400() - { - var dataSetVersion = await SetupDefaultDataSetVersion(); - - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - "timePeriods.in[]", "" - } - } + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + meta.TimePeriods ); - - var validationProblem = response.AssertValidationProblem(); - - Assert.Single(validationProblem.Errors); - - validationProblem.AssertHasNotEmptyError("timePeriods.in"); - } - - [Theory] - [InlineData("timePeriods.in")] - [InlineData("timePeriods.notIn")] - public async Task TimePeriods_InvalidMix_Returns400(string path) - { - var dataSetVersion = await SetupDefaultDataSetVersion(); - - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - path, new StringValues( - [ - "", - "invalid", - "|", - "2020/2019|AY", - "2020/2022|AY", - "2020|INVALID", - "2020/2021|CY", - "2020/2021|CYQ2", - "2020/2021|RY", - "2020/2021|W10", - "2020/2021|M5", - ] - ) - } - } + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" }, + meta.TimePeriods ); - var validationProblem = response.AssertValidationProblem(); - - Assert.Equal(11, validationProblem.Errors.Count); - - validationProblem.AssertHasNotEmptyError($"{path}[0]"); - validationProblem.AssertHasTimePeriodFormatError(expectedPath: $"{path}[1]", value: "invalid"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[2]", code: ""); - - validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{path}[3]", period: "2020/2019"); - validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{path}[4]", period: "2020/2022"); - - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[5]", code: "INVALID"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[6]", code: "CY"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[7]", code: "CYQ2"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[8]", code: "RY"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[9]", code: "W10"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[10]", code: "M5"); + Assert.Equal(5, meta.Indicators.Count); + Assert.Contains("enrolments", meta.Indicators); + Assert.Contains("sess_authorised", meta.Indicators); + Assert.Contains("sess_possible", meta.Indicators); + Assert.Contains("sess_unauthorised", meta.Indicators); + Assert.Contains("sess_unauthorised_percent", meta.Indicators); } [Fact] - public async Task TimePeriods_AllCriteriaInvalid_Returns400() + public async Task AllIndicators_Returns200_CorrectDebuggedResultLabels() { var dataSetVersion = await SetupDefaultDataSetVersion(); - string[] invalidTimePeriods = - [ - "", - "invalid", - "|" - ]; - var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - "timePeriods.eq", "2020/2019|AY" - }, - { - "timePeriods.notEq", "2020/2021|W10" - }, - { - "timePeriods.in", invalidTimePeriods - }, - { - "timePeriods.notIn[]", "" - } - } + indicators: + [ + "enrolments", + "sess_authorised", + "sess_possible", + "sess_unauthorised", + "sess_unauthorised_percent", + ], + debug: true ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Equal(6, validationProblem.Errors.Count); + Assert.Equal(216, viewModel.Results.Count); - validationProblem.AssertHasTimePeriodYearRangeError("timePeriods.eq", period: "2020/2019"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: "timePeriods.notEq", code: "W10"); - validationProblem.AssertHasNotEmptyError(expectedPath: "timePeriods.in[0]"); - validationProblem.AssertHasTimePeriodFormatError(expectedPath: "timePeriods.in[1]", value: "invalid"); - validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: "timePeriods.in[2]", code: ""); - validationProblem.AssertHasNotEmptyError(expectedPath: "timePeriods.notIn"); - } + var meta = GatherQueryResultsMeta(viewModel); - [Theory] - [InlineData("timePeriods.in")] - [InlineData("timePeriods.notIn")] - public async Task TimePeriods_NotFound_Returns200_HasWarning(string path) - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + Assert.Equal(3, meta.Filters.Count); - string[] notFoundTimePeriods = - [ - "2021|CY", - "2022|CY", - "2030|CY", - "2023/2024|AY", - "2018/2019|AY", - ]; + Assert.Equal(3, meta.Filters["academy_type"].Count); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - queryParameters: new Dictionary - { - { - path, new StringValues(["2020/2021|AY", ..notFoundTimePeriods]) - } - } - ); + Assert.Contains("dP0Zw :: Primary sponsor led academy", meta.Filters["academy_type"]); + Assert.Contains("9U4vZ :: Secondary free school", meta.Filters["academy_type"]); + Assert.Contains("O7CLF :: Secondary sponsor led academy", meta.Filters["academy_type"]); - var viewModel = response.AssertOk(useSystemJson: true); + Assert.Equal(4, meta.Filters["ncyear"].Count); + Assert.Contains("IzBzg :: Year 4", meta.Filters["ncyear"]); + Assert.Contains("it6Xr :: Year 6", meta.Filters["ncyear"]); + Assert.Contains("7zXob :: Year 8", meta.Filters["ncyear"]); + Assert.Contains("pTSoj :: Year 10", meta.Filters["ncyear"]); - Assert.Single(viewModel.Warnings); + Assert.Equal(3, meta.Filters["school_type"].Count); + Assert.Contains("LxWjE :: State-funded primary", meta.Filters["school_type"]); + Assert.Contains("6jrfe :: State-funded secondary", meta.Filters["school_type"]); + Assert.Contains("0kT5D :: Total", meta.Filters["school_type"]); - viewModel.AssertHasTimePeriodsNotFoundWarning(path, notFoundTimePeriods); - } + Assert.Equal(4, meta.Locations.Count); - [Theory] - [InlineData(-1)] - [InlineData(0)] - public async Task PageTooSmall_Returns400(int page) - { - var dataSetVersion = await SetupDefaultDataSetVersion(); + Assert.Single(meta.Locations["NAT"]); + Assert.Contains("pTSoj :: England (code = E92000001)", meta.Locations["NAT"]); - var response = await QueryDataSet( - dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - page: page + Assert.Equal(2, meta.Locations["REG"].Count); + Assert.Contains("it6Xr :: Outer London (code = E13000002)", meta.Locations["REG"]); + Assert.Contains("IzBzg :: Yorkshire and The Humber (code = E12000003)", meta.Locations["REG"]); + + Assert.Equal(4, meta.Locations["LA"].Count); + Assert.Contains("9U4vZ :: Barnet (code = E09000003, oldCode = 302)", meta.Locations["LA"]); + Assert.Contains("O7CLF :: Barnsley (code = E08000016, oldCode = 370)", meta.Locations["LA"]); + Assert.Contains( + "dP0Zw :: Kingston upon Thames / Richmond upon Thames (code = E09000021 / E09000027, oldCode = 314)", + meta.Locations["LA"] ); + Assert.Contains("7zXob :: Sheffield (code = E08000019, oldCode = 373)", meta.Locations["LA"]); - var validationProblem = response.AssertValidationProblem(); + Assert.Equal(8, meta.Locations["SCH"].Count); + Assert.Contains("qFjG7 :: Colindale Primary School (urn = 101269, laEstab = 3022014)", meta.Locations["SCH"]); + Assert.Contains("0kT5D :: Greenhill Primary School (urn = 145374, laEstab = 3732341)", meta.Locations["SCH"]); + Assert.Contains( + "arLPb :: Hoyland Springwood Primary School (urn = 141973, laEstab = 3702039)", + meta.Locations["SCH"] + ); + Assert.Contains( + "6jrfe :: King Athelstan Primary School (urn = 102579, laEstab = 3142032)", + meta.Locations["SCH"] + ); + Assert.Contains("HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", meta.Locations["SCH"]); + Assert.Contains("LxWjE :: Penistone Grammar School (urn = 106653, laEstab = 3704027)", meta.Locations["SCH"]); + Assert.Contains("CpId1 :: The Kingston Academy (urn = 141862, laEstab = 3144001)", meta.Locations["SCH"]); + Assert.Contains("YPHKM :: Wren Academy Finchley (urn = 135507, laEstab = 3026906)", meta.Locations["SCH"]); - Assert.Single(validationProblem.Errors); + Assert.Equal(4, meta.GeographicLevels.Count); + Assert.Contains(GeographicLevel.Country, meta.GeographicLevels); + Assert.Contains(GeographicLevel.Region, meta.GeographicLevels); + Assert.Contains(GeographicLevel.LocalAuthority, meta.GeographicLevels); + Assert.Contains(GeographicLevel.School, meta.GeographicLevels); - validationProblem.AssertHasGreaterThanOrEqualError("page", comparisonValue: 1); + Assert.Equal(3, meta.TimePeriods.Count); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2020/2021" }, + meta.TimePeriods + ); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + meta.TimePeriods + ); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" }, + meta.TimePeriods + ); + + Assert.Equal(5, meta.Indicators.Count); + Assert.Contains("enrolments", meta.Indicators); + Assert.Contains("sess_authorised", meta.Indicators); + Assert.Contains("sess_possible", meta.Indicators); + Assert.Contains("sess_unauthorised", meta.Indicators); + Assert.Contains("sess_unauthorised_percent", meta.Indicators); } - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(10001)] - public async Task PageSizeOutOfBounds_Returns400(int pageSize) + [Fact] + public async Task AllFacets_MixOfConditions_Returns200() { var dataSetVersion = await SetupDefaultDataSetVersion(); var response = await QueryDataSet( dataSetId: dataSetVersion.DataSetId, - indicators: ["sess_authorised"], - pageSize: pageSize + indicators: ["enrolments", "sess_authorised"], + debug: true, + queryParameters: new Dictionary + { + // Year 8 + { "filters.notEq", "7zXob" }, + // Secondary free school and Secondary sponsor led academy types + { "filters.in", new StringValues(["9U4vZ", "O7CLF"]) }, + { "geographicLevels.notEq", "NAT" }, + // England + { "locations.eq", "NAT|id|pTSoj" }, + // Outer London, Barnsley + { "locations.notIn", new StringValues(["REG|code|E13000002", "LA|oldCode|370"]) }, + { "timePeriods.gt", "2020/2021|AY" }, + { "timePeriods.lt", "2022/2023|AY" } + } ); - var validationProblem = response.AssertValidationProblem(); + var viewModel = response.AssertOk(useSystemJson: true); - Assert.Single(validationProblem.Errors); + var result = Assert.Single(viewModel.Results); - validationProblem.AssertHasInclusiveBetweenError("pageSize", from: 1, to: 10000); + Assert.Equal(3, result.Filters.Count); + Assert.Equal("pTSoj :: Year 10", result.Filters["ncyear"]); + Assert.Equal("6jrfe :: State-funded secondary", result.Filters["school_type"]); + Assert.Equal("O7CLF :: Secondary sponsor led academy", result.Filters["academy_type"]); + + Assert.Equal(GeographicLevel.School, result.GeographicLevel); + + Assert.Equal(4, result.Locations.Count); + Assert.Equal("pTSoj :: England (code = E92000001)", result.Locations["NAT"]); + Assert.Equal("IzBzg :: Yorkshire and The Humber (code = E12000003)", result.Locations["REG"]); + Assert.Equal("7zXob :: Sheffield (code = E08000019, oldCode = 373)", result.Locations["LA"]); + Assert.Equal( + "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", + result.Locations["SCH"] + ); + + Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); + Assert.Equal("2021/2022", result.TimePeriod.Period); + + Assert.Equal(2, result.Values.Count); + Assert.Equal("752009", result.Values["enrolments"]); + Assert.Equal("262396", result.Values["sess_authorised"]); } + } - private async Task QueryDataSet( - Guid dataSetId, - IEnumerable indicators, - string? dataSetVersion = null, - int? page = null, - int? pageSize = null, - IEnumerable? sorts = null, - bool? debug = null, - IDictionary? queryParameters = null) + private async Task QueryDataSet( + Guid dataSetId, + IEnumerable indicators, + string? dataSetVersion = null, + int? page = null, + int? pageSize = null, + IEnumerable? sorts = null, + bool? debug = null, + IDictionary? queryParameters = null) + { + var query = new Dictionary { - var query = new Dictionary - { - { "indicators", indicators.ToArray() } - }; + { "indicators", indicators.ToArray() } + }; - if (dataSetVersion is not null) - { - query["dataSetVersion"] = dataSetVersion; - } + if (dataSetVersion is not null) + { + query["dataSetVersion"] = dataSetVersion; + } - if (page is not null) - { - query["page"] = page.ToString(); - } + if (page is not null) + { + query["page"] = page.ToString(); + } - if (pageSize is not null) - { - query["pageSize"] = pageSize.ToString(); - } + if (pageSize is not null) + { + query["pageSize"] = pageSize.ToString(); + } - if (sorts is not null) - { - query["sorts"] = sorts.ToArray(); - } + if (sorts is not null) + { + query["sorts"] = sorts.ToArray(); + } - if (debug is true) - { - query["debug"] = "true"; - } + if (debug is true) + { + query["debug"] = "true"; + } - if (queryParameters is not null) - { - query.AddRange(queryParameters); - } + if (queryParameters is not null) + { + query.AddRange(queryParameters); + } - var client = BuildApp().CreateClient(); + var client = BuildApp().CreateClient(); - var uri = QueryHelpers.AddQueryString($"{BaseUrl}/{dataSetId}/query", query); + var uri = QueryHelpers.AddQueryString($"{BaseUrl}/{dataSetId}/query", query); - return await client.GetAsync(uri); - } + return await client.GetAsync(uri); } private async Task SetupDefaultDataSetVersion( @@ -2074,7 +2124,7 @@ await TestApp.AddTestData( return dataSetVersion; } - + private WebApplicationFactory BuildApp() { return TestApp.ConfigureServices(services => From 659cc60e8d1c7b3dce6cd3fc8ce769eaf050ce46 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Tue, 30 Apr 2024 17:50:12 +0100 Subject: [PATCH 08/66] EES-5040 remove formik --- pnpm-lock.yaml | 59 +- .../config/webpack.config.js | 2 - .../package.json | 1 - .../components/comments/CommentAddForm.tsx | 12 +- .../components/comments/CommentEditForm.tsx | 12 +- .../editable/EditableContentForm.tsx | 12 +- .../components/editable/EditableEmbedForm.tsx | 14 +- .../editable/FeaturedTableLinkInsertForm.tsx | 16 +- .../editable/GlossaryItemInsertForm.tsx | 16 +- .../src/components/form/FormFieldEditor.tsx | 236 ++--- .../form/FormFieldThemeTopicSelect.tsx | 2 +- .../components/form/RHFFormFieldEditor.tsx | 137 --- .../ReleaseSeriesLegacyLinkForm.tsx | 14 +- .../components/AdoptMethodologyForm.tsx | 12 +- .../components/MethodologySummaryForm.tsx | 16 +- .../components/MethodologyNotesAddForm.tsx | 12 +- .../components/MethodologyNotesEditForm.tsx | 16 +- .../components/MethodologyStatusForm.tsx | 22 +- .../components/ExternalMethodologyForm.tsx | 14 +- .../components/PublicationContactForm.tsx | 18 +- .../components/PublicationDetailsForm.tsx | 20 +- .../components/PublicationForm.tsx | 24 +- .../PublicationInviteNewUsersForm.tsx | 16 +- .../PublicationReleaseContributorsForm.tsx | 12 +- .../release/components/ReleaseStatusForm.tsx | 30 +- .../release/components/ReleaseSummaryForm.tsx | 22 +- .../EditableKeyStatDataBlockForm.tsx | 18 +- .../components/EditableKeyStatTextForm.tsx | 22 +- .../components/RelatedPagesSection.tsx | 17 +- .../content/components/ReleaseNoteForm.tsx | 16 +- .../release/data/ReleaseDataFilePage.tsx | 12 +- .../data/components/AncillaryFileForm.tsx | 20 +- .../data/components/DataFileUploadForm.tsx | 24 +- .../components/ReleaseDataGuidanceSection.tsx | 16 +- .../components/DataBlockDetailsForm.tsx | 26 +- .../chart/ChartAxisConfiguration.tsx | 52 +- .../ChartBoundaryLevelsConfiguration.tsx | 12 +- .../components/chart/ChartConfiguration.tsx | 50 +- .../ChartCustomDataGroupingsConfiguration.tsx | 6 +- .../chart/ChartDataGroupingForm.tsx | 22 +- .../chart/ChartDataSetsConfiguration.tsx | 18 +- .../chart/ChartLegendConfiguration.tsx | 12 +- .../components/chart/ChartLegendItems.tsx | 16 +- .../ChartReferenceLineConfigurationForm.tsx | 30 +- ...tCustomDataGroupingsConfiguration.test.tsx | 2 +- .../ChartReferenceLinesConfiguration.test.tsx | 2 +- .../components/FilterGroupDetails.tsx | 10 +- .../footnotes/components/FootnoteForm.tsx | 16 +- .../footnotes/components/IndicatorDetails.tsx | 4 +- .../components/PreReleaseUserAccessForm.tsx | 12 +- .../components/PublicPreReleaseAccessForm.tsx | 12 +- .../src/pages/themes/components/ThemeForm.tsx | 14 +- .../themes/topics/components/TopicForm.tsx | 12 +- .../src/pages/users/UserInvitePage.tsx | 16 +- .../InviteUserPublicationRoleForm.tsx | 6 +- .../components/InviteUserReleaseRoleForm.tsx | 6 +- .../components/PublicationAccessForm.tsx | 14 +- .../users/components/ReleaseAccessForm.tsx | 17 +- .../src/pages/users/components/RoleForm.tsx | 12 +- .../PrototypeAddPublicationSubject.tsx | 14 +- .../components/PrototypeChangeStatusForm.tsx | 20 +- .../PrototypeEditPublicationSubject.tsx | 12 +- .../PrototypeEditPublicationSubjectTitle.tsx | 12 +- .../components/PrototypeMapFacetModal.tsx | 16 +- .../PrototypeNotificationCreate.tsx | 16 +- .../PrototypePrepareNextSubjectStep1.tsx | 12 +- .../PrototypePrepareNextSubjectStep2.tsx | 8 +- .../PrototypePrepareNextSubjectStep5.tsx | 16 +- .../PrototypePreviewStagedDataset.tsx | 12 +- .../components/PrototypePublicationForm.tsx | 12 +- .../components/PrototypeTableHeadersForm.tsx | 8 +- .../package.json | 1 - .../src/components/form/Form.tsx | 45 +- .../src/components/form/FormCheckbox.tsx | 6 +- .../src/components/form/FormCheckboxGroup.tsx | 11 +- ...ount.tsx => FormCheckboxSelectedCount.tsx} | 4 +- .../src/components/form/FormColourInput.tsx | 20 +- .../src/components/form/FormField.tsx | 157 ++- .../src/components/form/FormFieldCheckbox.tsx | 54 +- .../form/FormFieldCheckboxGroup.tsx | 120 ++- ...nu.tsx => FormFieldCheckboxGroupsMenu.tsx} | 16 +- ...kboxMenu.tsx => FormFieldCheckboxMenu.tsx} | 27 +- .../form/FormFieldCheckboxSearchGroup.tsx | 114 ++- .../form/FormFieldCheckboxSearchSubGroups.tsx | 156 +-- .../components/form/FormFieldColourInput.tsx | 12 +- .../components/form/FormFieldDateInput.tsx | 97 +- .../components/form/FormFieldFileInput.tsx | 109 ++- .../components/form/FormFieldNumberInput.tsx | 27 +- .../components/form/FormFieldRadioGroup.tsx | 96 +- .../form/FormFieldRadioSearchGroup.tsx | 63 +- .../src/components/form/FormFieldSelect.tsx | 16 +- .../src/components/form/FormFieldTextArea.tsx | 16 +- .../components/form/FormFieldTextInput.tsx | 15 +- .../form/{rhf => }/FormProvider.tsx | 6 +- .../src/components/form/FormTextArea.tsx | 15 +- .../components/form/__tests__/Form.test.tsx | 380 ++++---- .../FormFieldCheckbox.test.tsx} | 40 +- .../__tests__/FormFieldCheckboxGroup.test.tsx | 478 ++++------ .../FormFieldCheckboxGroupsMenu.test.tsx} | 24 +- .../FormFieldCheckboxSearchGroup.test.tsx | 445 ++++----- .../FormFieldCheckboxSearchSubGroups.test.tsx | 224 ++--- .../__tests__/FormFieldDateInput.test.tsx | 431 ++++----- .../__tests__/FormFieldRadioGroup.test.tsx | 267 +++--- .../FormFieldRadioSearchGroup.test.tsx | 291 +++--- .../form/__tests__/FormFieldSelect.test.tsx | 265 +++-- .../form/__tests__/FormTextArea.test.tsx | 221 +++-- .../FormFieldCheckboxGroupsMenu.test.tsx.snap | 0 .../form/{rhf => }/hooks/useRegister.ts | 0 .../src/components/form/rhf/RHFForm.tsx | 131 --- .../components/form/rhf/RHFFormCheckbox.tsx | 114 --- .../form/rhf/RHFFormColourInput.tsx | 62 -- .../src/components/form/rhf/RHFFormField.tsx | 139 --- .../form/rhf/RHFFormFieldCheckbox.tsx | 50 - .../form/rhf/RHFFormFieldCheckboxGroup.tsx | 81 -- .../rhf/RHFFormFieldCheckboxSearchGroup.tsx | 75 -- .../RHFFormFieldCheckboxSearchSubGroups.tsx | 112 --- .../form/rhf/RHFFormFieldColourInput.tsx | 20 - .../form/rhf/RHFFormFieldDateInput.tsx | 160 ---- .../form/rhf/RHFFormFieldFileInput.tsx | 100 -- .../form/rhf/RHFFormFieldNumberInput.tsx | 29 - .../form/rhf/RHFFormFieldRadioGroup.tsx | 69 -- .../form/rhf/RHFFormFieldRadioSearchGroup.tsx | 46 - .../form/rhf/RHFFormFieldSelect.tsx | 21 - .../form/rhf/RHFFormFieldTextArea.tsx | 18 - .../form/rhf/RHFFormFieldTextInput.tsx | 19 - .../components/form/rhf/RHFFormTextArea.tsx | 36 - .../form/rhf/__tests__/RHFForm.test.tsx | 733 -------------- .../RHFFormFieldCheckboxGroup.test.tsx | 569 ----------- .../RHFFormFieldCheckboxSearchGroup.test.tsx | 543 ----------- ...FFormFieldCheckboxSearchSubGroups.test.tsx | 902 ------------------ .../__tests__/RHFFormFieldDateInput.test.tsx | 494 ---------- .../__tests__/RHFFormFieldRadioGroup.test.tsx | 349 ------- .../RHFFormFieldRadioSearchGroup.test.tsx | 366 ------- .../rhf/__tests__/RHFFormFieldSelect.test.tsx | 359 ------- .../rhf/__tests__/RHFFormTextArea.test.tsx | 258 ----- .../__snapshots__/RHFForm.test.tsx.snap | 174 ---- ...FFormFieldCheckboxGroupsMenu.test.tsx.snap | 293 ------ .../RHFFormFieldDateInput.test.tsx.snap | 52 - .../RHFFormTextArea.test.tsx.snap | 59 -- .../rhf/util/handleAllRHFCheckboxChange.ts | 37 - .../form/{rhf => }/util/getErrorMessage.ts | 0 .../form/util/handleAllCheckboxChange.ts | 47 +- .../__tests__/createErrorHelper.test.ts} | 34 +- .../createErrorHelper.ts} | 2 +- .../src/hooks/useFormSubmit.ts | 92 -- .../table-tool/components/DataSetStep.tsx | 12 +- .../table-tool/components/DownloadTable.tsx | 12 +- .../table-tool/components/FiltersForm.tsx | 24 +- .../components/FormCheckboxSelectedCount.tsx | 28 - .../FormFieldCheckboxGroupsMenu.tsx | 32 - .../components/FormFieldCheckboxMenu.tsx | 56 -- .../components/LocationFiltersForm.tsx | 18 +- .../table-tool/components/PublicationForm.tsx | 14 +- .../components/TableHeadersAxis.tsx | 4 +- .../components/TableHeadersForm.tsx | 8 +- .../table-tool/components/TimePeriodForm.tsx | 18 +- .../FormFieldCheckboxGroupsMenu.test.tsx | 193 ---- .../__tests__/TableHeadersAxis.test.tsx | 2 +- .../__tests__/TableHeadersGroup.test.tsx | 2 +- .../TableHeadersReadOnlyList.test.tsx | 2 +- .../TableHeadersReorderableList.test.tsx | 2 +- .../__tests__/createErrorHelper.test.ts | 314 ------ .../src/validation/createErrorHelper.ts | 64 -- .../src/validation/serverValidations.ts | 22 +- .../package.json | 1 - .../src/modules/cookies/CookiesPage.tsx | 12 +- .../components/DownloadStep.tsx | 12 +- .../data-catalogue/components/ReleaseForm.tsx | 12 +- .../subscriptions/SubscriptionPage.tsx | 12 +- 169 files changed, 2845 insertions(+), 10414 deletions(-) delete mode 100644 src/explore-education-statistics-admin/src/components/form/RHFFormFieldEditor.tsx rename src/explore-education-statistics-common/src/components/form/{rhf/RHFFormCheckboxSelectedCount.tsx => FormCheckboxSelectedCount.tsx} (80%) rename src/explore-education-statistics-common/src/components/form/{rhf/RHFFormFieldCheckboxGroupsMenu.tsx => FormFieldCheckboxGroupsMenu.tsx} (69%) rename src/explore-education-statistics-common/src/components/form/{rhf/RHFFormFieldCheckboxMenu.tsx => FormFieldCheckboxMenu.tsx} (51%) rename src/explore-education-statistics-common/src/components/form/{rhf => }/FormProvider.tsx (97%) rename src/explore-education-statistics-common/src/components/form/{rhf/__tests__/RHFFormFieldCheckbox.test.tsx => __tests__/FormFieldCheckbox.test.tsx} (83%) rename src/explore-education-statistics-common/src/components/form/{rhf/__tests__/RHFFormFieldCheckboxGroupsMenu.test.tsx => __tests__/FormFieldCheckboxGroupsMenu.test.tsx} (90%) rename src/explore-education-statistics-common/src/{modules/table-tool/components => components/form}/__tests__/__snapshots__/FormFieldCheckboxGroupsMenu.test.tsx.snap (100%) rename src/explore-education-statistics-common/src/components/form/{rhf => }/hooks/useRegister.ts (100%) delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFForm.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormCheckbox.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormColourInput.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormField.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldCheckbox.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldCheckboxGroup.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldCheckboxSearchGroup.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldCheckboxSearchSubGroups.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldColourInput.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldDateInput.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldFileInput.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldNumberInput.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldRadioGroup.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldRadioSearchGroup.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldSelect.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldTextArea.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormFieldTextInput.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/RHFFormTextArea.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFForm.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldCheckboxGroup.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldCheckboxSearchGroup.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldCheckboxSearchSubGroups.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldDateInput.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldRadioGroup.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldRadioSearchGroup.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormFieldSelect.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/RHFFormTextArea.test.tsx delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/__snapshots__/RHFForm.test.tsx.snap delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/__snapshots__/RHFFormFieldCheckboxGroupsMenu.test.tsx.snap delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/__snapshots__/RHFFormFieldDateInput.test.tsx.snap delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/__tests__/__snapshots__/RHFFormTextArea.test.tsx.snap delete mode 100644 src/explore-education-statistics-common/src/components/form/rhf/util/handleAllRHFCheckboxChange.ts rename src/explore-education-statistics-common/src/components/form/{rhf => }/util/getErrorMessage.ts (100%) rename src/explore-education-statistics-common/src/components/form/{rhf/validation/__tests__/createRHFErrorHelper.test.ts => validation/__tests__/createErrorHelper.test.ts} (88%) rename src/explore-education-statistics-common/src/components/form/{rhf/validation/createRHFErrorHelper.ts => validation/createErrorHelper.ts} (95%) delete mode 100644 src/explore-education-statistics-common/src/hooks/useFormSubmit.ts delete mode 100644 src/explore-education-statistics-common/src/modules/table-tool/components/FormCheckboxSelectedCount.tsx delete mode 100644 src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxGroupsMenu.tsx delete mode 100644 src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxMenu.tsx delete mode 100644 src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/FormFieldCheckboxGroupsMenu.test.tsx delete mode 100644 src/explore-education-statistics-common/src/validation/__tests__/createErrorHelper.test.ts delete mode 100644 src/explore-education-statistics-common/src/validation/createErrorHelper.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 746102e9988..73e9b149038 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,9 +230,6 @@ importers: file-loader: specifier: ^6.2.0 version: 6.2.0(webpack@5.88.1) - formik: - specifier: ^2.4.2 - version: 2.4.2(react@18.2.0) fs-extra: specifier: ^9.1.0 version: 9.1.0 @@ -651,9 +648,6 @@ importers: domhandler: specifier: ^4.2.2 version: 4.3.1 - formik: - specifier: ^2.4.2 - version: 2.4.2(react@18.2.0) geojson: specifier: ^0.5.0 version: 0.5.0 @@ -880,9 +874,6 @@ importers: express-basic-auth: specifier: ^1.2.0 version: 1.2.1 - formik: - specifier: ^2.4.2 - version: 2.4.2(react@18.2.0) govuk-frontend: specifier: ^5.2.0 version: 5.2.0 @@ -4178,7 +4169,6 @@ packages: /@discoveryjs/json-ext@0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - dev: true /@eslint-community/eslint-utils@4.4.0(eslint@8.43.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} @@ -6572,7 +6562,6 @@ packages: dependencies: webpack: 5.88.1(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.88.1) - dev: true /@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.88.1): resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} @@ -6583,7 +6572,6 @@ packages: dependencies: webpack: 5.88.1(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.88.1) - dev: true /@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.88.1): resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} @@ -6598,7 +6586,6 @@ packages: dependencies: webpack: 5.88.1(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.88.1) - dev: true /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -7859,7 +7846,6 @@ packages: is-plain-object: 2.0.4 kind-of: 6.0.3 shallow-clone: 3.0.1 - dev: true /cls-hooked@4.2.2: resolution: {integrity: sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==} @@ -7960,7 +7946,6 @@ packages: /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - dev: true /commander@11.0.0: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} @@ -8262,7 +8247,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.25) postcss-value-parser: 4.2.0 semver: 7.5.4 - webpack: 5.88.1 + webpack: 5.88.1(webpack-cli@5.1.4) /css-minimizer-webpack-plugin@3.4.1(webpack@5.88.1): resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} @@ -8733,11 +8718,6 @@ packages: /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - /deepmerge@2.2.1: - resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} - engines: {node: '>=0.10.0'} - dev: false - /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -9151,7 +9131,6 @@ packages: resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} engines: {node: '>=4'} hasBin: true - dev: true /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -10057,7 +10036,6 @@ packages: /fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} - dev: true /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} @@ -10278,21 +10256,6 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 - /formik@2.4.2(react@18.2.0): - resolution: {integrity: sha512-C6nx0hifW2uENP3M6HpPmnAE6HFWCcd8/sqBZEOHZY6lpHJ5qehsfAy43ktpFLEmkBmhiZDei726utcUB9leqg==} - peerDependencies: - react: '>=16.8.0' - dependencies: - deepmerge: 2.2.1 - hoist-non-react-statics: 3.3.2 - lodash: 4.17.21 - lodash-es: 4.17.21 - react: 18.2.0 - react-fast-compare: 2.0.4 - tiny-warning: 1.0.3 - tslib: 2.6.0 - dev: false - /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -11069,7 +11032,6 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - dev: true /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -11133,7 +11095,6 @@ packages: /interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} - dev: true /intersection-observer@0.11.0: resolution: {integrity: sha512-KZArj2QVnmdud9zTpKf279m2bbGfG+4/kn16UU0NL3pTVl52ZHiJ9IRNSsnn6jaHrL9EGLFM5eWjTx2fz/+zoQ==} @@ -11414,7 +11375,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: isobject: 3.0.1 - dev: true /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} @@ -11540,7 +11500,6 @@ packages: /isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - dev: true /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} @@ -15145,10 +15104,6 @@ packages: resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==} dev: false - /react-fast-compare@2.0.4: - resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} - dev: false - /react-fast-compare@3.2.0: resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} dev: false @@ -15575,7 +15530,6 @@ packages: engines: {node: '>= 10.13.0'} dependencies: resolve: 1.22.2 - dev: true /recursive-readdir@2.2.3: resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} @@ -15751,7 +15705,6 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 - dev: true /resolve-from@3.0.0: resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} @@ -15765,7 +15718,6 @@ packages: /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - dev: true /resolve-pathname@3.0.0: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} @@ -16159,7 +16111,6 @@ packages: engines: {node: '>=8'} dependencies: kind-of: 6.0.3 - dev: true /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -16673,7 +16624,7 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - webpack: 5.88.1 + webpack: 5.88.1(webpack-cli@5.1.4) /style-search@0.1.0: resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} @@ -17118,7 +17069,7 @@ packages: schema-utils: 3.3.0 serialize-javascript: 6.0.1 terser: 5.18.2 - webpack: 5.88.1 + webpack: 5.88.1(webpack-cli@5.1.4) /terser@4.8.1: resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} @@ -17903,7 +17854,6 @@ packages: rechoir: 0.8.0 webpack: 5.88.1(webpack-cli@5.1.4) webpack-merge: 5.8.0 - dev: true /webpack-dev-middleware@5.3.3(webpack@5.75.0): resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==} @@ -18049,7 +17999,6 @@ packages: dependencies: clone-deep: 4.0.1 wildcard: 2.0.0 - dev: true /webpack-sources@1.4.3: resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} @@ -18185,7 +18134,6 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: true /websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} @@ -18289,7 +18237,6 @@ packages: /wildcard@2.0.0: resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} - dev: true /wmf@1.0.2: resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} diff --git a/src/explore-education-statistics-admin/config/webpack.config.js b/src/explore-education-statistics-admin/config/webpack.config.js index 1dc0ba0ef60..e1120bcd105 100644 --- a/src/explore-education-statistics-admin/config/webpack.config.js +++ b/src/explore-education-statistics-admin/config/webpack.config.js @@ -309,7 +309,6 @@ module.exports = webpackEnv => { './dist/cpexcel.js': false, '@admin': paths.appSrc, '@common': 'explore-education-statistics-common/src', - formik: require.resolve('formik'), react: require.resolve('react'), }, fallback: { @@ -334,7 +333,6 @@ module.exports = webpackEnv => { babelRuntimeRegenerator, // EES - Add extra allowed files for compatibility // with our custom import aliases - require.resolve('formik'), require.resolve('react'), ]), ], diff --git a/src/explore-education-statistics-admin/package.json b/src/explore-education-statistics-admin/package.json index 16f1193ab9c..e0ec3b335c7 100644 --- a/src/explore-education-statistics-admin/package.json +++ b/src/explore-education-statistics-admin/package.json @@ -40,7 +40,6 @@ "explore-education-statistics-ckeditor": "workspace:*", "explore-education-statistics-common": "workspace:*", "file-loader": "^6.2.0", - "formik": "^2.4.2", "fs-extra": "^9.1.0", "geojson": "^0.5.0", "govuk-frontend": "^5.2.0", diff --git a/src/explore-education-statistics-admin/src/components/comments/CommentAddForm.tsx b/src/explore-education-statistics-admin/src/components/comments/CommentAddForm.tsx index a1f8c6758b1..e187285d00c 100644 --- a/src/explore-education-statistics-admin/src/components/comments/CommentAddForm.tsx +++ b/src/explore-education-statistics-admin/src/components/comments/CommentAddForm.tsx @@ -3,9 +3,9 @@ import { useCommentsContext } from '@admin/contexts/CommentsContext'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import usePinElementToContainer from '@common/hooks/usePinElementToContainer'; import Yup from '@common/validation/yup'; import React, { RefObject, useEffect, useRef } from 'react'; @@ -66,12 +66,12 @@ export default function CommentAddForm({ > {({ formState }) => { return ( - - + label="Comment" hideLabel name="content" @@ -95,7 +95,7 @@ export default function CommentAddForm({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/components/comments/CommentEditForm.tsx b/src/explore-education-statistics-admin/src/components/comments/CommentEditForm.tsx index d92cb9059e1..845b7312e93 100644 --- a/src/explore-education-statistics-admin/src/components/comments/CommentEditForm.tsx +++ b/src/explore-education-statistics-admin/src/components/comments/CommentEditForm.tsx @@ -3,9 +3,9 @@ import { Comment } from '@admin/services/types/content'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import useMounted from '@common/hooks/useMounted'; import Yup from '@common/validation/yup'; import React, { useRef } from 'react'; @@ -56,12 +56,12 @@ export default function CommentEditForm({ > {({ formState }) => { return ( - - + hideLabel label="Comment" name="content" @@ -74,7 +74,7 @@ export default function CommentEditForm({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/components/editable/EditableContentForm.tsx b/src/explore-education-statistics-admin/src/components/editable/EditableContentForm.tsx index 150f78ba6b6..385f5a9abce 100644 --- a/src/explore-education-statistics-admin/src/components/editable/EditableContentForm.tsx +++ b/src/explore-education-statistics-admin/src/components/editable/EditableContentForm.tsx @@ -1,7 +1,7 @@ import { useCommentsContext } from '@admin/contexts/CommentsContext'; import CommentsWrapper from '@admin/components/comments/CommentsWrapper'; import styles from '@admin/components/editable/EditableContentForm.module.scss'; -import RHFFormFieldEditor from '@admin/components/form/RHFFormFieldEditor'; +import FormFieldEditor from '@admin/components/form/FormFieldEditor'; import { Element, Node, @@ -14,8 +14,8 @@ import { } from '@admin/utils/ckeditor/CustomUploadAdapter'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import useAsyncCallback from '@common/hooks/useAsyncCallback'; import useToggle from '@common/hooks/useToggle'; import logger from '@common/services/logger'; @@ -198,12 +198,12 @@ const EditableContentForm = ({ {({ formState }) => { const isSaving = formState.isSubmitting || isAutoSaving; return ( - - + allowComments={allowComments} altTextError={altTextError} error={ @@ -244,7 +244,7 @@ const EditableContentForm = ({ text="Saving" /> - + ); }} diff --git a/src/explore-education-statistics-admin/src/components/editable/EditableEmbedForm.tsx b/src/explore-education-statistics-admin/src/components/editable/EditableEmbedForm.tsx index 15648b1442b..1e85a34d967 100644 --- a/src/explore-education-statistics-admin/src/components/editable/EditableEmbedForm.tsx +++ b/src/explore-education-statistics-admin/src/components/editable/EditableEmbedForm.tsx @@ -3,9 +3,9 @@ import ButtonGroup from '@common/components/ButtonGroup'; import Yup from '@common/validation/yup'; import { mapFieldErrors } from '@common/validation/serverValidations'; import { useConfig } from '@admin/contexts/ConfigContext'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import React, { useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -66,14 +66,14 @@ const EditableEmbedForm = ({ initialValues={initialValues} validationSchema={validationSchema} > - - +
+ name="title" hint="This will show to users of assistive technology, it does not show on the release page" label="Title" /> - + name="url" hint="Embedded dashboards must be hosted on the DfE Shiny apps domain (https://department-for-education.shinyapps.io/)" label="URL" @@ -84,7 +84,7 @@ const EditableEmbedForm = ({ - + ); }; diff --git a/src/explore-education-statistics-admin/src/components/editable/FeaturedTableLinkInsertForm.tsx b/src/explore-education-statistics-admin/src/components/editable/FeaturedTableLinkInsertForm.tsx index 0f68056452c..51301841abc 100644 --- a/src/explore-education-statistics-admin/src/components/editable/FeaturedTableLinkInsertForm.tsx +++ b/src/explore-education-statistics-admin/src/components/editable/FeaturedTableLinkInsertForm.tsx @@ -4,10 +4,10 @@ import { useReleaseContentState } from '@admin/pages/release/content/contexts/Re import { FeaturedTableLink } from '@admin/types/ckeditor'; import Button from '@common/components/Button'; import FormComboBox from '@common/components/form/FormComboBox'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import createRHFErrorHelper from '@common/components/form/rhf/validation/createRHFErrorHelper'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import createErrorHelper from '@common/components/form/validation/createErrorHelper'; import ButtonGroup from '@common/components/ButtonGroup'; import useDebouncedCallback from '@common/hooks/useDebouncedCallback'; import Yup from '@common/validation/yup'; @@ -62,14 +62,14 @@ export default function FeaturedTableLinkInsertForm({ return ( {({ formState, getValues, handleSubmit, setValue, trigger }) => { - const { getError } = createRHFErrorHelper({ + const { getError } = createErrorHelper({ errors: formState.errors, touchedFields: formState.touchedFields, isSubmitted: true, }); return ( - {getValues('dataBlockParentId') && ( - + formGroupClass="govuk-!-margin-top-5 govuk-!-margin-bottom-2" id="text" label="Link text" @@ -138,7 +138,7 @@ export default function FeaturedTableLinkInsertForm({ )} - + ); }} diff --git a/src/explore-education-statistics-admin/src/components/editable/GlossaryItemInsertForm.tsx b/src/explore-education-statistics-admin/src/components/editable/GlossaryItemInsertForm.tsx index 203637b6fe4..b4ae0fc4cbc 100644 --- a/src/explore-education-statistics-admin/src/components/editable/GlossaryItemInsertForm.tsx +++ b/src/explore-education-statistics-admin/src/components/editable/GlossaryItemInsertForm.tsx @@ -3,10 +3,10 @@ import glossaryQueries from '@admin/queries/glossaryQueries'; import { GlossaryItem } from '@admin/types/ckeditor'; import Button from '@common/components/Button'; import FormComboBox from '@common/components/form/FormComboBox'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import createRHFErrorHelper from '@common/components/form/rhf/validation/createRHFErrorHelper'; -import FormProvider from '@common/components/form/rhf/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import createErrorHelper from '@common/components/form/validation/createErrorHelper'; +import FormProvider from '@common/components/form/FormProvider'; import ButtonGroup from '@common/components/ButtonGroup'; import useDebouncedCallback from '@common/hooks/useDebouncedCallback'; import { GlossaryEntry } from '@common/services/types/glossary'; @@ -72,7 +72,7 @@ export default function GlossaryItemInsertForm({ onCancel, onSubmit }: Props) { return ( {({ formState, getValues, handleSubmit, setValue, trigger }) => { - const { getError } = createRHFErrorHelper({ + const { getError } = createErrorHelper({ errors: formState.errors, touchedFields: formState.touchedFields, isSubmitted: true, @@ -81,7 +81,7 @@ export default function GlossaryItemInsertForm({ onCancel, onSubmit }: Props) { const slug = getValues('slug'); return ( -

Slug: {slug}

- + formGroupClass="govuk-!-margin-top-5 govuk-!-margin-bottom-2" id="text" label="Link text" @@ -139,7 +139,7 @@ export default function GlossaryItemInsertForm({ onCancel, onSubmit }: Props) { Cancel -
+ ); }}
diff --git a/src/explore-education-statistics-admin/src/components/form/FormFieldEditor.tsx b/src/explore-education-statistics-admin/src/components/form/FormFieldEditor.tsx index 59c94db1520..fffb29c2269 100644 --- a/src/explore-education-statistics-admin/src/components/form/FormFieldEditor.tsx +++ b/src/explore-education-statistics-admin/src/components/form/FormFieldEditor.tsx @@ -1,186 +1,124 @@ -import { Element, Node } from '@admin/types/ckeditor'; +import { Element } from '@admin/types/ckeditor'; import FormEditor, { EditorElementsHandler, FormEditorProps, } from '@admin/components/form/FormEditor'; +import { InvalidUrl } from '@admin/components/editable/EditableContentForm'; import { useFormIdContext } from '@common/components/form/contexts/FormIdContext'; import FormGroup from '@common/components/form/FormGroup'; import { OmitStrict } from '@common/types'; -import createErrorHelper from '@common/validation/createErrorHelper'; import WarningMessage from '@common/components/WarningMessage'; -import useToggle from '@common/hooks/useToggle'; -import Yup from '@common/validation/yup'; -import { Field, FieldProps } from 'formik'; -import React, { useRef, useState } from 'react'; - -interface InvalidUrl { - text: string; - url: string; -} +import useRegister from '@common/components/form/hooks/useRegister'; +import getErrorMessage from '@common/components/form/util/getErrorMessage'; +import React, { useRef } from 'react'; +import { + FieldValues, + Path, + PathValue, + useFormContext, + useWatch, +} from 'react-hook-form'; export const elementsFieldName = (name: string) => `__${name}`; -type Props = { +export interface Props + extends OmitStrict { + altTextError?: string; + name: Path; formGroupClass?: string; id?: string; - name: keyof FormValues | string; - shouldValidateAltText?: boolean; + invalidLinkErrors?: InvalidUrl[]; shouldValidateLinks?: boolean; showError?: boolean; testId?: string; onBlur?: (isDirty: boolean) => void; -} & OmitStrict; + onChange?: (elements?: Element[]) => void; +} -function FormFieldEditor({ +export default function FormFieldEditor({ + altTextError, error, formGroupClass, id, + invalidLinkErrors = [], name, - shouldValidateAltText = true, - shouldValidateLinks = true, showError = true, testId, onBlur, + onChange, ...props -}: Props) { +}: Props) { + const { + formState: { errors }, + register, + getValues, + setValue, + getFieldState, + } = useFormContext(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { ref, ...field } = useRegister(name, register); + const value = useWatch({ name }) || ''; const { fieldId } = useFormIdContext(); const elements = useRef([]); - const [altTextError, toggleAltTextError] = useToggle(false); - const [invalidLinkErrors, setInvalidLinkErrors] = useState([]); - - function validateAltText(els: Element[]): string { - const hasInvalidImage = els.some( - element => - isInvalidImage(element) || - Array.from(element.getChildren()).some(child => isInvalidImage(child)), - ); - toggleAltTextError(hasInvalidImage); - return hasInvalidImage ? 'All images must have alternative text. ' : ''; - } - - function validateLinks(els: Element[]): string { - const invalidLinks = getInvalidLinks(els); - setInvalidLinkErrors(invalidLinks); - return invalidLinks.length - ? `${ - invalidLinks.length === 1 - ? '1 link has an invalid URL.' - : `${invalidLinks.length} links have invalid URLs.` - }` - : ''; - } + const handleElements: EditorElementsHandler = nextElements => { + elements.current = nextElements; + }; + const errorMessage = error || getErrorMessage(errors, name, showError); return ( - { - const invalidLinksError = shouldValidateLinks - ? validateLinks(elements.current) - : ''; - const invalidAltTextError = shouldValidateAltText - ? validateAltText(elements.current) - : ''; - return `${invalidAltTextError}${invalidLinksError}`; - }} - > - {({ field, form }: FieldProps) => { - const { getError } = createErrorHelper(form); - - let errorMessage = error || getError(name); - - if (!showError) { - errorMessage = ''; - } - - const handleElements: EditorElementsHandler = nextElements => { - elements.current = nextElements; - }; - - return ( - - {altTextError && } - - {invalidLinkErrors.length > 0 && ( - - The following links have invalid URLs: -
    - {invalidLinkErrors.map(link => ( -
  • - {link?.text} ({link?.url}) -
  • - ))} -
-
- )} - - { - form.setFieldTouched(name as string, true); - if (onBlur) { - onBlur(form.dirty); - } - }} - onElementsChange={handleElements} - onElementsReady={handleElements} - onChange={value => { - form.setFieldValue(name as string, value); - }} - error={errorMessage} - /> -
- ); - }} -
+ + {altTextError && } + + {invalidLinkErrors.length > 0 && ( + + The following links have invalid URLs: +
    + {invalidLinkErrors.map(link => ( +
  • + {link?.text} ({link?.url}) +
  • + ))} +
+
+ )} + + { + const currentValue = getValues(name); + setValue(name, currentValue, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }); + + if (onBlur) { + const fieldState = getFieldState(name); + onBlur(fieldState.isDirty); + } + }} + onElementsChange={handleElements} + onElementsReady={handleElements} + onChange={nextValue => { + setValue( + name, + nextValue as PathValue>, + { shouldDirty: true, shouldTouch: true, shouldValidate: true }, + ); + + onChange?.(elements.current); + }} + error={errorMessage} + /> +
); } -export default FormFieldEditor; - -function isInvalidImage(element: Element | Node) { - return ( - (element.name === 'imageBlock' || element.name === 'imageInline') && - !element.getAttribute('alt') - ); -} - -function getInvalidLinks(elements: Element[]) { - return elements - .flatMap(element => - Array.from(element.getChildren()).flatMap(child => child), - ) - .reduce((acc, el) => { - if (!el.getAttribute('linkHref')) { - return acc; - } - const jsonEl = el.toJSON(); - const attributes = jsonEl.attributes as Record; - const url = attributes.linkHref as string; - - try { - // exclude anchor links, localhost and emails as they fail Yup url validation. - if ( - url && - !url.startsWith('#') && - !url.startsWith('http://localhost') && - !url.startsWith('mailto:') - ) { - Yup.string().url().validateSync(url.trim()); - } - } catch { - acc.push({ - text: jsonEl.data as string, - url, - }); - } - return acc; - }, []); -} - export function AltTextWarningMessage() { return ( diff --git a/src/explore-education-statistics-admin/src/components/form/FormFieldThemeTopicSelect.tsx b/src/explore-education-statistics-admin/src/components/form/FormFieldThemeTopicSelect.tsx index 7f25e76b324..1e724ca0595 100644 --- a/src/explore-education-statistics-admin/src/components/form/FormFieldThemeTopicSelect.tsx +++ b/src/explore-education-statistics-admin/src/components/form/FormFieldThemeTopicSelect.tsx @@ -2,7 +2,7 @@ import FormThemeTopicSelect, { FormThemeTopicSelectProps, } from '@admin/components/form/FormThemeTopicSelect'; import { OmitStrict } from '@common/types'; -import getErrorMessage from '@common/components/form/rhf/util/getErrorMessage'; +import getErrorMessage from '@common/components/form/util/getErrorMessage'; import React from 'react'; import { FieldValues, diff --git a/src/explore-education-statistics-admin/src/components/form/RHFFormFieldEditor.tsx b/src/explore-education-statistics-admin/src/components/form/RHFFormFieldEditor.tsx deleted file mode 100644 index 138edb4a5d9..00000000000 --- a/src/explore-education-statistics-admin/src/components/form/RHFFormFieldEditor.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Element } from '@admin/types/ckeditor'; -import FormEditor, { - EditorElementsHandler, - FormEditorProps, -} from '@admin/components/form/FormEditor'; -import { InvalidUrl } from '@admin/components/editable/EditableContentForm'; -import { useFormIdContext } from '@common/components/form/contexts/FormIdContext'; -import FormGroup from '@common/components/form/FormGroup'; -import { OmitStrict } from '@common/types'; -import WarningMessage from '@common/components/WarningMessage'; -import useRegister from '@common/components/form/rhf/hooks/useRegister'; -import getErrorMessage from '@common/components/form/rhf/util/getErrorMessage'; -import React, { useRef } from 'react'; -import { - FieldValues, - Path, - PathValue, - useFormContext, - useWatch, -} from 'react-hook-form'; - -export const elementsFieldName = (name: string) => `__${name}`; - -export interface Props - extends OmitStrict { - altTextError?: string; - name: Path; - formGroupClass?: string; - id?: string; - invalidLinkErrors?: InvalidUrl[]; - shouldValidateLinks?: boolean; - showError?: boolean; - testId?: string; - onBlur?: (isDirty: boolean) => void; - onChange?: (elements?: Element[]) => void; -} - -export default function RHFFormFieldEditor({ - altTextError, - error, - formGroupClass, - id, - invalidLinkErrors = [], - name, - showError = true, - testId, - onBlur, - onChange, - ...props -}: Props) { - const { - formState: { errors }, - register, - getValues, - setValue, - getFieldState, - } = useFormContext(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ref, ...field } = useRegister(name, register); - const value = useWatch({ name }) || ''; - const { fieldId } = useFormIdContext(); - const elements = useRef([]); - - const handleElements: EditorElementsHandler = nextElements => { - elements.current = nextElements; - }; - const errorMessage = error || getErrorMessage(errors, name, showError); - - return ( - - {altTextError && } - - {invalidLinkErrors.length > 0 && ( - - The following links have invalid URLs: -
    - {invalidLinkErrors.map(link => ( -
  • - {link?.text} ({link?.url}) -
  • - ))} -
-
- )} - - { - const currentValue = getValues(name); - setValue(name, currentValue, { - shouldDirty: true, - shouldTouch: true, - shouldValidate: true, - }); - - if (onBlur) { - const fieldState = getFieldState(name); - onBlur(fieldState.isDirty); - } - }} - onElementsChange={handleElements} - onElementsReady={handleElements} - onChange={nextValue => { - setValue( - name, - nextValue as PathValue>, - { shouldDirty: true, shouldTouch: true, shouldValidate: true }, - ); - - onChange?.(elements.current); - }} - error={errorMessage} - /> -
- ); -} - -export function AltTextWarningMessage() { - return ( - - Alternative text must be added for images, for guidance see{' '} - - W3C tips on writing alternative text - - .
- Images without alternative text are outlined in red. -
- ); -} diff --git a/src/explore-education-statistics-admin/src/pages/legacy-releases/components/ReleaseSeriesLegacyLinkForm.tsx b/src/explore-education-statistics-admin/src/pages/legacy-releases/components/ReleaseSeriesLegacyLinkForm.tsx index 23ab698c9f5..9a800dd603f 100644 --- a/src/explore-education-statistics-admin/src/pages/legacy-releases/components/ReleaseSeriesLegacyLinkForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/legacy-releases/components/ReleaseSeriesLegacyLinkForm.tsx @@ -1,8 +1,8 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import Yup from '@common/validation/yup'; import React, { ReactNode } from 'react'; @@ -37,14 +37,14 @@ const ReleaseSeriesLegacyLinkForm = ({ > {({ formState }) => { return ( - - +
+ name="description" label="Description" className="govuk-!-width-two-thirds" /> - + name="url" label="URL" className="govuk-!-width-two-thirds" @@ -56,7 +56,7 @@ const ReleaseSeriesLegacyLinkForm = ({ {cancelButton} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/methodology/adopt-methodology/components/AdoptMethodologyForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/adopt-methodology/components/AdoptMethodologyForm.tsx index f3d2be4ea0e..f2ac96a939d 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/adopt-methodology/components/AdoptMethodologyForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/adopt-methodology/components/AdoptMethodologyForm.tsx @@ -1,4 +1,4 @@ -import RHFFormFieldRadioSearchGroup from '@common/components/form/rhf/RHFFormFieldRadioSearchGroup'; +import FormFieldRadioSearchGroup from '@common/components/form/FormFieldRadioSearchGroup'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Button from '@common/components/Button'; import Details from '@common/components/Details'; @@ -12,8 +12,8 @@ import { MethodologyVersion } from '@admin/services/methodologyService'; import Tag from '@common/components/Tag'; import TagGroup from '@common/components/TagGroup'; import { RadioOption } from '@common/components/form/FormRadioGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -89,8 +89,8 @@ const AdoptMethodologyForm = ({ methodologies, onCancel, onSubmit }: Props) => { initialValues={{ methodologyId: '' }} validationSchema={validationSchema} > - - + { Cancel - + ); }; diff --git a/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx index fbde4381320..2eeb265410c 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx @@ -1,10 +1,10 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; @@ -64,8 +64,8 @@ const MethodologySummaryForm = ({ > {form => { return ( - - +
+ legend="Methodology title" name="titleType" order={[]} @@ -83,7 +83,7 @@ const MethodologySummaryForm = ({ label: 'Set an alternative title', value: 'alternative', conditional: ( - + label="Enter methodology title" name="title" /> @@ -95,7 +95,7 @@ const MethodologySummaryForm = ({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesAddForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesAddForm.tsx index 5b8f10a18b9..4f8e156dd1d 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesAddForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesAddForm.tsx @@ -1,8 +1,8 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -29,8 +29,8 @@ export default function MethodologyNotesAddForm({ onCancel, onSubmit }: Props) { initialValues={{ content: '' }} validationSchema={validationSchema} > - - +
+ label="New methodology note" name="content" rows={3} @@ -42,7 +42,7 @@ export default function MethodologyNotesAddForm({ onCancel, onSubmit }: Props) { Cancel - + ); } diff --git a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesEditForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesEditForm.tsx index 27d794a415e..ac0906ee4c3 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesEditForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/content/components/MethodologyNotesEditForm.tsx @@ -1,9 +1,9 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import RHFFormFieldDateInput from '@common/components/form/rhf/RHFFormFieldDateInput'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormFieldDateInput from '@common/components/form/FormFieldDateInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -39,13 +39,13 @@ export default function MethodologyNotesEditForm({ initialValues={initialValues} validationSchema={validationSchema} > - - +
+ name="displayDate" legend="Edit date" legendSize="s" /> - + label="Edit methodology note" name="content" rows={3} @@ -57,7 +57,7 @@ export default function MethodologyNotesEditForm({ Cancel - + ); } diff --git a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/components/MethodologyStatusForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/components/MethodologyStatusForm.tsx index 136b1e7e2de..14d417aa7c6 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/components/MethodologyStatusForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/components/MethodologyStatusForm.tsx @@ -9,12 +9,12 @@ import { IdTitlePair } from '@admin/services/types/common'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import { FormSelect } from '@common/components/form'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -80,10 +80,10 @@ const MethodologyStatusForm = ({ {({ watch }) => { const status = watch('status'); return ( - +

Edit methodology status

- + legend="Status" hint={ isPublished && @@ -109,7 +109,7 @@ const MethodologyStatusForm = ({ ]} order={[]} /> - + name="latestInternalReleaseNote" className="govuk-!-width-one-half" label="Internal note" @@ -117,7 +117,7 @@ const MethodologyStatusForm = ({ rows={2} /> {status === 'Approved' && ( - + name="publishingStrategy" legend="When to publish" legendSize="m" @@ -128,7 +128,7 @@ const MethodologyStatusForm = ({ label: 'With a specific release', value: 'WithRelease', conditional: ( - + label="Select release" name="withReleaseId" order={FormSelect.unordered} @@ -154,7 +154,7 @@ const MethodologyStatusForm = ({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/methodology/external-methodology/components/ExternalMethodologyForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/external-methodology/components/ExternalMethodologyForm.tsx index a7cfb61ce0b..b242cb5fbf0 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/external-methodology/components/ExternalMethodologyForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/external-methodology/components/ExternalMethodologyForm.tsx @@ -1,12 +1,12 @@ import { FormFieldset, FormGroup } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import ButtonGroup from '@common/components/ButtonGroup'; import Button from '@common/components/Button'; import { ExternalMethodology } from '@admin/services/publicationService'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; interface Props { initialValues?: ExternalMethodology; @@ -51,19 +51,19 @@ const ExternalMethodologyForm = ({ } validationSchema={validationSchema} > - +
- - - +
); }; diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationContactForm.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationContactForm.tsx index c19c0bbb0dc..a06de4db8cf 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationContactForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationContactForm.tsx @@ -4,9 +4,9 @@ import publicationService, { import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import ModalConfirm from '@common/components/ModalConfirm'; import useToggle from '@common/hooks/useToggle'; import Yup from '@common/validation/yup'; @@ -82,29 +82,29 @@ export default function PublicationContactForm({ {form => { return ( <> - toggleConfirmModal.on()} > - + name="teamName" label="Team name" className="govuk-!-width-one-half" /> - + name="teamEmail" label="Team email" className="govuk-!-width-one-half" /> - + name="contactName" label="Contact name" className="govuk-!-width-one-half" /> - + name="contactTelNo" label="Contact telephone (optional)" className="govuk-!-width-one-half" @@ -114,7 +114,7 @@ export default function PublicationContactForm({ Cancel - + { return ( <> - toggleConfirmModal.on()}> +
toggleConfirmModal.on()}> {canUpdatePublication && ( - + name="title" label="Publication title" className="govuk-!-width-one-half" @@ -113,7 +113,7 @@ export default function PublicationDetailsForm({ )} {canUpdatePublicationSummary && ( - + name="summary" label="Publication summary" className="govuk-!-width-one-half" @@ -139,7 +139,7 @@ export default function PublicationDetailsForm({ legend="Archive this publication" legendSize="s" > - + className="govuk-!-width-one-half" hint="If superseded by a publication with a live release, this will archive the current publication immediately" label="Superseding publication" @@ -157,7 +157,7 @@ export default function PublicationDetailsForm({ Cancel - + {showConfirmModal && ( - - +
+ label="Publication title" name="title" className="govuk-!-width-two-thirds" /> - + label="Publication summary" name="summary" className="govuk-!-width-one-half" @@ -140,25 +140,25 @@ export default function PublicationForm({ legendSize="m" hint="They will be the main point of contact for data and methodology enquiries for this publication and its releases." > - + name="teamName" label="Team name" className="govuk-!-width-one-half" /> - + name="teamEmail" label="Team email address" className="govuk-!-width-one-half" /> - + name="contactName" label="Contact name" className="govuk-!-width-one-half" /> - + name="contactTelNo" label="Contact telephone (optional)" width={10} @@ -170,7 +170,7 @@ export default function PublicationForm({ {cancelButton} - + ); } diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationInviteNewUsersForm.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationInviteNewUsersForm.tsx index 95e6b9d8b55..b53246dfd74 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationInviteNewUsersForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationInviteNewUsersForm.tsx @@ -8,10 +8,10 @@ import userService from '@admin/services/userService'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldCheckboxGroup from '@common/components/form/rhf/RHFFormFieldCheckboxGroup'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldCheckboxGroup from '@common/components/form/FormFieldCheckboxGroup'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; @@ -94,13 +94,13 @@ const PublicationInviteNewUsersForm = ({ > {({ formState }) => { return ( - - + - + name="releaseIds" legend="Select which releases you wish the user to have access" legendSize="m" @@ -135,7 +135,7 @@ const PublicationInviteNewUsersForm = ({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationReleaseContributorsForm.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationReleaseContributorsForm.tsx index af8badc748e..30fe5df73d1 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationReleaseContributorsForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationReleaseContributorsForm.tsx @@ -9,9 +9,9 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; import WarningMessage from '@common/components/WarningMessage'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFFormFieldCheckboxGroup from '@common/components/form/rhf/RHFFormFieldCheckboxGroup'; +import Form from '@common/components/form/Form'; +import FormProvider from '@common/components/form/FormProvider'; +import FormFieldCheckboxGroup from '@common/components/form/FormFieldCheckboxGroup'; import React from 'react'; import { generatePath, useHistory } from 'react-router-dom'; @@ -86,8 +86,8 @@ const PublicationReleaseContributorsForm = ({ {({ formState }) => { return ( - - +
+ name="userIds" legend="Select contributors for this release" legendSize="m" @@ -121,7 +121,7 @@ const PublicationReleaseContributorsForm = ({ Cancel - + ); }}
diff --git a/src/explore-education-statistics-admin/src/pages/release/components/ReleaseStatusForm.tsx b/src/explore-education-statistics-admin/src/pages/release/components/ReleaseStatusForm.tsx index 71f43bf4c92..9bb44b5f112 100644 --- a/src/explore-education-statistics-admin/src/pages/release/components/ReleaseStatusForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/components/ReleaseStatusForm.tsx @@ -6,12 +6,12 @@ import { import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldCheckbox from '@common/components/form/rhf/RHFFormFieldCheckbox'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import RHFFormFieldDateInput from '@common/components/form/rhf/RHFFormFieldDateInput'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldCheckbox from '@common/components/form/FormFieldCheckbox'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import FormFieldDateInput from '@common/components/form/FormFieldDateInput'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import FormattedDate from '@common/components/FormattedDate'; import ModalConfirm from '@common/components/ModalConfirm'; import WarningMessage from '@common/components/WarningMessage'; @@ -233,8 +233,8 @@ const ReleaseStatusForm = ({ const approvalStatus = watch('approvalStatus'); return ( <> - - +
+ legend="Status" name="approvalStatus" order={[]} @@ -267,7 +267,7 @@ const ReleaseStatusForm = ({ }} /> - + name="internalReleaseNote" className="govuk-!-width-one-half" label="Internal note" @@ -277,12 +277,12 @@ const ReleaseStatusForm = ({ {approvalStatus === 'Approved' && release.amendment && ( <> - - + name="updatePublishedDate" label="Update published date" conditional={ @@ -296,7 +296,7 @@ const ReleaseStatusForm = ({ )} {approvalStatus === 'Approved' && ( - + name="publishMethod" legend="When to publish" legendSize="m" @@ -315,7 +315,7 @@ const ReleaseStatusForm = ({ date. )} - + name="publishScheduled" legend="Publish date" legendSize="s" @@ -347,7 +347,7 @@ const ReleaseStatusForm = ({ /> )} - + name="nextReleaseDate" legend="Next release expected (optional)" legendSize="m" @@ -387,7 +387,7 @@ const ReleaseStatusForm = ({ Cancel - + +
- + label="Type" name="timePeriodCoverageCode" optGroups={timePeriodOptions} /> - + name="timePeriodCoverageStartYear" label={` ${ @@ -159,7 +159,7 @@ export default function ReleaseSummaryForm({ width={4} /> - + legend="Release type" name="releaseType" options={permittedReleaseTypes.map(type => ({ @@ -169,7 +169,7 @@ export default function ReleaseSummaryForm({ /> {templateRelease && ( - + legend="Select template" name="templateReleaseId" options={[ @@ -188,7 +188,7 @@ export default function ReleaseSummaryForm({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatDataBlockForm.tsx b/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatDataBlockForm.tsx index e4a1ea30cc8..09d9938030c 100644 --- a/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatDataBlockForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatDataBlockForm.tsx @@ -2,10 +2,10 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import styles from '@common/modules/find-statistics/components/KeyStat.module.scss'; import KeyStatTile from '@common/modules/find-statistics/components/KeyStatTile'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import { KeyStatisticDataBlock } from '@common/services/publicationService'; import Yup from '@common/validation/yup'; import React from 'react'; @@ -58,7 +58,7 @@ export default function EditableKeyStatDataBlockForm({ > {({ formState }) => { return ( - @@ -69,19 +69,19 @@ export default function EditableKeyStatDataBlockForm({ value={statistic} isReordering={isReordering} > - + name="trend" label={Trend} /> - + formGroupClass="govuk-!-margin-top-2" name="guidanceTitle" label="Guidance title" /> - + label="Guidance text" name="guidanceText" rows={3} @@ -95,7 +95,7 @@ export default function EditableKeyStatDataBlockForm({ Cancel - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatTextForm.tsx b/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatTextForm.tsx index a3fed26f189..75966684f56 100644 --- a/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatTextForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/content/components/EditableKeyStatTextForm.tsx @@ -1,9 +1,9 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import styles from '@common/modules/find-statistics/components/KeyStat.module.scss'; import { KeyStatisticText } from '@common/services/publicationService'; import React from 'react'; @@ -61,7 +61,7 @@ export default function EditableKeyStatTextForm({ > {({ formState }) => { return ( -
- + name="title" className={classNames({ 'govuk-!-width-one-third': isReordering, })} label={Title} /> - + name="statistic" className={classNames({ 'govuk-!-width-one-third': isReordering, })} label={Statistic} /> - + name="trend" className={classNames({ 'govuk-!-width-one-third': isReordering, @@ -93,7 +93,7 @@ export default function EditableKeyStatTextForm({ />
- + formGroupClass="govuk-!-margin-top-2" name="guidanceTitle" className={classNames({ @@ -102,7 +102,7 @@ export default function EditableKeyStatTextForm({ label="Guidance title" /> - + label="Guidance text" name="guidanceText" rows={3} @@ -116,7 +116,7 @@ export default function EditableKeyStatTextForm({ Cancel -
+ ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/content/components/RelatedPagesSection.tsx b/src/explore-education-statistics-admin/src/pages/release/content/components/RelatedPagesSection.tsx index 5b6b344e44a..baac52c2923 100644 --- a/src/explore-education-statistics-admin/src/pages/release/content/components/RelatedPagesSection.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/content/components/RelatedPagesSection.tsx @@ -4,9 +4,9 @@ import releaseContentRelatedInformationService from '@admin/services/releaseCont import { EditableRelease } from '@admin/services/releaseContentService'; import Button from '@common/components/Button'; import { FormFieldset } from '@common/components/form'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import useToggle from '@common/hooks/useToggle'; import ButtonGroup from '@common/components/ButtonGroup'; import { BasicLink } from '@common/services/publicationService'; @@ -72,20 +72,17 @@ export default function RelatedPagesSection({ release }: Props) { .required('Enter a link URL'), })} > - +
- + label="Title" name="description" /> - - label="Link URL" - name="url" - /> + label="Link URL" name="url" /> diff --git a/src/explore-education-statistics-admin/src/pages/release/content/components/ReleaseNoteForm.tsx b/src/explore-education-statistics-admin/src/pages/release/content/components/ReleaseNoteForm.tsx index 916de94158c..0819cd87df1 100644 --- a/src/explore-education-statistics-admin/src/pages/release/content/components/ReleaseNoteForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/content/components/ReleaseNoteForm.tsx @@ -1,9 +1,9 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldDateInput from '@common/components/form/rhf/RHFFormFieldDateInput'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldDateInput from '@common/components/form/FormFieldDateInput'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -48,15 +48,15 @@ export default function ReleaseNoteForm({ initialValues={initialValues} validationSchema={validationSchema} > - +
{isEditing && ( - + name="on" legend="Edit date" legendSize="s" /> )} - + label={isEditing ? 'Edit release note' : 'New release note'} name="reason" rows={3} @@ -70,7 +70,7 @@ export default function ReleaseNoteForm({ Cancel - + ); } diff --git a/src/explore-education-statistics-admin/src/pages/release/data/ReleaseDataFilePage.tsx b/src/explore-education-statistics-admin/src/pages/release/data/ReleaseDataFilePage.tsx index 04b56644068..eabe7adef74 100644 --- a/src/explore-education-statistics-admin/src/pages/release/data/ReleaseDataFilePage.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/data/ReleaseDataFilePage.tsx @@ -3,9 +3,9 @@ import { ReleaseRouteParams, releaseDataRoute, } from '@admin/routes/releaseRoutes'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import WarningMessage from '@common/components/WarningMessage'; import useAsyncRetry from '@common/hooks/useAsyncRetry'; import React from 'react'; @@ -68,15 +68,15 @@ export default function ReleaseDataFilePage({ title: Yup.string().required('Enter a title'), })} > - - +
+ className="govuk-!-width-two-thirds" label="Title" name="title" /> - + ) : ( Could not load data file details diff --git a/src/explore-education-statistics-admin/src/pages/release/data/components/AncillaryFileForm.tsx b/src/explore-education-statistics-admin/src/pages/release/data/components/AncillaryFileForm.tsx index ec5c062391b..e47434e8d4a 100644 --- a/src/explore-education-statistics-admin/src/pages/release/data/components/AncillaryFileForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/data/components/AncillaryFileForm.tsx @@ -2,11 +2,11 @@ import { AncillaryFile } from '@admin/services/releaseAncillaryFileService'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFFormFieldFileInput from '@common/components/form/rhf/RHFFormFieldFileInput'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import FormFieldFileInput from '@common/components/form/FormFieldFileInput'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import Form from '@common/components/form/Form'; import LoadingSpinner from '@common/components/LoadingSpinner'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; @@ -111,22 +111,22 @@ export default function AncillaryFileForm({ > {({ formState, reset }) => { return ( - - +
+ className="govuk-!-width-one-half" disabled={formState.isSubmitting} label="Title" name="title" /> - + className="govuk-!-width-one-half" disabled={formState.isSubmitting} label="Summary" name="summary" /> - + disabled={formState.isSubmitting} hint="Maximum file size 2GB" label={isEditing ? 'Upload new file' : 'Upload file'} @@ -157,7 +157,7 @@ export default function AncillaryFileForm({ text="Saving file" /> - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/data/components/DataFileUploadForm.tsx b/src/explore-education-statistics-admin/src/pages/release/data/components/DataFileUploadForm.tsx index 72b2604c698..316ac961978 100644 --- a/src/explore-education-statistics-admin/src/pages/release/data/components/DataFileUploadForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/data/components/DataFileUploadForm.tsx @@ -2,11 +2,11 @@ import { DataFile } from '@admin/services/releaseDataFileService'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldFileInput from '@common/components/form/rhf/RHFFormFieldFileInput'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldFileInput from '@common/components/form/FormFieldFileInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import LoadingSpinner from '@common/components/LoadingSpinner'; import { FieldMessageMapper, @@ -199,20 +199,20 @@ export default function DataFileUploadForm({ > {({ formState, reset }) => { return ( - +
{formState.isSubmitting && ( )} {!isDataReplacement && ( - + name="subjectTitle" label="Subject title" className="govuk-!-width-two-thirds" /> )} - + name="uploadType" legend="Choose upload method" hint={`Filenames must be under ${MAX_FILENAME_SIZE} characters in length`} @@ -222,13 +222,13 @@ export default function DataFileUploadForm({ value: 'csv', conditional: ( <> - + name="dataFile" label="Upload data file" accept=".csv" /> - + name="metadataFile" label="Upload metadata file" accept=".csv" @@ -241,7 +241,7 @@ export default function DataFileUploadForm({ hint: 'Recommended for larger data files', value: 'zip', conditional: ( - + hint="Must contain both the data and metadata CSV files" name="zipFile" label="Upload ZIP file" @@ -270,7 +270,7 @@ export default function DataFileUploadForm({
- +
); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/data/components/ReleaseDataGuidanceSection.tsx b/src/explore-education-statistics-admin/src/pages/release/data/components/ReleaseDataGuidanceSection.tsx index f3f6740e481..efcc4dc3dae 100644 --- a/src/explore-education-statistics-admin/src/pages/release/data/components/ReleaseDataGuidanceSection.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/data/components/ReleaseDataGuidanceSection.tsx @@ -1,13 +1,13 @@ import releaseDataGuidanceService from '@admin/services/releaseDataGuidanceService'; -import RHFFormFieldEditor from '@admin/components/form/RHFFormFieldEditor'; +import FormFieldEditor from '@admin/components/form/FormFieldEditor'; import Accordion from '@common/components/Accordion'; import AccordionSection from '@common/components/AccordionSection'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import InsetText from '@common/components/InsetText'; import LoadingSpinner from '@common/components/LoadingSpinner'; import ContentHtml from '@common/components/ContentHtml'; @@ -136,9 +136,9 @@ const ReleaseDataGuidanceSection = ({ releaseId, canUpdateRelease }: Props) => { const values = getValues() as DataGuidanceFormValues; return ( - +
{isEditing ? ( - + name="content" label="Main guidance content" /> @@ -175,7 +175,7 @@ const ReleaseDataGuidanceSection = ({ releaseId, canUpdateRelease }: Props) => { dataSet={dataSet} renderContent={() => isEditing ? ( - + label="File guidance content" name={`dataSets.${index}.content`} rows={3} @@ -219,7 +219,7 @@ const ReleaseDataGuidanceSection = ({ releaseId, canUpdateRelease }: Props) => { )} )} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/DataBlockDetailsForm.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/DataBlockDetailsForm.tsx index 325b5d0a40d..d7a3631b594 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/DataBlockDetailsForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/DataBlockDetailsForm.tsx @@ -1,13 +1,13 @@ import Button from '@common/components/Button'; import { FormFieldset, FormGroup } from '@common/components/form'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import { OmitStrict } from '@common/types'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; -import RHFFormFieldCheckbox from '@common/components/form/rhf/RHFFormFieldCheckbox'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormFieldCheckbox from '@common/components/form/FormFieldCheckbox'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import { ObjectSchema } from 'yup'; interface FormValues { @@ -80,18 +80,18 @@ const DataBlockDetailsForm = ({ > {({ formState, getValues }) => { return ( - +

Data block details

- + name="name" label="Name" hint="Name of the data block. This will not be visible to users." className="govuk-!-width-one-half" /> - + name="heading" className="govuk-!-width-two-thirds" label="Table title" @@ -102,7 +102,7 @@ const DataBlockDetailsForm = ({ }} /> - + name="source" label="Source" hint="The data source used to create this data." @@ -115,18 +115,18 @@ const DataBlockDetailsForm = ({ legendSize="s" hint="Checking this option will make this table available as a featured table when the publication is selected via the table builder" > - + name="isHighlight" label="Set as a featured table for this publication" conditional={ <> - + name="highlightName" label="Featured table name" hint="We will show this name to table builder users as a featured table" className="govuk-!-width-two-thirds" /> - + name="highlightDescription" label="Featured table description" hint="Describe the contents of this featured table to table builder users" @@ -145,7 +145,7 @@ const DataBlockDetailsForm = ({ Save data block - +
); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartAxisConfiguration.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartAxisConfiguration.tsx index 60be61790f1..f14f61e6824 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartAxisConfiguration.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartAxisConfiguration.tsx @@ -6,13 +6,13 @@ import Effect from '@common/components/Effect'; import { FormFieldset } from '@common/components/form'; import { RadioOption } from '@common/components/form/FormRadioGroup'; import { SelectOption } from '@common/components/form/FormSelect'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldCheckbox from '@common/components/form/rhf/RHFFormFieldCheckbox'; -import RHFFormFieldNumberInput from '@common/components/form/rhf/RHFFormFieldNumberInput'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldCheckbox from '@common/components/form/FormFieldCheckbox'; +import FormFieldNumberInput from '@common/components/form/FormFieldNumberInput'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import { AxesConfiguration, AxisConfiguration, @@ -185,14 +185,14 @@ const ChartAxisConfiguration = ({ value: 'filters', conditional: ( <> - + label="Select a filter" name="groupByFilter" options={categories} order={[]} /> {isGroupedByFilterWithGroups && ( - + name="groupByFilterGroups" label="Group by filter groups" /> @@ -542,7 +542,7 @@ const ChartAxisConfiguration = ({ const values = watch(); return ( - +
{validationSchema.fields.size && ( - + name="size" min={0} label="Size of axis (pixels)" @@ -573,18 +573,18 @@ const ChartAxisConfiguration = ({ )} {validationSchema.fields.showGrid && ( - + name="showGrid" label="Show grid lines" /> )} {validationSchema.fields.visible && ( - + name="visible" label="Show axis" conditional={ - + label="Displayed unit" name="unit" hint="Leave blank to set default from metadata" @@ -595,7 +595,7 @@ const ChartAxisConfiguration = ({ )} {validationSchema.fields.groupBy && ( - + legend="Group data by" legendSize="s" name="groupBy" @@ -609,9 +609,9 @@ const ChartAxisConfiguration = ({ )} - + - {validationSchema.fields.labelRotated && ( - @@ -631,7 +631,7 @@ const ChartAxisConfiguration = ({
{validationSchema.fields.sortAsc && ( - + name="sortAsc" label="Sort ascending" /> @@ -639,7 +639,7 @@ const ChartAxisConfiguration = ({ )} {validationSchema.fields.tickConfig && ( - + name="tickConfig" legend="Tick display type" legendSize="s" @@ -657,7 +657,7 @@ const ChartAxisConfiguration = ({ label: 'Custom', value: 'custom', conditional: ( - + name="tickSpacing" width={10} label="Every nth value" @@ -679,7 +679,7 @@ const ChartAxisConfiguration = ({ >
{validationSchema.fields.min && ( - + name="min" width={10} label="Minimum value" @@ -687,7 +687,7 @@ const ChartAxisConfiguration = ({ /> )} {validationSchema.fields.max && ( - + name="max" width={10} label="Maximum value" @@ -706,7 +706,7 @@ const ChartAxisConfiguration = ({ legendSize="s" > {validationSchema.fields.min && ( - + isNumberField label="Minimum" name="min" @@ -714,7 +714,7 @@ const ChartAxisConfiguration = ({ /> )} {validationSchema.fields.max && ( - + isNumberField label="Maximum" name="max" @@ -750,7 +750,7 @@ const ChartAxisConfiguration = ({ > {buttons} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartBoundaryLevelsConfiguration.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartBoundaryLevelsConfiguration.tsx index b47efdb74a9..9e06cbb6e50 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartBoundaryLevelsConfiguration.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartBoundaryLevelsConfiguration.tsx @@ -2,9 +2,9 @@ import ChartBuilderSaveActions from '@admin/pages/release/datablocks/components/ import { useChartBuilderFormsContext } from '@admin/pages/release/datablocks/components/chart/contexts/ChartBuilderFormsContext'; import { ChartOptions } from '@admin/pages/release/datablocks/components/chart/reducers/chartBuilderReducer'; import Effect from '@common/components/Effect'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { FullTableMeta } from '@common/modules/table-tool/types/fullTable'; import parseNumber from '@common/utils/number/parseNumber'; import Yup from '@common/validation/yup'; @@ -69,7 +69,7 @@ export default function ChartBoundaryLevelsConfiguration({ {({ formState, watch }) => { const values = watch(); return ( - { onSubmit(normalizeValues(values)); @@ -86,7 +86,7 @@ export default function ChartBoundaryLevelsConfiguration({ onChange={updateForm} onMount={updateForm} /> - + label="Boundary level" hint="Select a version of geographical data to use" name="boundaryLevel" @@ -110,7 +110,7 @@ export default function ChartBoundaryLevelsConfiguration({ > {buttons} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartConfiguration.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartConfiguration.tsx index bca0781bc80..87ac405eb8c 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartConfiguration.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartConfiguration.tsx @@ -3,15 +3,15 @@ import { useChartBuilderFormsContext } from '@admin/pages/release/datablocks/com import { ChartOptions } from '@admin/pages/release/datablocks/components/chart/reducers/chartBuilderReducer'; import Effect from '@common/components/Effect'; import FormGroup from '@common/components/form/FormGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldCheckbox from '@common/components/form/rhf/RHFFormFieldCheckbox'; -import RHFFormFieldFileInput from '@common/components/form/rhf/RHFFormFieldFileInput'; -import RHFFormFieldNumberInput from '@common/components/form/rhf/RHFFormFieldNumberInput'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldCheckbox from '@common/components/form/FormFieldCheckbox'; +import FormFieldFileInput from '@common/components/form/FormFieldFileInput'; +import FormFieldNumberInput from '@common/components/form/FormFieldNumberInput'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import { ChartDefinition, BarChartDataLabelPosition, @@ -26,7 +26,7 @@ import { ValidationProblemDetails } from '@common/services/types/problemDetails' import parseNumber from '@common/utils/number/parseNumber'; import { mapFieldErrors, - rhfConvertServerFieldErrors, + convertServerFieldErrors, } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; import capitalize from 'lodash/capitalize'; @@ -269,7 +269,7 @@ const ChartConfiguration = ({ const values = watch(); if (submitError) { - const fieldErrors = rhfConvertServerFieldErrors( + const fieldErrors = convertServerFieldErrors( submitError, initialChartOptions.current, errorMappings, @@ -285,7 +285,7 @@ const ChartConfiguration = ({ } return ( - { onSubmit(normalizeValues(v)); @@ -310,14 +310,14 @@ const ChartConfiguration = ({ /> {validationSchema.fields.file && ( - + name="file" label="Upload new infographic" accept="image/*" /> )}
- + hint="Communicate the headline message of the chart. For example 'Increase in number of people living alone'." legend="Chart title" legendSize="s" @@ -332,7 +332,7 @@ const ChartConfiguration = ({ label: 'Set an alternative title', value: 'alternative', conditional: ( - + label="Enter chart title" name="title" hint="Use a concise descriptive title that summarises the main message in the chart." @@ -342,7 +342,7 @@ const ChartConfiguration = ({ ]} /> - + label="Subtitle" name="subtitle" hint="The statistical subtitle should say what the data is, the geography the data relates to and the time period shown. @@ -351,7 +351,7 @@ const ChartConfiguration = ({ />
- + className="govuk-!-width-three-quarters" name="alt" label="Alt text" @@ -361,14 +361,14 @@ const ChartConfiguration = ({ /> {validationSchema.fields.stacked && ( - + name="stacked" label="Stacked bars" /> )} {validationSchema.fields.height && ( - + name="height" label="Height (pixels)" width={5} @@ -376,7 +376,7 @@ const ChartConfiguration = ({ )} {validationSchema.fields.width && ( - + name="width" label="Width (pixels)" hint="Leave blank to set as full width" @@ -385,7 +385,7 @@ const ChartConfiguration = ({ )} {validationSchema.fields.barThickness && ( - + name="barThickness" label="Bar thickness (pixels)" width={5} @@ -394,7 +394,7 @@ const ChartConfiguration = ({ {validationSchema.fields.includeNonNumericData && ( - + name="includeNonNumericData" label="Include data sets with non-numerical values" /> @@ -402,7 +402,7 @@ const ChartConfiguration = ({ )} {validationSchema.fields.showDataLabels && ( - + name="showDataLabels" hint={ legendPosition === 'inline' @@ -413,7 +413,7 @@ const ChartConfiguration = ({ showError={!!formState.errors.showDataLabels} conditional={ validationSchema.fields.dataLabelPosition && ( - + label="Data label position" name="dataLabelPosition" order={[]} @@ -431,7 +431,7 @@ const ChartConfiguration = ({ > {buttons} -
+ ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartCustomDataGroupingsConfiguration.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartCustomDataGroupingsConfiguration.tsx index 182c25780c1..a2088bfac41 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartCustomDataGroupingsConfiguration.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartCustomDataGroupingsConfiguration.tsx @@ -1,6 +1,6 @@ import { ChartDataGroupingFormValues } from '@admin/pages/release/datablocks/components/chart/ChartDataGroupingForm'; import Button from '@common/components/Button'; -import RHFFormFieldNumberInput from '@common/components/form/rhf/RHFFormFieldNumberInput'; +import FormFieldNumberInput from '@common/components/form/FormFieldNumberInput'; import Tooltip from '@common/components/Tooltip'; import VisuallyHidden from '@common/components/VisuallyHidden'; import { CustomDataGroup } from '@common/modules/charts/types/chart'; @@ -68,7 +68,7 @@ export default function ChartCustomDataGroupingsConfiguration({ - - + name="numberOfGroups" label="Number of data groups" width={3} @@ -222,7 +222,7 @@ export default function ChartDataGroupingForm({ value: 'Quantiles', hint: 'Data is grouped so that each group has a similar number of data points.', conditional: ( - + name="numberOfGroupsQuantiles" label="Number of data groups" width={3} @@ -274,7 +274,7 @@ export default function ChartDataGroupingForm({ value: 'CopyCustom', hint: 'Copy custom groups from another data set.', conditional: ( - + name="copyCustomGroups" className={styles.selectContainer} label="Copy custom groups from another data set" @@ -290,7 +290,7 @@ export default function ChartDataGroupingForm({ return (
- { const copiedCustomGroups: CustomDataGroup[] = @@ -324,7 +324,7 @@ export default function ChartDataGroupingForm({ }); }} > - + legend="Select a grouping type" legendSize="s" name="type" @@ -341,7 +341,7 @@ export default function ChartDataGroupingForm({ Cancel - +
); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartDataSetsConfiguration.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartDataSetsConfiguration.tsx index 7fbf65e857e..a55fc2b3e5f 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartDataSetsConfiguration.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartDataSetsConfiguration.tsx @@ -11,9 +11,9 @@ import { FormSelect } from '@common/components/form'; import SubmitError from '@common/components/form/util/SubmitError'; import ModalConfirm from '@common/components/ModalConfirm'; import VisuallyHidden from '@common/components/VisuallyHidden'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; +import Form from '@common/components/form/Form'; import { DataSet } from '@common/modules/charts/types/dataSet'; import expandDataSet from '@common/modules/charts/util/expandDataSet'; @@ -145,12 +145,12 @@ const ChartDataSetsConfiguration = ({ timePeriod: Yup.string(), })} > - +
{orderBy(Object.entries(meta.filters), ([_, value]) => value.order) .filter(([, filters]) => filters.options.length > 1) .map(([categoryName, filters]) => ( - 1 && ( - + name="indicator" label="Indicator" formGroupClass={styles.formSelectGroup} @@ -177,7 +177,7 @@ const ChartDataSetsConfiguration = ({ )} {locationOptions.length > 1 && ( - + name="location" label="Location" formGroupClass={styles.formSelectGroup} @@ -188,7 +188,7 @@ const ChartDataSetsConfiguration = ({ )} {meta.timePeriodRange.length > 1 && ( - + name="timePeriod" label="Time period" formGroupClass={styles.formSelectGroup} @@ -201,7 +201,7 @@ const ChartDataSetsConfiguration = ({
- +
{forms.dataSets && diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendConfiguration.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendConfiguration.tsx index 13b711fd2e3..21b94ddeecc 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendConfiguration.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendConfiguration.tsx @@ -2,9 +2,9 @@ import ChartBuilderSaveActions from '@admin/pages/release/datablocks/components/ import { useChartBuilderFormsContext } from '@admin/pages/release/datablocks/components/chart/contexts/ChartBuilderFormsContext'; import Effect from '@common/components/Effect'; import FormSelect, { SelectOption } from '@common/components/form/FormSelect'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { AxisConfiguration, ChartDefinition, @@ -240,7 +240,7 @@ const ChartLegendConfiguration = ({ {({ formState, watch }) => { const values = watch(); return ( - { onSubmit(values); @@ -263,7 +263,7 @@ const ChartLegendConfiguration = ({ /> {validationSchema.fields.position && ( - + name="position" hint={ capabilities.canPositionLegendInline && showDataLabels @@ -297,7 +297,7 @@ const ChartLegendConfiguration = ({ > {buttons} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendItems.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendItems.tsx index bcac093eaf9..2a4bb45a56b 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendItems.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/ChartLegendItems.tsx @@ -10,12 +10,12 @@ import { LegendConfiguration, LegendPosition, } from '@common/modules/charts/types/legend'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import RHFFormFieldColourInput from '@common/components/form/rhf/RHFFormFieldColourInput'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormFieldColourInput from '@common/components/form/FormFieldColourInput'; import Effect from '@common/components/Effect'; import useDebouncedCallback from '@common/hooks/useDebouncedCallback'; import FormFieldset from '@common/components/form/FormFieldset'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import FormSelect, { SelectOption } from '@common/components/form/FormSelect'; import { ChartCapabilities } from '@common/modules/charts/types/chart'; import { Dictionary } from '@common/types'; @@ -96,7 +96,7 @@ export default function ChartLegendItems({ >
-
- - - - {axisType === 'major' ? ( <> - + name={`referenceLines.${index}.position`} id="referenceLines-position" label="Position" @@ -78,7 +78,7 @@ export default function ChartReferenceLineConfigurationForm({ legendSize="s" legendWeight="regular" > - + className="govuk-!-margin-bottom-2" name={`referenceLines.${index}.otherAxisStart`} id="referenceLines-otherAxisStart" @@ -90,7 +90,7 @@ export default function ChartReferenceLineConfigurationForm({ options={majorAxisOptions} />
- + name={`referenceLines.${index}.otherAxisEnd`} id="referenceLines-otherAxisEnd" label="End point" @@ -104,7 +104,7 @@ export default function ChartReferenceLineConfigurationForm({ )} ) : ( - + name={`referenceLines.${index}.position`} id="referenceLines-position" label="Position" @@ -116,7 +116,7 @@ export default function ChartReferenceLineConfigurationForm({ {axisType === 'minor' ? ( <> - + name={`referenceLines.${index}.otherAxisPositionType`} id="referenceLines-otherAxisPositionType" label={`${axis === 'x' ? 'Y' : 'X'} axis position`} @@ -145,7 +145,7 @@ export default function ChartReferenceLineConfigurationForm({ {referenceLine?.otherAxisPositionType === otherAxisPositionTypes.custom ? (
- + name={`referenceLines.${index}.otherAxisPosition`} id="referenceLines-otherAxisPosition" label={`Percent along ${axis === 'x' ? 'Y' : 'X'} axis`} @@ -161,7 +161,7 @@ export default function ChartReferenceLineConfigurationForm({ legendSize="s" legendWeight="regular" > - + className="govuk-!-margin-bottom-2" name={`referenceLines.${index}.otherAxisStart`} id="referenceLines-otherAxisStart" @@ -173,7 +173,7 @@ export default function ChartReferenceLineConfigurationForm({ options={majorAxisOptions} />
- + name={`referenceLines.${index}.otherAxisEnd`} id="referenceLines-otherAxisEnd" label="End point" @@ -189,7 +189,7 @@ export default function ChartReferenceLineConfigurationForm({ )} ) : ( - + name={`referenceLines.${index}.otherAxisPosition`} id="referenceLines-otherAxisPosition" label={axis === 'x' ? 'Y axis position' : 'X axis position'} @@ -206,7 +206,7 @@ export default function ChartReferenceLineConfigurationForm({ )} - + name={`referenceLines.${index}.label`} id="referenceLines-label" label="Label" @@ -215,7 +215,7 @@ export default function ChartReferenceLineConfigurationForm({ /> - + name={`referenceLines.${index}.labelWidth`} id="referenceLines-labelWidth" hint="Pixels (optional)" @@ -226,7 +226,7 @@ export default function ChartReferenceLineConfigurationForm({ /> - + className={styles.styleSelect} name={`referenceLines.${index}.style`} id="referenceLines-style" diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartCustomDataGroupingsConfiguration.test.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartCustomDataGroupingsConfiguration.test.tsx index ec22f01a695..87f9f889330 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartCustomDataGroupingsConfiguration.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartCustomDataGroupingsConfiguration.test.tsx @@ -1,5 +1,5 @@ import ChartCustomDataGroupingsConfiguration from '@admin/pages/release/datablocks/components/chart/ChartCustomDataGroupingsConfiguration'; -import FormProvider from '@common/components/form/rhf/FormProvider'; +import FormProvider from '@common/components/form/FormProvider'; import baseRender from '@common-test/render'; import { screen, waitFor, within } from '@testing-library/react'; import noop from 'lodash/noop'; diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartReferenceLinesConfiguration.test.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartReferenceLinesConfiguration.test.tsx index d2974c8d377..a27746908c6 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartReferenceLinesConfiguration.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/components/chart/__tests__/ChartReferenceLinesConfiguration.test.tsx @@ -11,7 +11,7 @@ import { LocationFilter } from '@common/modules/table-tool/types/filters'; import { screen, waitFor, within } from '@testing-library/react'; import noop from 'lodash/noop'; import React, { ReactNode } from 'react'; -import FormProvider from '@common/components/form/rhf/FormProvider'; +import FormProvider from '@common/components/form/FormProvider'; import baseRender from '@common-test/render'; describe('ChartReferenceLinesConfiguration', () => { diff --git a/src/explore-education-statistics-admin/src/pages/release/footnotes/components/FilterGroupDetails.tsx b/src/explore-education-statistics-admin/src/pages/release/footnotes/components/FilterGroupDetails.tsx index 05b955acb5f..c2081a481cc 100644 --- a/src/explore-education-statistics-admin/src/pages/release/footnotes/components/FilterGroupDetails.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/footnotes/components/FilterGroupDetails.tsx @@ -1,7 +1,7 @@ import { FootnoteSubjectMeta } from '@admin/services/footnoteService'; import Details from '@common/components/Details'; -import RHFFormFieldCheckbox from '@common/components/form/rhf/RHFFormFieldCheckbox'; -import RHFFormFieldCheckboxGroup from '@common/components/form/rhf/RHFFormFieldCheckboxGroup'; +import FormFieldCheckbox from '@common/components/form/FormFieldCheckbox'; +import FormFieldCheckboxGroup from '@common/components/form/FormFieldCheckboxGroup'; import classNames from 'classnames'; import React from 'react'; import { useFormContext } from 'react-hook-form'; @@ -32,7 +32,7 @@ export default function FilterGroupDetails({ testId={`filter-${groupId}`} > {selectAll && groupId && ( - {!hideGrouping && ( - - - +

Select which subjects, filters and indicators your footnote applies to and these will appear alongside the associated data in your published @@ -142,7 +142,7 @@ export default function FootnoteForm({ critical to understanding the data in the table or chart it refers to.

- + name="content" label="Footnote" includePlugins={pluginsConfigLinksOnly} @@ -156,7 +156,7 @@ export default function FootnoteForm({ const { subjectId, subjectName } = subject; return ( - Save footnote {cancelButton} - + ); } diff --git a/src/explore-education-statistics-admin/src/pages/release/footnotes/components/IndicatorDetails.tsx b/src/explore-education-statistics-admin/src/pages/release/footnotes/components/IndicatorDetails.tsx index 9e088b8ef74..a8ffdca55c4 100644 --- a/src/explore-education-statistics-admin/src/pages/release/footnotes/components/IndicatorDetails.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/footnotes/components/IndicatorDetails.tsx @@ -1,6 +1,6 @@ import { FootnoteSubjectMeta } from '@admin/services/footnoteService'; import Details from '@common/components/Details'; -import RHFFormFieldCheckboxGroup from '@common/components/form/rhf/RHFFormFieldCheckboxGroup'; +import FormFieldCheckboxGroup from '@common/components/form/FormFieldCheckboxGroup'; import React from 'react'; interface Props { @@ -29,7 +29,7 @@ export default function IndicatorDetails({ : '' } > - {({ formState, getValues, reset }) => { return ( - - +
+ label="Invite new users by email" name="emails" className="govuk-!-width-one-third" @@ -191,7 +191,7 @@ export default function PreReleaseUserAccessForm({ onExit={handleModalCancel} /> )} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/release/pre-release/components/PublicPreReleaseAccessForm.tsx b/src/explore-education-statistics-admin/src/pages/release/pre-release/components/PublicPreReleaseAccessForm.tsx index 9d70f83c636..6c93d1bf5a2 100644 --- a/src/explore-education-statistics-admin/src/pages/release/pre-release/components/PublicPreReleaseAccessForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/pre-release/components/PublicPreReleaseAccessForm.tsx @@ -1,10 +1,10 @@ -import RHFFormFieldEditor from '@admin/components/form/RHFFormFieldEditor'; +import FormFieldEditor from '@admin/components/form/FormFieldEditor'; import PreviewHtml from '@admin/components/PreviewHtml'; import styles from '@admin/pages/release/pre-release/components/PublicPreReleaseAccessForm.module.scss'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import InsetText from '@common/components/InsetText'; import WarningMessage from '@common/components/WarningMessage'; import useToggle from '@common/hooks/useToggle'; @@ -69,8 +69,8 @@ export default function PublicPreReleaseAccessForm({ preReleaseAccessList: preReleaseAccessList || defaultAccessListText, }} > - - +
+ name="preReleaseAccessList" label="Public access list" focusOnInit @@ -83,7 +83,7 @@ export default function PublicPreReleaseAccessForm({ Cancel - + ) : ( <> diff --git a/src/explore-education-statistics-admin/src/pages/themes/components/ThemeForm.tsx b/src/explore-education-statistics-admin/src/pages/themes/components/ThemeForm.tsx index 78be2940b21..bf1408c710d 100644 --- a/src/explore-education-statistics-admin/src/pages/themes/components/ThemeForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/themes/components/ThemeForm.tsx @@ -1,9 +1,9 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import { mapFieldErrors } from '@common/validation/serverValidations'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import Yup from '@common/validation/yup'; import React, { ReactNode, useMemo } from 'react'; import { ObjectSchema } from 'yup'; @@ -56,14 +56,14 @@ const ThemeForm = ({ > {({ formState }) => { return ( - - +
+ label="Title" name="title" className="govuk-!-width-two-thirds" /> - + label="Summary" name="summary" className="govuk-!-width-two-thirds" @@ -75,7 +75,7 @@ const ThemeForm = ({ {cancelButton} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/themes/topics/components/TopicForm.tsx b/src/explore-education-statistics-admin/src/pages/themes/topics/components/TopicForm.tsx index 79e54cdaa74..a6b503e3612 100644 --- a/src/explore-education-statistics-admin/src/pages/themes/topics/components/TopicForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/themes/topics/components/TopicForm.tsx @@ -1,8 +1,8 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; import React, { ReactNode, useMemo } from 'react'; @@ -53,8 +53,8 @@ const TopicForm = ({ > {({ formState }) => { return ( - - +
+ label="Title" name="title" className="govuk-!-width-two-thirds" @@ -66,7 +66,7 @@ const TopicForm = ({ {cancelButton} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/users/UserInvitePage.tsx b/src/explore-education-statistics-admin/src/pages/users/UserInvitePage.tsx index 63d853408d4..f204b8e2df5 100644 --- a/src/explore-education-statistics-admin/src/pages/users/UserInvitePage.tsx +++ b/src/explore-education-statistics-admin/src/pages/users/UserInvitePage.tsx @@ -2,8 +2,8 @@ import Page from '@admin/components/Page'; import userService, { UserInvite } from '@admin/services/userService'; import Button from '@common/components/Button'; import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import { ErrorControlState } from '@common/contexts/ErrorControlContext'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; @@ -13,8 +13,8 @@ import LoadingSpinner from '@common/components/LoadingSpinner'; import ButtonGroup from '@common/components/ButtonGroup'; import InviteUserReleaseRoleForm from '@admin/pages/users/components/InviteUserReleaseRoleForm'; import InviteUserPublicationRoleForm from '@admin/pages/users/components/InviteUserPublicationRoleForm'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { ObjectSchema } from 'yup'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; @@ -159,15 +159,15 @@ export default function UserInvitePage({ }} validationSchema={validationSchema} > - - +
+ label="User email" name="userEmail" width={20} hint="The invited user must be on the DfE AAD. Contact explore.statistics@education.gov.uk if unsure." /> - + label="Role" name="roleId" hint="The user's role within the service." @@ -192,7 +192,7 @@ export default function UserInvitePage({ Cancel - + diff --git a/src/explore-education-statistics-admin/src/pages/users/components/InviteUserPublicationRoleForm.tsx b/src/explore-education-statistics-admin/src/pages/users/components/InviteUserPublicationRoleForm.tsx index d3678f86037..7dba520f1ad 100644 --- a/src/explore-education-statistics-admin/src/pages/users/components/InviteUserPublicationRoleForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/users/components/InviteUserPublicationRoleForm.tsx @@ -9,7 +9,7 @@ import ButtonText from '@common/components/ButtonText'; import keyBy from 'lodash/keyBy'; import orderBy from 'lodash/orderBy'; import { PublicationSummary } from '@common/services/publicationService'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { useFormContext } from 'react-hook-form'; interface Props { @@ -93,7 +93,7 @@ export default function InviteUserPublicationRoleForm({ legendSize="m" hint="The user's publication roles within the service." > - + label="Publication" name="publicationId" placeholder="Choose publication" @@ -102,7 +102,7 @@ export default function InviteUserPublicationRoleForm({ value: publication.id, }))} /> - + label="Publication role" name="publicationRole" placeholder="Choose publication role" diff --git a/src/explore-education-statistics-admin/src/pages/users/components/InviteUserReleaseRoleForm.tsx b/src/explore-education-statistics-admin/src/pages/users/components/InviteUserReleaseRoleForm.tsx index ce086fff8fc..8090843e19d 100644 --- a/src/explore-education-statistics-admin/src/pages/users/components/InviteUserReleaseRoleForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/users/components/InviteUserReleaseRoleForm.tsx @@ -1,6 +1,6 @@ import Button from '@common/components/Button'; import { FormFieldset } from '@common/components/form'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { IdTitlePair } from '@admin/services/types/common'; import { InviteUserReleaseRole, @@ -91,7 +91,7 @@ export default function InviteUserReleaseRoleForm({ legendSize="m" hint="The user's release roles within the service." > - + label="Release" name="releaseId" placeholder="Choose release" @@ -100,7 +100,7 @@ export default function InviteUserReleaseRoleForm({ value: release.id, }))} /> - + label="Release role" name="releaseRole" placeholder="Choose release role" diff --git a/src/explore-education-statistics-admin/src/pages/users/components/PublicationAccessForm.tsx b/src/explore-education-statistics-admin/src/pages/users/components/PublicationAccessForm.tsx index 73041579f59..840d7e85b9f 100644 --- a/src/explore-education-statistics-admin/src/pages/users/components/PublicationAccessForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/users/components/PublicationAccessForm.tsx @@ -6,10 +6,10 @@ import { IdTitlePair } from '@admin/services/types/common'; import Button from '@common/components/Button'; import ButtonText from '@common/components/ButtonText'; import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import { mapFieldErrors } from '@common/validation/serverValidations'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import orderBy from 'lodash/orderBy'; import React from 'react'; @@ -65,7 +65,7 @@ export default function PublicationAccessForm({ > {({ formState }) => { return ( - @@ -77,7 +77,7 @@ export default function PublicationAccessForm({ >
- + label="Publication" name="publicationId" options={publications?.map(publication => ({ @@ -88,7 +88,7 @@ export default function PublicationAccessForm({
- + label="Publication role" name="publicationRole" options={publicationRoles?.map(role => ({ @@ -151,7 +151,7 @@ export default function PublicationAccessForm({ ))} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/users/components/ReleaseAccessForm.tsx b/src/explore-education-statistics-admin/src/pages/users/components/ReleaseAccessForm.tsx index 4b96d94bbc6..4479a0d1743 100644 --- a/src/explore-education-statistics-admin/src/pages/users/components/ReleaseAccessForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/users/components/ReleaseAccessForm.tsx @@ -6,9 +6,9 @@ import { IdTitlePair } from '@admin/services/types/common'; import Button from '@common/components/Button'; import ButtonText from '@common/components/ButtonText'; import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { mapFieldErrors } from '@common/validation/serverValidations'; import orderBy from 'lodash/orderBy'; import React from 'react'; @@ -63,10 +63,7 @@ export default function ReleaseAccessForm({ > {({ formState }) => { return ( - +
- + label="Release" name="releaseId" options={releases?.map(release => ({ @@ -86,7 +83,7 @@ export default function ReleaseAccessForm({
- + label="Release role" name="releaseRole" options={releaseRoles?.map(role => ({ @@ -151,7 +148,7 @@ export default function ReleaseAccessForm({ ))} - + ); }} diff --git a/src/explore-education-statistics-admin/src/pages/users/components/RoleForm.tsx b/src/explore-education-statistics-admin/src/pages/users/components/RoleForm.tsx index 579d138797f..0e56a841cc8 100644 --- a/src/explore-education-statistics-admin/src/pages/users/components/RoleForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/users/components/RoleForm.tsx @@ -1,9 +1,9 @@ import userService, { Role, User } from '@admin/services/userService'; import Button from '@common/components/Button'; import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { mapFieldErrors } from '@common/validation/serverValidations'; import Yup from '@common/validation/yup'; import React, { useMemo } from 'react'; @@ -49,7 +49,7 @@ const RoleForm = ({ roles, user, onUpdate }: Props) => { }} validationSchema={validationSchema} > - +
{ >
- + label="Role" name="roleId" options={roles?.map(role => ({ @@ -75,7 +75,7 @@ const RoleForm = ({ roles, user, onUpdate }: Props) => {
- +
); }; diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAddPublicationSubject.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAddPublicationSubject.tsx index d700fa6380e..7b2d8db6e20 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAddPublicationSubject.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAddPublicationSubject.tsx @@ -1,9 +1,9 @@ import Button from '@common/components/Button'; import InsetText from '@common/components/InsetText'; import Yup from '@common/validation/yup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import React from 'react'; import WarningMessage from '@common/components/WarningMessage'; import { @@ -76,7 +76,7 @@ const PrototypeAddPublicationSubject = ({ })} > {({ reset }) => ( - { reset(); @@ -84,14 +84,14 @@ const PrototypeAddPublicationSubject = ({ }} > {/* - */} - + id="subjectId" name="subjectId" label="Available data sets" @@ -102,7 +102,7 @@ const PrototypeAddPublicationSubject = ({ placeholder="Select a data set" /> - + )} )} diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeChangeStatusForm.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeChangeStatusForm.tsx index c9b4c75f607..6889c487531 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeChangeStatusForm.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeChangeStatusForm.tsx @@ -1,9 +1,9 @@ import Button from '@common/components/Button'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldDateInput from '@common/components/form/rhf/RHFFormFieldDateInput'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldDateInput from '@common/components/form/FormFieldDateInput'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import { PartialDate } from '@common/utils/date/partialDate'; import React from 'react'; @@ -29,9 +29,9 @@ const PrototypeChangeStatusForm = ({ selectedStatus, onSubmit }: Props) => { }} > {() => ( - +
<> - + legend="Changes on current live version (version 1.0)" name="status" order={[]} @@ -45,14 +45,14 @@ const PrototypeChangeStatusForm = ({ selectedStatus, onSubmit }: Props) => { value: 'deprecated', conditional: ( <> - + hint="These notes will be appended to the published API dataset. They are used to explain to the public users why this data set is being deprecated." label="Public guidance notes" name="notes" rows={3} /> - { /> - + )} ); diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubject.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubject.tsx index 34947b4ac3d..40cfa291b80 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubject.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubject.tsx @@ -1,7 +1,7 @@ import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import Button from '@common/components/Button'; import Yup from '@common/validation/yup'; import React from 'react'; @@ -49,7 +49,7 @@ const PrototypeEditPublicationSubject = ({ })} > {() => ( - { onSubmit({ @@ -58,7 +58,7 @@ const PrototypeEditPublicationSubject = ({ }); }} > - + id="subjectId" name="subjectId" label="Available data sets" @@ -69,7 +69,7 @@ const PrototypeEditPublicationSubject = ({ placeholder="Choose a subject" /> - + )} diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubjectTitle.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubjectTitle.tsx index 67cca3654b2..8a584978cfa 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubjectTitle.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeEditPublicationSubjectTitle.tsx @@ -1,7 +1,7 @@ import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFFormFieldTextInput from '@common/components/form/rhf/RHFFormFieldTextInput'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import FormFieldTextInput from '@common/components/form/FormFieldTextInput'; +import Form from '@common/components/form/Form'; import Button from '@common/components/Button'; import Yup from '@common/validation/yup'; import React from 'react'; @@ -41,7 +41,7 @@ const PrototypeEditPublicationSubjectTitle = ({ title: Yup.string().required('Enter a title'), })} > - { onSubmit({ @@ -50,14 +50,14 @@ const PrototypeEditPublicationSubjectTitle = ({ }); }} > - + className="govuk-!-width-two-thirds" label="Title" name="title" /> - + diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeMapFacetModal.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeMapFacetModal.tsx index 596ef6efad3..fd0241b7c47 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeMapFacetModal.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeMapFacetModal.tsx @@ -1,10 +1,10 @@ import SummaryList from '@common/components/SummaryList'; import SummaryListItem from '@common/components/SummaryListItem'; import Button from '@common/components/Button'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldCheckbox from '@common/components/form/rhf/RHFFormFieldCheckbox'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldCheckbox from '@common/components/form/FormFieldCheckbox'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; import { FormTextSearchInput } from '@common/components/form'; import Yup from '@common/validation/yup'; import Modal from '@common/components/Modal'; @@ -61,7 +61,7 @@ const PrototypeMapFacetModal = ({ selectedItem: Yup.string().required(`Choose a ${name}`), })} > - { onSubmit(selectedItem); @@ -121,7 +121,7 @@ const PrototypeMapFacetModal = ({ /> {Object.entries(groupedNewItems).map(([key, items]) => { return ( -
- */}
-
+ ); diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeNotificationCreate.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeNotificationCreate.tsx index 1a179b490a8..d8f9358928c 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeNotificationCreate.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeNotificationCreate.tsx @@ -8,10 +8,10 @@ import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import Yup from '@common/validation/yup'; import React, { useState } from 'react'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldCheckboxGroup from '@common/components/form/rhf/RHFFormFieldCheckboxGroup'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldCheckboxGroup from '@common/components/form/FormFieldCheckboxGroup'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; interface FormValues { summary: string; @@ -80,8 +80,8 @@ const PrototypeNotificationCreate = ({ {({ watch }) => { const values = watch(); return ( - {}}> - {}}> + Summary of updates @@ -91,7 +91,7 @@ const PrototypeNotificationCreate = ({ hint=" Describe any expected breaking changes of updates in the next data set version." /> - - + ); }} diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep1.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep1.tsx index 883804f93f4..67834f0b52f 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep1.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep1.tsx @@ -5,9 +5,9 @@ import { InjectedWizardProps } from '@common/modules/table-tool/components/Wizar import WizardStepFormActions from '@common/modules/table-tool/components/WizardStepFormActions'; import WizardStepHeading from '@common/modules/table-tool/components/WizardStepHeading'; import WizardStepSummary from '@common/modules/table-tool/components/WizardStepSummary'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import { FormFieldset } from '@common/components/form'; import Yup from '@common/validation/yup'; import React from 'react'; @@ -58,7 +58,7 @@ const PrototypePrepareNextSubjectStep1 = ({ })} > {() => ( - { onSubmit(values.subjectId); @@ -84,7 +84,7 @@ const PrototypePrepareNextSubjectStep1 = ({ removed in the new data set - + id="subjectId" name="subjectId" label="Available data sets" @@ -100,7 +100,7 @@ const PrototypePrepareNextSubjectStep1 = ({ submitText="Next step - locations" {...stepProps} /> - + )}
diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep2.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep2.tsx index a3fabed0a49..d750f222684 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep2.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep2.tsx @@ -6,8 +6,8 @@ import WizardStepHeading from '@common/modules/table-tool/components/WizardStepH import WizardStepSummary from '@common/modules/table-tool/components/WizardStepSummary'; import WizardStepFormActions from '@common/modules/table-tool/components/WizardStepFormActions'; import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import React, { useState } from 'react'; import Yup from '@common/validation/yup'; import capitalize from 'lodash/capitalize'; @@ -89,7 +89,7 @@ const PrototypePrepareNextSubjectStep2 = ({ name, ...stepProps }: Props) => { })} > {({ formState }) => ( - { goToNextStep(); @@ -267,7 +267,7 @@ const PrototypePrepareNextSubjectStep2 = ({ name, ...stepProps }: Props) => { {...stepProps} submitText={`Next - ${nextStep}`} /> - + )}
diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep5.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep5.tsx index 3c0298ba05e..ffb5f634f70 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep5.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePrepareNextSubjectStep5.tsx @@ -6,10 +6,10 @@ import WizardStepFormActions from '@common/modules/table-tool/components/WizardS import WizardStepHeading from '@common/modules/table-tool/components/WizardStepHeading'; import WizardStepSummary from '@common/modules/table-tool/components/WizardStepSummary'; import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import RHFFormFieldTextArea from '@common/components/form/rhf/RHFFormFieldTextArea'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormFieldTextArea from '@common/components/form/FormFieldTextArea'; import React, { useEffect, useState } from 'react'; import ChangelogExample from './PrototypeChangelogExamples'; import { @@ -73,7 +73,7 @@ const PrototypePrepareNextSubjectStep5 = ({ }} > {() => ( - { setVersionNotes(values.versionNotes); @@ -83,7 +83,7 @@ const PrototypePrepareNextSubjectStep5 = ({ > <> - + hint="Use the public guidance notes to highlight any extra information to your end users that may not be apparent in the automated changelog below" label="Public guidance notes" @@ -92,7 +92,7 @@ const PrototypePrepareNextSubjectStep5 = ({ />
- + legend="Changes on current live version (version 1.0)" name="versionType" onChange={event => @@ -138,7 +138,7 @@ const PrototypePrepareNextSubjectStep5 = ({ submitText="Next step - complete this API data set version" {...stepProps} /> - + )}
diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewStagedDataset.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewStagedDataset.tsx index 66650519b6d..680093ad16a 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewStagedDataset.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewStagedDataset.tsx @@ -1,7 +1,7 @@ import ButtonText from '@common/components/ButtonText'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import Button from '@common/components/Button'; import Yup from '@common/validation/yup'; import React from 'react'; @@ -49,7 +49,7 @@ const PrototypePreviewSubject = ({ })} > {() => ( - { onSubmit({ @@ -58,7 +58,7 @@ const PrototypePreviewSubject = ({ }); }} > - + id="subjectId" name="subjectId" label="Available data sets" @@ -69,7 +69,7 @@ const PrototypePreviewSubject = ({ placeholder="Choose a subject" /> - + )} diff --git a/src/explore-education-statistics-admin/src/prototypes/components/PrototypePublicationForm.tsx b/src/explore-education-statistics-admin/src/prototypes/components/PrototypePublicationForm.tsx index 17250d973b8..b9332a1bbda 100644 --- a/src/explore-education-statistics-admin/src/prototypes/components/PrototypePublicationForm.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/components/PrototypePublicationForm.tsx @@ -19,9 +19,9 @@ import styles from '@admin/prototypes/components/PrototypePublicationForm.module import PrototypeFormTextSearchInput from '@admin/prototypes/components/PrototypeFormTextSearchInput'; import orderBy from 'lodash/orderBy'; import React, { useMemo, useState } from 'react'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; export interface PublicationFormValues { publicationId: string; @@ -103,7 +103,7 @@ const PrototypePublicationForm = ({ const values = watch(); if (isActive) { return ( - +

Search or select a theme to find publications

@@ -158,7 +158,7 @@ const PrototypePublicationForm = ({ />
- + id={`${formId}-publications`} legend={ <> @@ -196,7 +196,7 @@ const PrototypePublicationForm = ({
-
+ ); } diff --git a/src/explore-education-statistics-admin/src/prototypes/components/PrototypeTableHeadersForm.tsx b/src/explore-education-statistics-admin/src/prototypes/components/PrototypeTableHeadersForm.tsx index 064ad5135a8..92afe600fe8 100644 --- a/src/explore-education-statistics-admin/src/prototypes/components/PrototypeTableHeadersForm.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/components/PrototypeTableHeadersForm.tsx @@ -1,7 +1,7 @@ import Button from '@common/components/Button'; import { FormGroup } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import ScreenReaderMessage from '@common/components/ScreenReaderMessage'; import useToggle from '@common/hooks/useToggle'; import useMounted from '@common/hooks/useMounted'; @@ -269,7 +269,7 @@ export default function TableHeadersForm({ onSubmit, initialValues }: Props) { > {form => { return ( - Update and view reordered table - + ); }} diff --git a/src/explore-education-statistics-common/package.json b/src/explore-education-statistics-common/package.json index a057b8b2475..3fefd741139 100644 --- a/src/explore-education-statistics-common/package.json +++ b/src/explore-education-statistics-common/package.json @@ -18,7 +18,6 @@ "date-fns": "^2.16.1", "date-fns-tz": "^2.0.1", "domhandler": "^4.2.2", - "formik": "^2.4.2", "geojson": "^0.5.0", "govuk-frontend": "^5.2.0", "html-react-parser": "^1.3.0", diff --git a/src/explore-education-statistics-common/src/components/form/Form.tsx b/src/explore-education-statistics-common/src/components/form/Form.tsx index bd58fcddebc..6887be9cb73 100644 --- a/src/explore-education-statistics-common/src/components/form/Form.tsx +++ b/src/explore-education-statistics-common/src/components/form/Form.tsx @@ -2,10 +2,9 @@ import ErrorSummary, { ErrorSummaryMessage, } from '@common/components/ErrorSummary'; import { FormIdContextProvider } from '@common/components/form/contexts/FormIdContext'; +import createErrorHelper from '@common/components/form/validation/createErrorHelper'; import useMountedRef from '@common/hooks/useMountedRef'; import useToggle from '@common/hooks/useToggle'; -import createErrorHelper from '@common/validation/createErrorHelper'; -import { useFormikContext } from 'formik'; import camelCase from 'lodash/camelCase'; import React, { FormEvent, @@ -15,25 +14,28 @@ import React, { useMemo, useRef, } from 'react'; +import { FieldValues, Path, useFormContext, useWatch } from 'react-hook-form'; -interface Props { +interface Props { children: ReactNode; id: string; + initialTouched?: Path[]; submitId?: string; showErrorSummary?: boolean; visuallyHiddenErrorSummary?: boolean; + onSubmit: (values: TFormValues) => Promise | void; } /** - * Form wrapper to integrate with Formik. + * Form wrapper to integrate with React Hook Form. * * This provides a bunch of conveniences for displaying errors * and linking to them correctly (in error message hrefs) as * long as certain conventions are followed. * * Fields with errors should have ids which share the form's - * id and camelCased value key in Formik - * e.g. if form id is `timePeriodForm`, then a Formik value with a + * id and camelCased value key in the form + * e.g. if form id is `timePeriodForm`, then a form value with a * key of `startDate` should have a field with an id of * `timePeriodForm-startDate`. * @@ -41,27 +43,32 @@ interface Props { * requests will also be added to the error summary. These * will link to the `submitId` prop. */ -const Form = ({ +export default function Form({ children, id, - showErrorSummary = true, + initialTouched, submitId = `${id}-submit`, + showErrorSummary = true, visuallyHiddenErrorSummary = false, -}: Props) => { + onSubmit, +}: Props) { const isMounted = useMountedRef(); - const formik = useFormikContext(); - const { errors, touched, values, submitCount, submitForm } = formik; + const { + formState: { errors, submitCount, touchedFields, isSubmitted }, + handleSubmit: submit, + } = useFormContext(); + const values = useWatch(); const previousValues = useRef(values); const previousSubmitCount = useRef(submitCount); const { getAllErrors } = createErrorHelper({ errors, - touched, - submitCount, + initialTouched, + isSubmitted, + touchedFields, }); - const [hasSummaryFocus, toggleSummaryFocus] = useToggle(false); const allErrors = useMemo(() => { @@ -82,7 +89,7 @@ const Form = ({ } previousValues.current = values; - }, [submitCount, values, isMounted]); + }, [isSubmitted, submitCount, values, isMounted]); useEffect(() => { if (!isMounted.current) { @@ -100,9 +107,9 @@ const Form = ({ event.preventDefault(); toggleSummaryFocus.off(); - await submitForm(); + await submit(async data => onSubmit(data))(event); }, - [toggleSummaryFocus, submitForm], + [submit, toggleSummaryFocus, onSubmit], ); return ( @@ -121,6 +128,4 @@ const Form = ({ ); -}; - -export default Form; +} diff --git a/src/explore-education-statistics-common/src/components/form/FormCheckbox.tsx b/src/explore-education-statistics-common/src/components/form/FormCheckbox.tsx index 7e178198993..57b052afdcc 100644 --- a/src/explore-education-statistics-common/src/components/form/FormCheckbox.tsx +++ b/src/explore-education-statistics-common/src/components/form/FormCheckbox.tsx @@ -28,9 +28,9 @@ export interface FormCheckboxProps { name: string; onBlur?: FocusEventHandler; onChange?: CheckboxChangeEventHandler; - value: string; disabled?: boolean; inputRef?: Ref; + value?: string; } const FormCheckbox = ({ @@ -46,9 +46,9 @@ const FormCheckbox = ({ name, onBlur, onChange, - value, disabled = false, inputRef, + value, }: FormCheckboxProps) => { const { onMounted } = useMounted(undefined, false); @@ -76,9 +76,9 @@ const FormCheckbox = ({ } }} type="checkbox" - value={value} disabled={disabled} ref={inputRef} + value={value} />
+ ); }} diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/DownloadTable.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/DownloadTable.tsx index a6dbe16605d..5ae3a9505f9 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/DownloadTable.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/DownloadTable.tsx @@ -1,6 +1,6 @@ -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; import Button from '@common/components/Button'; import ButtonGroup from '@common/components/ButtonGroup'; import { FullTable } from '@common/modules/table-tool/types/fullTable'; @@ -70,7 +70,7 @@ const DownloadTable = ({ > {({ formState }) => { return ( - { await onSubmit?.(fileFormat); @@ -90,7 +90,7 @@ const DownloadTable = ({ }, 'Download Table', )} - + legend="Select file format:" legendSize="s" legendWeight="regular" @@ -125,7 +125,7 @@ const DownloadTable = ({ /> - + ); }} diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/FiltersForm.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/FiltersForm.tsx index 5ca9c5babd9..60adfe0a986 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/FiltersForm.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/FiltersForm.tsx @@ -2,11 +2,11 @@ import ButtonText from '@common/components/ButtonText'; import VisuallyHidden from '@common/components/VisuallyHidden'; import CollapsibleList from '@common/components/CollapsibleList'; import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormCheckboxSelectedCount from '@common/components/form/rhf/RHFFormCheckboxSelectedCount'; -import RHFFormFieldCheckboxSearchSubGroups from '@common/components/form/rhf/RHFFormFieldCheckboxSearchSubGroups'; -import RHFFormFieldCheckboxGroupsMenu from '@common/components/form/rhf/RHFFormFieldCheckboxGroupsMenu'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormCheckboxSelectedCount from '@common/components/form/FormCheckboxSelectedCount'; +import FormFieldCheckboxSearchSubGroups from '@common/components/form/FormFieldCheckboxSearchSubGroups'; +import FormFieldCheckboxGroupsMenu from '@common/components/form/FormFieldCheckboxGroupsMenu'; import SummaryList from '@common/components/SummaryList'; import SummaryListItem from '@common/components/SummaryListItem'; import ResetFormOnPreviousStep from '@common/modules/table-tool/components/ResetFormOnPreviousStep'; @@ -19,7 +19,7 @@ import styles from '@common/modules/table-tool/components/FiltersForm.module.scs import { SelectedPublication } from '@common/modules/table-tool/types/selectedPublication'; import { Subject, SubjectMeta } from '@common/services/tableBuilderService'; import { Dictionary } from '@common/types'; -import createRHFErrorHelper from '@common/components/form/rhf/validation/createRHFErrorHelper'; +import createErrorHelper from '@common/components/form/validation/createErrorHelper'; import { getErrorCode, hasErrorMessage, @@ -195,13 +195,13 @@ export default function FiltersForm({ validationSchema={validationSchema} > {({ formState, getValues, reset, setValue }) => { - const { getError } = createRHFErrorHelper({ + const { getError } = createErrorHelper({ errors: formState.errors, touchedFields: formState.touchedFields, }); if (isActive) { return ( - +
{tableQueryError && formState.submitCount > 0 && (
- Indicators - + } legendSize="m" @@ -282,7 +282,7 @@ export default function FiltersForm({ 'order', ); return ( - - + ); } diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/FormCheckboxSelectedCount.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/FormCheckboxSelectedCount.tsx deleted file mode 100644 index 2598a22c093..00000000000 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/FormCheckboxSelectedCount.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Tag from '@common/components/Tag'; -import { useFormikContext } from 'formik'; -import get from 'lodash/get'; -import React from 'react'; - -interface Props { - name: string; -} - -const FormCheckboxSelectedCount = ({ name }: Props) => { - const formik = useFormikContext(); - - const value = get(formik.values, name); - let count = 0; - - if (Array.isArray(value)) { - count = value.length; - } - - return count > 0 ? ( - - - - {count} selected - - ) : null; -}; - -export default FormCheckboxSelectedCount; diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxGroupsMenu.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxGroupsMenu.tsx deleted file mode 100644 index 585555651a3..00000000000 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxGroupsMenu.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import DetailsMenu from '@common/components/DetailsMenu'; -import FormFieldCheckboxSearchSubGroups, { - FormFieldCheckboxSearchSubGroupsProps, -} from '@common/components/form/FormFieldCheckboxSearchSubGroups'; -import { useField } from 'formik'; -import React from 'react'; -import FormCheckboxSelectionCount from './FormCheckboxSelectedCount'; - -interface Props - extends FormFieldCheckboxSearchSubGroupsProps { - legend: string; - open?: boolean; -} - -function FormFieldCheckboxGroupsMenu(props: Props) { - const { name, legend, open = false } = props; - const [, meta] = useField(name); - - return ( - } - > - {...props} legendHidden /> - - ); -} - -export default FormFieldCheckboxGroupsMenu; diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxMenu.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxMenu.tsx deleted file mode 100644 index 8816c2474f7..00000000000 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/FormFieldCheckboxMenu.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import DetailsMenu from '@common/components/DetailsMenu'; -import { - FormFieldCheckboxGroup, - FormFieldCheckboxSearchGroup, -} from '@common/components/form'; -import { FormFieldCheckboxSearchGroupProps } from '@common/components/form/FormFieldCheckboxSearchGroup'; -import { useField } from 'formik'; -import React, { useEffect, useState } from 'react'; -import FormCheckboxSelectionCount from './FormCheckboxSelectedCount'; - -interface Props - extends FormFieldCheckboxSearchGroupProps { - legend: string; - open?: boolean; -} - -function FormFieldCheckboxMenu(props: Props) { - const { name, open: defaultOpen = false, options, legend } = props; - const [open, setOpen] = useState(defaultOpen); - - const [, meta] = useField(name); - - useEffect(() => { - if (meta.error && meta.touched) { - setOpen(true); - } - }, [meta.error, meta.touched]); - - return ( - } - > - {options.length > 1 ? ( - - selectAll - legendHidden - {...props} - name={name} - options={options} - /> - ) : ( - - selectAll - {...props} - name={name} - options={options} - /> - )} - - ); -} - -export default FormFieldCheckboxMenu; diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/LocationFiltersForm.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/LocationFiltersForm.tsx index e8a11924204..55adee7c333 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/LocationFiltersForm.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/LocationFiltersForm.tsx @@ -1,9 +1,9 @@ import { FormFieldset } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldCheckboxGroupsMenu from '@common/components/form/rhf/RHFFormFieldCheckboxGroupsMenu'; -import RHFFormFieldCheckboxMenu from '@common/components/form/rhf/RHFFormFieldCheckboxMenu'; -import getErrorMessage from '@common/components/form/rhf/util/getErrorMessage'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldCheckboxGroupsMenu from '@common/components/form/FormFieldCheckboxGroupsMenu'; +import FormFieldCheckboxMenu from '@common/components/form/FormFieldCheckboxMenu'; +import getErrorMessage from '@common/components/form/util/getErrorMessage'; import { LocationOption, SubjectMeta, @@ -156,7 +156,7 @@ const LocationFiltersForm = ({ } return ( - ) : ( - - + ); }} diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/PublicationForm.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/PublicationForm.tsx index c2c5f9fe29e..0afe47f1770 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/PublicationForm.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/PublicationForm.tsx @@ -3,9 +3,9 @@ import { FormGroup, FormTextSearchInput, } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; import InsetText from '@common/components/InsetText'; import SummaryList from '@common/components/SummaryList'; import SummaryListItem from '@common/components/SummaryListItem'; @@ -142,7 +142,7 @@ const PublicationForm = ({ {({ formState, getValues, resetField }) => { if (isActive) { return ( - +

Search or select a theme to find publications

@@ -179,7 +179,7 @@ const PublicationForm = ({

or

- + legend="Select a theme" legendSize="s" name="themeId" @@ -196,7 +196,7 @@ const PublicationForm = ({ />
- + id="publications" legend={ <> @@ -247,7 +247,7 @@ const PublicationForm = ({
- +
); } diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersAxis.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersAxis.tsx index baa84a2dcbf..cf84f2ce44c 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersAxis.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersAxis.tsx @@ -3,7 +3,7 @@ import styles from '@common/modules/table-tool/components/TableHeadersAxis.modul import TableHeadersGroup from '@common/modules/table-tool/components/TableHeadersGroup'; import { TableHeadersFormValues } from '@common/modules/table-tool/components/TableHeadersForm'; import useTableHeadersContext from '@common/modules/table-tool/contexts/TableHeadersContext'; -import createRHFErrorHelper from '@common/components/form/rhf/validation/createRHFErrorHelper'; +import createErrorHelper from '@common/components/form/validation/createErrorHelper'; import getTableHeaderGroupId from '@common/modules/table-tool/components/utils/getTableHeaderGroupId'; import { CategoryFilter, @@ -40,7 +40,7 @@ export default function TableHeadersAxis({ const values = getValues(name); - const { getError } = createRHFErrorHelper({ + const { getError } = createErrorHelper({ errors: formState.errors, touchedFields: formState.touchedFields, }); diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersForm.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersForm.tsx index 064ad5135a8..92afe600fe8 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersForm.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/TableHeadersForm.tsx @@ -1,7 +1,7 @@ import Button from '@common/components/Button'; import { FormGroup } from '@common/components/form'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import ScreenReaderMessage from '@common/components/ScreenReaderMessage'; import useToggle from '@common/hooks/useToggle'; import useMounted from '@common/hooks/useMounted'; @@ -269,7 +269,7 @@ export default function TableHeadersForm({ onSubmit, initialValues }: Props) { > {form => { return ( - Update and view reordered table - + ); }} diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/TimePeriodForm.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/TimePeriodForm.tsx index 9fececc885b..48bd1cdec74 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/TimePeriodForm.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/TimePeriodForm.tsx @@ -1,8 +1,8 @@ import { FormFieldset } from '@common/components/form'; import { SelectOption } from '@common/components/form/FormSelect'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; -import RHFFormFieldSelect from '@common/components/form/rhf/RHFFormFieldSelect'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; +import FormFieldSelect from '@common/components/form/FormFieldSelect'; import SummaryList from '@common/components/SummaryList'; import SummaryListItem from '@common/components/SummaryListItem'; import ResetFormOnPreviousStep from '@common/modules/table-tool/components/ResetFormOnPreviousStep'; @@ -171,25 +171,25 @@ const TimePeriodForm = ({ {({ formState, getValues, reset }) => { if (isActive) { return ( - +
- + name="start" label="Start date" disabled={formState.isSubmitting} options={timePeriodOptions} - order={RHFFormFieldSelect.unordered} + order={FormFieldSelect.unordered} /> - + name="end" label="End date" disabled={formState.isSubmitting} options={timePeriodOptions} - order={RHFFormFieldSelect.unordered} + order={FormFieldSelect.unordered} /> @@ -197,7 +197,7 @@ const TimePeriodForm = ({ {...stepProps} isSubmitting={formState.isSubmitting} /> - +
); } diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/FormFieldCheckboxGroupsMenu.test.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/FormFieldCheckboxGroupsMenu.test.tsx deleted file mode 100644 index fe0635b48c7..00000000000 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/FormFieldCheckboxGroupsMenu.test.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { Formik } from 'formik'; -import noop from 'lodash/noop'; -import React from 'react'; -import FormFieldCheckboxGroupsMenu from '../FormFieldCheckboxGroupsMenu'; - -describe('FormFieldCheckboxGroupsMenu', () => { - test('renders multiple checkbox groups in correct order with search input', () => { - const { container } = render( - - - , - ); - - expect(screen.queryByLabelText('Search options')).not.toBeNull(); - - const checkboxes = screen.getAllByLabelText(/Option/); - - expect(checkboxes[0]).toHaveAttribute('value', '1'); - expect(checkboxes[1]).toHaveAttribute('value', '2'); - expect(checkboxes[2]).toHaveAttribute('value', '3'); - expect(checkboxes[3]).toHaveAttribute('value', '4'); - - expect(container.querySelector('#test')).toMatchSnapshot(); - }); - - test('renders single checkbox group with search input', () => { - const { container } = render( - - - , - ); - - expect(screen.queryByLabelText('Search options')).not.toBeNull(); - - const checkboxes = screen.getAllByLabelText(/Option/); - - expect(checkboxes[0]).toHaveAttribute('value', '1'); - expect(checkboxes[1]).toHaveAttribute('value', '2'); - - expect(container.querySelector('#test')).toMatchSnapshot(); - }); - - test('renders single checkbox group with single checkbox option and no search input', () => { - const { container, queryByLabelText } = render( - - - , - ); - - expect(queryByLabelText('Search options')).toBeNull(); - - expect(container.querySelector('#test')).toMatchSnapshot(); - }); - - test('menu contents is expanded if there is a field error', async () => { - render( - - - , - ); - - expect( - screen.getByRole('button', { name: 'Choose options' }), - ).toHaveAttribute('aria-expanded', 'true'); - expect(screen.getByRole('group', { name: 'Choose options' })).toBeVisible(); - }); - - test('clicking menu does not collapse it if there is a field error', async () => { - render( - - - , - ); - - const summary = screen.getByRole('button', { name: 'Choose options' }); - - expect(summary).toHaveAttribute('aria-expanded', 'true'); - expect(screen.getByRole('group', { name: 'Choose options' })).toBeVisible(); - - await userEvent.click(summary); - - expect(summary).toHaveAttribute('aria-expanded', 'true'); - expect(screen.getByRole('group', { name: 'Choose options' })).toBeVisible(); - }); -}); diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersAxis.test.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersAxis.test.tsx index 861e30ebb12..77beb717ebb 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersAxis.test.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersAxis.test.tsx @@ -1,4 +1,4 @@ -import FormProvider from '@common/components/form/rhf/FormProvider'; +import FormProvider from '@common/components/form/FormProvider'; import { TableHeadersFormValues } from '@common/modules/table-tool/components/TableHeadersForm'; import TableHeadersAxis from '@common/modules/table-tool/components/TableHeadersAxis'; import { TableHeadersContextProvider } from '@common/modules/table-tool/contexts/TableHeadersContext'; diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersGroup.test.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersGroup.test.tsx index 105d0b401ee..37befe7af4b 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersGroup.test.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersGroup.test.tsx @@ -1,4 +1,4 @@ -import FormProvider from '@common/components/form/rhf/FormProvider'; +import FormProvider from '@common/components/form/FormProvider'; import { TableHeadersFormValues } from '@common/modules/table-tool/components/TableHeadersForm'; import TableHeadersGroup from '@common/modules/table-tool/components/TableHeadersGroup'; import { TableHeadersContextProvider } from '@common/modules/table-tool/contexts/TableHeadersContext'; diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReadOnlyList.test.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReadOnlyList.test.tsx index 50aeeb6acb7..09676e3cf7a 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReadOnlyList.test.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReadOnlyList.test.tsx @@ -1,4 +1,4 @@ -import FormProvider from '@common/components/form/rhf/FormProvider'; +import FormProvider from '@common/components/form/FormProvider'; import { TableHeadersFormValues } from '@common/modules/table-tool/components/TableHeadersForm'; import TableHeadersReadOnlyList from '@common/modules/table-tool/components/TableHeadersReadOnlyList'; import { TableHeadersContextProvider } from '@common/modules/table-tool/contexts/TableHeadersContext'; diff --git a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReorderableList.test.tsx b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReorderableList.test.tsx index ff8c4bd15f3..008ae7c2fca 100644 --- a/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReorderableList.test.tsx +++ b/src/explore-education-statistics-common/src/modules/table-tool/components/__tests__/TableHeadersReorderableList.test.tsx @@ -1,4 +1,4 @@ -import FormProvider from '@common/components/form/rhf/FormProvider'; +import FormProvider from '@common/components/form/FormProvider'; import { TableHeadersFormValues } from '@common/modules/table-tool/components/TableHeadersForm'; import TableHeadersReorderableList from '@common/modules/table-tool/components/TableHeadersReorderableList'; import { TableHeadersContextProvider } from '@common/modules/table-tool/contexts/TableHeadersContext'; diff --git a/src/explore-education-statistics-common/src/validation/__tests__/createErrorHelper.test.ts b/src/explore-education-statistics-common/src/validation/__tests__/createErrorHelper.test.ts deleted file mode 100644 index 4c390f00287..00000000000 --- a/src/explore-education-statistics-common/src/validation/__tests__/createErrorHelper.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import createErrorHelper from '../createErrorHelper'; - -describe('createErrorHelper', () => { - describe('getError/hasError', () => { - test('gets touched error message', () => { - const { getError, hasError } = createErrorHelper({ - errors: { - test: 'Please select', - }, - touched: { - test: true, - }, - }); - - expect(getError('test')).toBe('Please select'); - expect(hasError('test')).toBe(true); - }); - - test('does not get untouched error message', () => { - const { getError, hasError } = createErrorHelper({ - errors: { - test: 'Please select', - }, - touched: { - test: false, - }, - }); - - expect(getError('test')).toBe(''); - expect(hasError('test')).toBe(false); - }); - - test('gets untouched error message when the form is submitted', () => { - const { getError, hasError } = createErrorHelper({ - errors: { - test: 'Please select', - }, - touched: { - test: false, - }, - submitCount: 1, - }); - - expect(getError('test')).toBe('Please select'); - expect(hasError('test')).toBe(true); - }); - - test('gets nested touched error message', () => { - const { getError } = createErrorHelper<{ test: { something: string } }>({ - errors: { - test: { - something: 'Please select', - }, - }, - touched: { - test: { - something: true, - }, - }, - }); - - expect(getError('test.something')).toBe('Please select'); - }); - - test('does not get nested untouched error message', () => { - const { getError, hasError } = createErrorHelper<{ - test: { something: string }; - }>({ - errors: { - test: { - something: 'Please select', - }, - }, - touched: { - test: { - something: false, - }, - }, - }); - - expect(getError('test.something')).toBe(''); - expect(hasError('test.something')).toBe(false); - }); - - test('does not get error message when path does not match', () => { - const { getError, hasError } = createErrorHelper<{ - test: { something: string }; - }>({ - errors: { - test: { - something: 'Please select', - }, - }, - touched: { - test: { - something: true, - }, - }, - }); - - expect(getError('invalidPath')).toBe(''); - expect(hasError('invalidPath')).toBe(false); - }); - - test('does not get error message when path only partially matches', () => { - const { getError, hasError } = createErrorHelper<{ - test: { something: string }; - }>({ - errors: { - test: { - something: 'Please select', - }, - }, - touched: { - test: { - something: true, - }, - }, - }); - - expect(getError('test')).toBe(''); - expect(hasError('test')).toBe(false); - }); - }); - - describe('getAllErrors', () => { - test('gets touched errors', () => { - const { getAllErrors } = createErrorHelper({ - errors: { - test: 'Please select', - }, - touched: { - test: true, - }, - }); - - expect(getAllErrors()).toEqual({ - test: 'Please select', - }); - }); - - test('does not get untouched errors', () => { - const { getAllErrors } = createErrorHelper({ - errors: { - test: 'Please select', - }, - touched: { - test: false, - }, - }); - - expect(getAllErrors()).toEqual({}); - }); - - test('gets untouched errors when the form is submitted', () => { - const { getAllErrors } = createErrorHelper({ - errors: { - test: 'Please select', - }, - touched: { - test: false, - }, - submitCount: 1, - }); - - expect(getAllErrors()).toEqual({ - test: 'Please select', - }); - }); - - test('gets nested touched errors as single-level object with dot-notation keys', () => { - const { getAllErrors } = createErrorHelper<{ - test: { something: string }; - }>({ - errors: { - test: { - something: 'Please select', - }, - }, - touched: { - test: { - something: true, - }, - }, - }); - - expect(getAllErrors()).toEqual({ - 'test.something': 'Please select', - }); - }); - - test('does not get nested untouched errors', () => { - const { getAllErrors } = createErrorHelper<{ - test: { something: string }; - }>({ - errors: { - test: { - something: 'Please select', - }, - }, - touched: { - test: { - something: false, - }, - }, - }); - - expect(getAllErrors()).toEqual({}); - }); - - test('gets deeply nested touched errors as single-level object with dot-notation keys', () => { - const { getAllErrors } = createErrorHelper<{ - a: { - b: { - c: { - d: string; - }; - }; - }; - }>({ - errors: { - a: { - b: { - c: { - d: 'Please select', - }, - }, - }, - }, - touched: { - a: { - b: { - c: { - d: true, - }, - }, - }, - }, - }); - - expect(getAllErrors()).toEqual({ - 'a.b.c.d': 'Please select', - }); - }); - - test('gets touched error messages for mixed error bag', () => { - const { getAllErrors } = createErrorHelper<{ - address: { - line1: string; - line2: string; - }; - firstName: string; - lastName: string; - }>({ - errors: { - address: { - line1: 'Address 1 is required', - line2: 'Address 2 is required', - }, - firstName: 'First name is required', - lastName: 'Last name is required', - }, - touched: { - address: { - line1: true, - line2: false, - }, - firstName: true, - lastName: false, - }, - }); - - expect(getAllErrors()).toEqual({ - 'address.line1': 'Address 1 is required', - firstName: 'First name is required', - }); - }); - - test('handle single undefined, touched value in array in errors object', () => { - const { getAllErrors } = createErrorHelper<{ - subjects: undefined[]; - }>({ - errors: { - subjects: [undefined], - } as never, - touched: { - subjects: [true], - } as never, - }); - expect(getAllErrors()).toEqual({}); - }); - - test('handle undefined in array in errors object', () => { - const { getAllErrors } = createErrorHelper<{ - subjects: ({ content: string } | undefined)[]; - }>({ - errors: { - subjects: [ - undefined, - { content: 'Error two' }, - { content: 'Error three' }, - ], - } as never, - touched: { - subjects: [{ content: false }, { content: true }, { content: true }], - } as never, - }); - expect(getAllErrors()).toEqual({ - 'subjects.1.content': 'Error two', - 'subjects.2.content': 'Error three', - }); - }); - }); -}); diff --git a/src/explore-education-statistics-common/src/validation/createErrorHelper.ts b/src/explore-education-statistics-common/src/validation/createErrorHelper.ts deleted file mode 100644 index 852df1a6a8a..00000000000 --- a/src/explore-education-statistics-common/src/validation/createErrorHelper.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { FormikErrors, FormikTouched, FormikValues } from 'formik'; -import get from 'lodash/get'; - -const createErrorHelper = ({ - touched, - errors, - submitCount = 0, -}: { - touched: FormikTouched; - errors: FormikErrors; - submitCount?: number; -}) => { - const getAllErrors = ( - errorGroup: FormikValues, - keyPrefix?: string, - ): { [key: string]: string } => { - return Object.entries(errorGroup).reduce((acc, [key, error]) => { - const errorKey = keyPrefix ? `${keyPrefix}.${key}` : key; - - const isTouched = get(touched, errorKey, false); - - // If the form isn't submitted, only show errors for touched fields. - if ((!submitCount && !isTouched) || typeof error === 'undefined') { - return acc; - } - - if (typeof error === 'string') { - return { - ...acc, - [errorKey]: error, - }; - } - - return { - ...acc, - ...getAllErrors(error, errorKey), - }; - }, {}); - }; - - const getError = (name: keyof T | string): string => { - const isTouched = get(touched, name, false); - - if (!submitCount && !isTouched) { - return ''; - } - - const error = get(errors, name); - - return typeof error === 'string' ? error : ''; - }; - - const hasError = (value: keyof T | string): boolean => { - return !!getError(value); - }; - - return { - getError, - hasError, - getAllErrors: () => getAllErrors(errors), - }; -}; - -export default createErrorHelper; diff --git a/src/explore-education-statistics-common/src/validation/serverValidations.ts b/src/explore-education-statistics-common/src/validation/serverValidations.ts index f3795856318..f00ce2d9046 100644 --- a/src/explore-education-statistics-common/src/validation/serverValidations.ts +++ b/src/explore-education-statistics-common/src/validation/serverValidations.ts @@ -4,7 +4,6 @@ import { } from '@common/services/types/problemDetails'; import { Dictionary, Path } from '@common/types'; import { AxiosError, isAxiosError } from 'axios'; -import { FormikErrors } from 'formik'; import camelCase from 'lodash/camelCase'; import has from 'lodash/has'; import set from 'lodash/set'; @@ -127,31 +126,14 @@ function normalizeField( } /** - * Convert server validation errors to Formik error messages. + * Convert server validation errors to RHF error messages. * * @param response The server validation error response. * @param formValues The form values that were submitted. * @param messageMappers Mappings between server validation errors and field error messages. * @param fallbackMapper Optional fallback mapper if no mapping is found. */ -export function convertServerFieldErrors( - response: ValidationProblemDetails, - formValues: FormValues, - messageMappers: FieldMessageMapper[] = [], - fallbackMapper?: FieldMessageMapper, -): FormikErrors { - return mapServerFieldErrors(response, messageMappers, fallbackMapper).reduce< - FormikErrors - >((errors, { field, message }) => { - if (has(formValues, field)) { - set(errors, field, message); - } - - return errors; - }, {}); -} - -export function rhfConvertServerFieldErrors( +export function convertServerFieldErrors( response: ValidationProblemDetails, formValues: FormValues, messageMappers: FieldMessageMapper[] = [], diff --git a/src/explore-education-statistics-frontend/package.json b/src/explore-education-statistics-frontend/package.json index 021d3ab1fd4..049d86960f9 100644 --- a/src/explore-education-statistics-frontend/package.json +++ b/src/explore-education-statistics-frontend/package.json @@ -18,7 +18,6 @@ "express": "^4.18.2", "express-basic-auth": "^1.2.0", "govuk-frontend": "^5.2.0", - "formik": "^2.4.2", "helmet": "^4.1.1", "immer": "^10.0.2", "leaflet": "^1.9.4", diff --git a/src/explore-education-statistics-frontend/src/modules/cookies/CookiesPage.tsx b/src/explore-education-statistics-frontend/src/modules/cookies/CookiesPage.tsx index cfdc64b9948..aab31c622df 100644 --- a/src/explore-education-statistics-frontend/src/modules/cookies/CookiesPage.tsx +++ b/src/explore-education-statistics-frontend/src/modules/cookies/CookiesPage.tsx @@ -1,8 +1,8 @@ import Button from '@common/components/Button'; import ButtonText from '@common/components/ButtonText'; -import RHFFormFieldRadioGroup from '@common/components/form/rhf/RHFFormFieldRadioGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldRadioGroup from '@common/components/form/FormFieldRadioGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import useMounted from '@common/hooks/useMounted'; import { Dictionary } from '@common/types'; import Yup from '@common/validation/yup'; @@ -78,7 +78,7 @@ function CookiesPage({ cookies }: Props) { We use cookies to store information about how you use the GOV.UK website, such as the pages you visit.

- { window.scrollTo(0, 0); @@ -115,7 +115,7 @@ function CookiesPage({ cookies }: Props) { We do not allow Google to use or share the data about how you use this site.

- + name="googleAnalytics" legend="Allow Google Analytics cookies" legendHidden @@ -149,7 +149,7 @@ function CookiesPage({ cookies }: Props) {

-
+ ); }} diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DownloadStep.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DownloadStep.tsx index be47cd94bdd..08b9b5671c0 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DownloadStep.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DownloadStep.tsx @@ -1,8 +1,8 @@ import { FormFieldset } from '@common/components/form'; import { CheckboxOption } from '@common/components/form/FormCheckboxGroup'; -import RHFFormFieldCheckboxGroup from '@common/components/form/rhf/RHFFormFieldCheckboxGroup'; -import FormProvider from '@common/components/form/rhf/FormProvider'; -import RHFForm from '@common/components/form/rhf/RHFForm'; +import FormFieldCheckboxGroup from '@common/components/form/FormFieldCheckboxGroup'; +import FormProvider from '@common/components/form/FormProvider'; +import Form from '@common/components/form/Form'; import { InjectedWizardProps } from '@common/modules/table-tool/components/Wizard'; import WizardStepHeading from '@common/modules/table-tool/components/WizardStepHeading'; import WizardStepFormActions from '@common/modules/table-tool/components/WizardStepFormActions'; @@ -137,10 +137,10 @@ const DownloadStep = ({ // isMounted check required as Form context can be undefined // if the step is active on page load. return isActive && isMounted ? ( - +
{checkboxOptions.length > 0 && ( - + name="files" order={[]} legend="Choose files from the list below" @@ -162,7 +162,7 @@ const DownloadStep = ({ ) : (

No downloads available.

)} - + ) : ( {({ formState, reset }) => { return isActive ? ( - - +
+ name="releaseId" legend={legend} disabled={formState.isSubmitting} @@ -103,7 +103,7 @@ export default function ReleaseForm({ ) : (

No releases available.

)} - + ) : ( = ({ > {({ formState }) => { return ( - - +
+ label="Enter your email address" hint="This will only be used to subscribe you to updates. You can unsubscribe at any time" name="email" @@ -126,7 +126,7 @@ const SubscriptionPage: NextPage = ({ ? 'Submitting' : 'Subscribe'} - + ); }} From cb2c8b53e8a7d2736987b97f79e6580da99730f3 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 14 May 2024 11:13:36 +0100 Subject: [PATCH 09/66] EES-5140 Fix UI test failure --- .../bau/publish_methodology_publication_update.robot | 5 ++--- tests/robot-tests/tests/libs/common.robot | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/robot-tests/tests/admin_and_public_2/bau/publish_methodology_publication_update.robot b/tests/robot-tests/tests/admin_and_public_2/bau/publish_methodology_publication_update.robot index d96abed9a40..0d6e872bfae 100644 --- a/tests/robot-tests/tests/admin_and_public_2/bau/publish_methodology_publication_update.robot +++ b/tests/robot-tests/tests/admin_and_public_2/bau/publish_methodology_publication_update.robot @@ -16,7 +16,6 @@ ${PUBLICATION_NAME}= UI tests - publish methodology p ${PUBLIC_METHODOLOGY_URL_ENDING}= /methodology/ui-tests-publish-methodology-publication-update-%{RUN_IDENTIFIER} ${PUBLICATION_NAME_UPDATED}= ${PUBLICATION_NAME} updated ${PUBLIC_PUBLICATION_URL_ENDING}= /find-statistics/ui-tests-publish-methodology-publication-update-%{RUN_IDENTIFIER} -${EXPECTED_PUBLIC_PUBLICATION_URL_ENDING}= %{PUBLIC_URL}${PUBLIC_PUBLICATION_URL_ENDING} ${RELEASE_NAME}= Academic year Q1 ${ACADEMIC_YEAR}= /2046-47 @@ -64,8 +63,8 @@ Update publication details user enters text into element label:Publication title ${PUBLICATION_NAME_UPDATED} user clicks button Update publication details ${modal}= user waits until modal is visible Confirm publication changes - user checks element contains valid URL id:before-url ${EXPECTED_PUBLIC_PUBLICATION_URL_ENDING} - user checks element contains valid url id:after-url ${EXPECTED_PUBLIC_PUBLICATION_URL_ENDING}-updated + user checks url contains id:before-url ${PUBLIC_PUBLICATION_URL_ENDING} + user checks url contains id:after-url ${PUBLIC_PUBLICATION_URL_ENDING}-updated user clicks button Confirm ${modal} user waits until modal is not visible Confirm publication changes user checks summary list contains Publication title ${PUBLICATION_NAME_UPDATED} diff --git a/tests/robot-tests/tests/libs/common.robot b/tests/robot-tests/tests/libs/common.robot index 8447c96f2d9..9bd2b1e5d7c 100644 --- a/tests/robot-tests/tests/libs/common.robot +++ b/tests/robot-tests/tests/libs/common.robot @@ -690,11 +690,6 @@ user checks element count is x [Arguments] ${locator} ${count} page should contain element ${locator} count=${count} -user checks element contains valid URL - [Arguments] ${locator} ${expected_url} - ${actual_url}= get value ${locator} - should be equal as strings ${actual_url} ${expected_url} - user checks url contains [Arguments] ${text} ${current_url}= get location From e65eb542b88420b198a342169183b196998fa86a Mon Sep 17 00:00:00 2001 From: SaicharanMuthyapwar Date: Tue, 14 May 2024 11:50:07 +0100 Subject: [PATCH 10/66] EES-5142 Actioned as per PR comments --- .../general-public/pages/FindStatisticsPage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts b/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts index c2dfd577767..c677440c888 100644 --- a/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts +++ b/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts @@ -11,7 +11,7 @@ export default class FindStatisticsPage { } async navigateToPublicReleasePage(publicationName: string) { - await this.releaseLink(publicationName).click(); - await this.page.waitForTimeout(2000); + await this.releaseLink(publicationName).click({ force: true }); + await this.page.waitForNavigation(); } } From 116b292a90125a8792cb7acd4222e1621a5aecd5 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Mon, 13 May 2024 18:01:49 +0100 Subject: [PATCH 11/66] EES-5139 Replace `File.PublicDataSetVersionId` with data set ID and version columns --- .../DataSetCandidatesControllerTests.cs | 4 +- ...etVersionIdWithSeparateColumns.Designer.cs | 2194 +++++++++++++++++ ...blicDataSetVersionIdWithSeparateColumns.cs | 68 + .../ContentDbContextModelSnapshot.cs | 12 +- .../Services/Public.Data/ReleaseService.cs | 5 +- .../DataSetFilesControllerTests.cs | 13 +- .../Fixtures/FileGeneratorExtensions.cs | 39 +- .../Database/ContentDbContext.cs | 13 +- .../File.cs | 7 +- .../DataSetFileService.cs | 4 +- .../DataSetVersion.cs | 3 + .../Services/DataSetService.cs | 3 +- 12 files changed, 2339 insertions(+), 26 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.Designer.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetCandidatesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetCandidatesControllerTests.cs index ee617f7b38c..0b3b54720df 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetCandidatesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetCandidatesControllerTests.cs @@ -18,7 +18,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Controllers.Api.Public.Data; -public class DataSetCandidatesControllerTests(TestApplicationFactory testApp) : IntegrationTestFixture(testApp) +public abstract class DataSetCandidatesControllerTests(TestApplicationFactory testApp) : IntegrationTestFixture(testApp) { private const string BaseUrl = "api/public-data/data-set-candidates"; @@ -161,7 +161,7 @@ public async Task ReleaseFileHasAssociatedApiDataSet_NotReturned() File file = DataFixture .DefaultFile() - .WithPublicDataSetVersionId(Guid.NewGuid()); + .WithPublicApiDataSetId(Guid.NewGuid()); var releaseVersion = release.Versions.Single(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.Designer.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.Designer.cs new file mode 100644 index 00000000000..beb260a6ace --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.Designer.cs @@ -0,0 +1,2194 @@ +// +using System; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations +{ + [DbContext(typeof(ContentDbContext))] + [Migration("20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns")] + partial class EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Common.Model.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContactTelNo") + .HasColumnType("nvarchar(max)"); + + b.Property("TeamEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TeamName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Common.Model.FreeTextRank", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Rank") + .HasColumnType("int"); + + b.ToTable((string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContentBlockId") + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("LegacyCreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Resolved") + .HasColumnType("datetime2"); + + b.Property("ResolvedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ContentBlockId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ResolvedById"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentSectionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Locked") + .HasColumnType("datetime2"); + + b.Property("LockedById") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ContentSectionId"); + + b.HasIndex("LockedById"); + + b.HasIndex("ReleaseVersionId"); + + b.HasIndex("Type"); + + b.ToTable("ContentBlock", (string)null); + + b.HasDiscriminator("Type").HasValue("ContentBlock"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Caption") + .HasColumnType("nvarchar(max)"); + + b.Property("Heading") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.HasKey("Id"); + + b.HasIndex("ReleaseVersionId"); + + b.HasIndex("Type"); + + b.ToTable("ContentSections"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("LatestDraftVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("LatestPublishedVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("LatestDraftVersionId") + .IsUnique() + .HasFilter("[LatestDraftVersionId] IS NOT NULL"); + + b.HasIndex("LatestPublishedVersionId") + .IsUnique() + .HasFilter("[LatestPublishedVersionId] IS NOT NULL"); + + b.ToTable("DataBlocks", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentBlockId") + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("DataBlockParentId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DataBlockId"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ContentBlockId"); + + b.HasIndex("DataBlockParentId"); + + b.HasIndex("ReleaseVersionId"); + + b.ToTable("DataBlockVersions", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("ExpectedImportedRows") + .HasColumnType("int"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("GeographicLevels") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImportedRows") + .HasColumnType("int"); + + b.Property("LastProcessedRowIndex") + .HasColumnType("int"); + + b.Property("MetaFileId") + .HasColumnType("uniqueidentifier"); + + b.Property("StagePercentageComplete") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("TotalRows") + .HasColumnType("int"); + + b.Property("ZipFileId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("FileId"), new[] { "Status" }); + + b.HasIndex("MetaFileId") + .IsUnique(); + + b.HasIndex("ZipFileId") + .IsUnique() + .HasFilter("[ZipFileId] IS NOT NULL"); + + b.ToTable("DataImports"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImportError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("DataImportId") + .HasColumnType("uniqueidentifier"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DataImportId"); + + b.ToTable("DataImportErrors"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("EmbedBlocks"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DataBlockId") + .HasColumnType("uniqueidentifier"); + + b.Property("DataBlockParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("UpdatedById") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DataBlockId") + .IsUnique(); + + b.HasIndex("DataBlockParentId"); + + b.HasIndex("ReleaseVersionId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("FeaturedTables"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentLength") + .HasColumnType("bigint"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DataSetFileId") + .HasColumnType("uniqueidentifier"); + + b.Property("DataSetFileMeta") + .HasColumnType("nvarchar(max)"); + + b.Property("DataSetFileVersion") + .HasColumnType("int"); + + b.Property("Filename") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublicApiDataSetId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicApiDataSetVersion") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ReplacedById") + .HasColumnType("uniqueidentifier"); + + b.Property("ReplacingId") + .HasColumnType("uniqueidentifier"); + + b.Property("RootPath") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("SubjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReplacedById") + .IsUnique() + .HasFilter("[ReplacedById] IS NOT NULL"); + + b.HasIndex("ReplacingId") + .IsUnique() + .HasFilter("[ReplacingId] IS NOT NULL"); + + b.HasIndex("SourceId"); + + b.HasIndex("Type"); + + b.HasIndex("PublicApiDataSetId", "PublicApiDataSetVersion") + .IsUnique() + .HasFilter("[PublicApiDataSetId] IS NOT NULL AND [PublicApiDataSetVersion] IS NOT NULL"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.GlossaryEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.ToTable("GlossaryEntries"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("GuidanceText") + .HasColumnType("nvarchar(max)"); + + b.Property("GuidanceTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Trend") + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("UpdatedById") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseVersionId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("KeyStatistics"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("LatestPublishedVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("OwningPublicationSlug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwningPublicationTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LatestPublishedVersionId") + .IsUnique() + .HasFilter("[LatestPublishedVersionId] IS NOT NULL"); + + b.ToTable("Methodologies"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("MethodologyVersionId"); + + b.ToTable("MethodologyFiles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayDate") + .HasColumnType("datetime2"); + + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("UpdatedById") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("MethodologyVersionId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("MethodologyNotes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyRedirect", b => + { + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.HasKey("MethodologyVersionId", "Slug"); + + b.ToTable("MethodologyRedirects"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("InternalReleaseNote") + .HasColumnType("nvarchar(max)"); + + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("MethodologyVersionId"); + + b.ToTable("MethodologyStatus"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AlternativeSlug") + .HasColumnType("nvarchar(max)"); + + b.Property("AlternativeTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("MethodologyId") + .HasColumnType("uniqueidentifier"); + + b.Property("PreviousVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("PublishingStrategy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledWithReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("MethodologyId"); + + b.HasIndex("PreviousVersionId"); + + b.HasIndex("ScheduledWithReleaseVersionId"); + + b.ToTable("MethodologyVersions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersionContent", b => + { + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Annexes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("MethodologyVersionId"); + + b.ToTable("MethodologyVersions", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Permalink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("DataSetTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MigratedFromLegacy") + .HasColumnType("bit"); + + b.Property("PublicationTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("SubjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ReleaseVersionId"); + + b.HasIndex("SubjectId"); + + b.ToTable("Permalinks"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactId") + .HasColumnType("uniqueidentifier"); + + b.Property("LatestPublishedReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReleaseSeries") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("nvarchar(160)"); + + b.Property("SupersededById") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TopicId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("LatestPublishedReleaseVersionId") + .IsUnique() + .HasFilter("[LatestPublishedReleaseVersionId] IS NOT NULL"); + + b.HasIndex("SupersededById"); + + b.HasIndex("TopicId"); + + b.ToTable("Publications"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationMethodology", b => + { + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("MethodologyId") + .HasColumnType("uniqueidentifier"); + + b.Property("Owner") + .HasColumnType("bit"); + + b.HasKey("PublicationId", "MethodologyId"); + + b.HasIndex("MethodologyId"); + + b.ToTable("PublicationMethodologies"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRedirect", b => + { + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.HasKey("PublicationId", "Slug"); + + b.ToTable("PublicationRedirects"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Releases"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FilterSequence") + .HasColumnType("nvarchar(max)"); + + b.Property("IndicatorSequence") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ReleaseVersionId"); + + b.ToTable("ReleaseFiles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("InternalReleaseNote") + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseVersionId"); + + b.ToTable("ReleaseStatus"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DataGuidance") + .HasColumnType("nvarchar(max)"); + + b.Property("NextReleaseDate") + .HasColumnType("nvarchar(max)"); + + b.Property("NotifiedOn") + .HasColumnType("datetime2"); + + b.Property("NotifySubscribers") + .HasColumnType("bit"); + + b.Property("PreReleaseAccessList") + .HasColumnType("nvarchar(max)"); + + b.Property("PreviousVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublishScheduled") + .HasColumnType("datetime2"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("RelatedInformation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReleaseName") + .HasColumnType("nvarchar(max)"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("bit"); + + b.Property("TimePeriodCoverage") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatePublishedDate") + .HasColumnType("bit"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("PublicationId"); + + b.HasIndex("ReleaseId"); + + b.HasIndex("Type"); + + b.HasIndex("PreviousVersionId", "Version"); + + b.ToTable("ReleaseVersions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("Summary") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("ThemeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ThemeId"); + + b.ToTable("Topics"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Update", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseVersionId"); + + b.ToTable("Update"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("PublicationId"); + + b.ToTable("UserPublicationInvites"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("datetime2"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("PublicationId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPublicationRoles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailSent") + .HasColumnType("bit"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("bit"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseVersionId"); + + b.ToTable("UserReleaseInvites"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("datetime2"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("bit"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("ReleaseVersionId"); + + b.HasIndex("UserId"); + + b.ToTable("UserReleaseRoles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("Charts") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Charts"); + + b.Property("Heading") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Heading"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Query") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Query"); + + b.Property("Source") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Table") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Table"); + + b.HasDiscriminator().HasValue("DataBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlockLink", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("EmbedBlockId") + .HasColumnType("uniqueidentifier") + .HasColumnName("EmbedBlockId"); + + b.HasIndex("EmbedBlockId") + .IsUnique() + .HasFilter("[EmbedBlockId] IS NOT NULL"); + + b.HasDiscriminator().HasValue("EmbedBlockLink"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.HtmlBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("Body") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("nvarchar(max)") + .HasColumnName("Body"); + + b.HasDiscriminator().HasValue("HtmlBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MarkDownBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("Body") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("nvarchar(max)") + .HasColumnName("Body"); + + b.HasDiscriminator().HasValue("MarkDownBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticDataBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic"); + + b.Property("DataBlockId") + .HasColumnType("uniqueidentifier"); + + b.Property("DataBlockParentId") + .HasColumnType("uniqueidentifier"); + + b.HasIndex("DataBlockId"); + + b.HasIndex("DataBlockParentId"); + + b.ToTable("KeyStatisticsDataBlock", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticText", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic"); + + b.Property("Statistic") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("KeyStatisticsText", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Comment", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", "ContentBlock") + .WithMany("Comments") + .HasForeignKey("ContentBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "ResolvedBy") + .WithMany() + .HasForeignKey("ResolvedById"); + + b.Navigation("ContentBlock"); + + b.Navigation("CreatedBy"); + + b.Navigation("ResolvedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", "ContentSection") + .WithMany("Content") + .HasForeignKey("ContentSectionId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany() + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ContentSection"); + + b.Navigation("LockedBy"); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany("Content") + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockVersion", "LatestDraftVersion") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", "LatestDraftVersionId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockVersion", "LatestPublishedVersion") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", "LatestPublishedVersionId"); + + b.Navigation("LatestDraftVersion"); + + b.Navigation("LatestPublishedVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockVersion", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", "ContentBlock") + .WithMany() + .HasForeignKey("ContentBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", "DataBlockParent") + .WithMany() + .HasForeignKey("DataBlockParentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany("DataBlockVersions") + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ContentBlock"); + + b.Navigation("DataBlockParent"); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "File") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "FileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "MetaFile") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "MetaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ZipFile") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "ZipFileId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("File"); + + b.Navigation("MetaFile"); + + b.Navigation("ZipFile"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImportError", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "DataImport") + .WithMany("Errors") + .HasForeignKey("DataImportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataImport"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", "DataBlock") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", "DataBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", "DataBlockParent") + .WithMany() + .HasForeignKey("DataBlockParentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany("FeaturedTables") + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("DataBlock"); + + b.Navigation("DataBlockParent"); + + b.Navigation("ReleaseVersion"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ReplacedBy") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ReplacedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "Replacing") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ReplacingId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "Source") + .WithMany() + .HasForeignKey("SourceId"); + + b.Navigation("CreatedBy"); + + b.Navigation("ReplacedBy"); + + b.Navigation("Replacing"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.GlossaryEntry", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany("KeyStatistics") + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("ReleaseVersion"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "LatestPublishedVersion") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", "LatestPublishedVersionId"); + + b.Navigation("LatestPublishedVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyFile", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("MethodologyVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyNote", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany("Notes") + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("CreatedBy"); + + b.Navigation("MethodologyVersion"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyRedirect", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MethodologyVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyStatus", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("MethodologyVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", "Methodology") + .WithMany("Versions") + .HasForeignKey("MethodologyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "PreviousVersion") + .WithMany() + .HasForeignKey("PreviousVersionId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ScheduledWithReleaseVersion") + .WithMany() + .HasForeignKey("ScheduledWithReleaseVersionId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Methodology"); + + b.Navigation("PreviousVersion"); + + b.Navigation("ScheduledWithReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersionContent", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", null) + .WithOne("MethodologyContent") + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersionContent", "MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Common.Model.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "LatestPublishedReleaseVersion") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "LatestPublishedReleaseVersionId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "SupersededBy") + .WithMany() + .HasForeignKey("SupersededById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", "Topic") + .WithMany("Publications") + .HasForeignKey("TopicId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ExternalMethodology", "ExternalMethodology", b1 => + { + b1.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("PublicationId"); + + b1.ToTable("ExternalMethodology", (string)null); + + b1.WithOwner() + .HasForeignKey("PublicationId"); + }); + + b.Navigation("Contact"); + + b.Navigation("ExternalMethodology"); + + b.Navigation("LatestPublishedReleaseVersion"); + + b.Navigation("SupersededBy"); + + b.Navigation("Topic"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationMethodology", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", "Methodology") + .WithMany("Publications") + .HasForeignKey("MethodologyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("Methodologies") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Methodology"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRedirect", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany() + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseFile", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany() + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseStatus", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany("ReleaseStatuses") + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "PreviousVersion") + .WithMany() + .HasForeignKey("PreviousVersionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("ReleaseVersions") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("Versions") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("PreviousVersion"); + + b.Navigation("Publication"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Theme", "Theme") + .WithMany("Topics") + .HasForeignKey("ThemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Update", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany("Updates") + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationInvite", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany() + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationRole", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany() + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Publication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseInvite", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany() + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ReleaseVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseRole", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", "ReleaseVersion") + .WithMany() + .HasForeignKey("ReleaseVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ReleaseVersion"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlockLink", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", "EmbedBlock") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlockLink", "EmbedBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbedBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticDataBlock", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", "DataBlock") + .WithMany() + .HasForeignKey("DataBlockId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlockParent", "DataBlockParent") + .WithMany() + .HasForeignKey("DataBlockParentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", null) + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticDataBlock", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataBlock"); + + b.Navigation("DataBlockParent"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticText", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", null) + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticText", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", b => + { + b.Navigation("Content"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", b => + { + b.Navigation("Errors"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => + { + b.Navigation("Publications"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", b => + { + b.Navigation("MethodologyContent") + .IsRequired(); + + b.Navigation("Notes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.Navigation("Methodologies"); + + b.Navigation("ReleaseVersions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion", b => + { + b.Navigation("Content"); + + b.Navigation("DataBlockVersions"); + + b.Navigation("FeaturedTables"); + + b.Navigation("KeyStatistics"); + + b.Navigation("ReleaseStatuses"); + + b.Navigation("Updates"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Theme", b => + { + b.Navigation("Topics"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", b => + { + b.Navigation("Publications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.cs new file mode 100644 index 00000000000..52647d525e6 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20240514011937_EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations +{ + /// + public partial class EES5139_ReplaceFilePublicDataSetVersionIdWithSeparateColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Files_PublicDataSetVersionId", + table: "Files"); + + migrationBuilder.DropColumn( + name: "PublicDataSetVersionId", + table: "Files"); + + migrationBuilder.AddColumn( + name: "PublicApiDataSetId", + table: "Files", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.AddColumn( + name: "PublicApiDataSetVersion", + table: "Files", + type: "nvarchar(20)", + maxLength: 20, + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Files_PublicApiDataSetId_PublicApiDataSetVersion", + table: "Files", + columns: new[] { "PublicApiDataSetId", "PublicApiDataSetVersion" }, + unique: true, + filter: "[PublicApiDataSetId] IS NOT NULL AND [PublicApiDataSetVersion] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Files_PublicApiDataSetId_PublicApiDataSetVersion", + table: "Files"); + + migrationBuilder.DropColumn( + name: "PublicApiDataSetVersion", + table: "Files"); + + migrationBuilder.DropColumn( + name: "PublicApiDataSetId", + table: "Files"); + + migrationBuilder.AddColumn( + name: "PublicDataSetVersionId", + table: "Files"); + + migrationBuilder.CreateIndex( + name: "IX_Files_PublicDataSetVersionId", + table: "Files", + column: "PublicDataSetVersionId"); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs index ff4af181475..9a57ffbb146 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs @@ -441,9 +441,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("PublicDataSetVersionId") + b.Property("PublicApiDataSetId") .HasColumnType("uniqueidentifier"); + b.Property("PublicApiDataSetVersion") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + b.Property("ReplacedById") .HasColumnType("uniqueidentifier"); @@ -468,8 +472,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CreatedById"); - b.HasIndex("PublicDataSetVersionId"); - b.HasIndex("ReplacedById") .IsUnique() .HasFilter("[ReplacedById] IS NOT NULL"); @@ -482,6 +484,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Type"); + b.HasIndex("PublicApiDataSetId", "PublicApiDataSetVersion") + .IsUnique() + .HasFilter("[PublicApiDataSetId] IS NOT NULL AND [PublicApiDataSetVersion] IS NOT NULL"); + b.ToTable("Files"); }); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs index ba4b1db2fd8..d7727e67563 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs @@ -40,13 +40,14 @@ private async Task> CheckReleaseVersionExis .SingleOrNotFoundAsync(rv => rv.Id == releaseVersionId, cancellationToken: cancellationToken); } - private async Task>> GetApiDataSetCandidates(Guid releaseVersionId) + private async Task>> GetApiDataSetCandidates( + Guid releaseVersionId) { return await contentDbContext.ReleaseFiles .AsNoTracking() .Where(rf => rf.ReleaseVersionId == releaseVersionId) .Where(rf => rf.File.Type == FileType.Data) - .Where(rf => rf.File.PublicDataSetVersionId == null) + .Where(rf => rf.File.PublicApiDataSetId == null) .Where(rf => rf.File.ReplacedById == null) .Where(rf => rf.File.ReplacingId == null) .Select(rf => new ApiDataSetCandidateViewModel diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs index 6b4d8d459b5..6a573b26345 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs @@ -698,8 +698,8 @@ public async Task FilterByDataSetType_Api_ReturnsOnlyDataSetsWithAssociatedApiDa var releaseVersionFiles = _fixture.DefaultReleaseFile() .WithReleaseVersion(publication.ReleaseVersions[0]) .WithFiles(_fixture.DefaultFile() - .ForIndex(0, s => s.SetPublicDataSetVersionId(Guid.NewGuid())) - .ForIndex(1, s => s.SetPublicDataSetVersionId(Guid.NewGuid())) + .ForIndex(0, s => s.SetPublicApiDataSetId(Guid.NewGuid())) + .ForIndex(1, s => s.SetPublicApiDataSetId(Guid.NewGuid())) .GenerateList(5)) .GenerateList(); @@ -722,7 +722,7 @@ public async Task FilterByDataSetType_Api_ReturnsOnlyDataSetsWithAssociatedApiDa var pagedResult = response.AssertOk>(); var expectedReleaseFiles = releaseVersionFiles - .Where(rf => rf.File.PublicDataSetVersionId.HasValue) + .Where(rf => rf.File.PublicApiDataSetId.HasValue) .OrderBy(rf => rf.Name) .ToList(); @@ -746,8 +746,8 @@ public async Task FilterByDataSetType_AllOrUnset_ReturnsAllDataSets(DataSetType? var releaseVersionFiles = _fixture.DefaultReleaseFile() .WithReleaseVersion(publication.ReleaseVersions[0]) .WithFiles(_fixture.DefaultFile() - .ForIndex(0, s => s.SetPublicDataSetVersionId(Guid.NewGuid())) - .ForIndex(1, s => s.SetPublicDataSetVersionId(Guid.NewGuid())) + .ForIndex(0, s => s.SetPublicApiDataSetId(Guid.NewGuid())) + .ForIndex(1, s => s.SetPublicApiDataSetId(Guid.NewGuid())) .GenerateList(5)) .GenerateList(); @@ -1782,8 +1782,7 @@ private static void AssertResultsForExpectedReleaseFiles( () => Assert.Equal(releaseVersion.Id == publication.LatestPublishedReleaseVersionId, viewModel.LatestData), () => Assert.Equal(releaseFile.ReleaseVersion.Published!.Value, viewModel.Published), - () => Assert.Equal(releaseFile.File.PublicDataSetVersionId.HasValue, - viewModel.HasApiDataSet) + () => Assert.Equal(releaseFile.File.PublicApiDataSetId.HasValue, viewModel.HasApiDataSet) ); }); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs index 1783f62e3fa..3f2ed85097c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs @@ -3,6 +3,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; +using Semver; namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; @@ -68,10 +69,22 @@ public static Generator WithReplacedById( Guid replacedById) => generator.ForInstance(s => s.SetReplacedById(replacedById)); - public static Generator WithPublicDataSetVersionId( + public static Generator WithPublicApiDataSetId( this Generator generator, - Guid publicDataSetVersionId) - => generator.ForInstance(s => s.SetPublicDataSetVersionId(publicDataSetVersionId)); + Guid publicDataSetId) + => generator.ForInstance(s => s.SetPublicApiDataSetId(publicDataSetId)); + + public static Generator WithPublicApiDataSetVersion( + this Generator generator, + int major, + int minor, + int patch = 0) + => generator.ForInstance(s => s.SetPublicApiDataSetVersion(major, minor, patch)); + + public static Generator WithPublicApiDataSetVersion( + this Generator generator, + SemVersion version) + => generator.ForInstance(s => s.SetPublicApiDataSetVersion(version)); public static Generator WithRootPath( this Generator generator, @@ -129,10 +142,24 @@ public static InstanceSetters SetReplacedById( Guid replacedById) => setters.Set(f => f.ReplacedById, replacedById); - public static InstanceSetters SetPublicDataSetVersionId( + public static InstanceSetters SetPublicApiDataSetId( + this InstanceSetters setters, + Guid publicDataSetId) + => setters.Set(f => f.PublicApiDataSetId, publicDataSetId); + + public static InstanceSetters SetPublicApiDataSetVersion( + this InstanceSetters setters, + int major, + int minor, + int patch = 0) + => setters.Set( + f => f.PublicApiDataSetVersion, + new SemVersion(major: major, minor: minor, patch: patch)); + + public static InstanceSetters SetPublicApiDataSetVersion( this InstanceSetters setters, - Guid publicDataSetVersionId) - => setters.Set(f => f.PublicDataSetVersionId, publicDataSetVersionId); + SemVersion version) + => setters.Set(f => f.PublicApiDataSetVersion, version); public static InstanceSetters SetReplacing( this InstanceSetters setters, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs index 310c9771715..cd3f9178303 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; +using Semver; // ReSharper disable StringLiteralTypo namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Database @@ -432,7 +433,17 @@ private static void ConfigureFile(ModelBuilder modelBuilder) .HasConversion( v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject(v)); - entity.HasIndex(f => f.PublicDataSetVersionId); + + entity.Property(f => f.PublicApiDataSetVersion) + .HasMaxLength(20) + .HasConversion( + v => v.ToString(), + v => SemVersion.Parse(v, SemVersionStyles.Strict, 20) + ); + + entity.HasIndex(f => new { + PublicDataSetId = f.PublicApiDataSetId, PublicDataSetVersion = f.PublicApiDataSetVersion }) + .IsUnique(); }); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs index a6cd2c5dda1..7e7b2148d28 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs @@ -1,6 +1,7 @@ #nullable enable using System; using GovUk.Education.ExploreEducationStatistics.Common.Model; +using Semver; namespace GovUk.Education.ExploreEducationStatistics.Content.Model { @@ -22,9 +23,11 @@ public class File : ICreatedTimestamp public int? DataSetFileVersion { get; set; } - public DataSetFileMeta? DataSetFileMeta { get; set; } + public DataSetFileMeta? DataSetFileMeta { get; set; } - public Guid? PublicDataSetVersionId { get; set; } + public Guid? PublicApiDataSetId { get; set; } + + public SemVersion? PublicApiDataSetVersion { get; set; } public Guid? ReplacedById { get; set; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs index 254c7222dde..1a55f399b9d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs @@ -118,7 +118,7 @@ private static Expression, DataSetFileSumm LatestData = result.Value.ReleaseVersionId == result.Value.ReleaseVersion.Publication.LatestPublishedReleaseVersionId, Published = result.Value.ReleaseVersion.Published!.Value, - HasApiDataSet = result.Value.File.PublicDataSetVersionId.HasValue, + HasApiDataSet = result.Value.File.PublicApiDataSetId.HasValue, Meta = BuildDataSetFileMetaViewModel( result.Value.File.DataSetFileMeta, result.Value.FilterSequence, @@ -343,7 +343,7 @@ internal static IQueryable OfDataSetType( return dataSetType switch { DataSetType.All => query, - DataSetType.Api => query.Where(rf => rf.File.PublicDataSetVersionId.HasValue), + DataSetType.Api => query.Where(rf => rf.File.PublicApiDataSetId.HasValue), _ => throw new ArgumentOutOfRangeException(nameof(dataSetType)), }; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Model/DataSetVersion.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Model/DataSetVersion.cs index 68520f1aee5..23b5442bf0a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Model/DataSetVersion.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Model/DataSetVersion.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Semver; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Model; @@ -67,6 +68,8 @@ public class DataSetVersion : ICreatedUpdatedTimestamps $"{VersionMajor}.{VersionMinor}"; + public SemVersion FullSemanticVersion() => new(major: VersionMajor, minor: VersionMinor, patch: VersionPatch); + public DataSetVersionType VersionType => VersionMinor == 0 ? DataSetVersionType.Major : DataSetVersionType.Minor; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs index 7a9f392d61e..6d4c293c9ae 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs @@ -148,7 +148,8 @@ private async Task UpdateFilePublicDataSetVersionId( DataSetVersion dataSetVersion, CancellationToken cancellationToken) { - releaseFile.File.PublicDataSetVersionId = dataSetVersion.Id; + releaseFile.File.PublicApiDataSetId = dataSetVersion.DataSetId; + releaseFile.File.PublicApiDataSetVersion = dataSetVersion.FullSemanticVersion(); await contentDbContext.SaveChangesAsync(cancellationToken); } From 2b7e33361a273c68e1230784b41dcc521f388791 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Mon, 13 May 2024 23:21:33 +0100 Subject: [PATCH 12/66] EES-5139 Update data set file endpoint view models with API data set details --- .../DataSetFilesControllerCachingTests.cs | 10 ++- .../DataSetFilesControllerTests.cs | 72 +++++++++---------- .../File.cs | 4 ++ .../DataSetFileService.cs | 18 ++++- .../DataSetFileApiViewModel.cs | 8 +++ .../DataSetFileSummaryViewModel.cs | 26 +++---- .../DataSetFileViewModel.cs | 42 +++++------ .../data-catalogue/__data__/testDataSets.ts | 6 +- .../components/DataSetFileSummary.tsx | 6 +- .../src/services/dataSetFileService.ts | 9 ++- 10 files changed, 122 insertions(+), 79 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileApiViewModel.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerCachingTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerCachingTests.cs index 2ccdfc4a4dc..12b1ff2e460 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerCachingTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerCachingTests.cs @@ -22,7 +22,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.Api.Tests.Controllers; -public class DataSetFilesControllerCachingTests +public static class DataSetFilesControllerCachingTests { public class ListDataSetsTests : CacheServiceTestFixture { @@ -44,6 +44,7 @@ public class ListDataSetsTests : CacheServiceTestFixture { new() { + Id = Guid.NewGuid(), FileId = Guid.NewGuid(), Filename = "Filename.csv", FileSize = "1 Mb", @@ -64,9 +65,14 @@ public class ListDataSetsTests : CacheServiceTestFixture Id = Guid.NewGuid(), Title = "Academic year 2001/02" }, + Api = new DataSetFileApiViewModel + { + Id = Guid.NewGuid(), + Version = "1.0.0" + }, + Meta = new DataSetFileMetaViewModel(), LatestData = true, Published = DateTime.UtcNow, - HasApiDataSet = true } }, 1, 1, 10); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs index 6a573b26345..e9fc1009197 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs @@ -49,12 +49,8 @@ private ListDataSetFilesTests(TestApplicationFactory testApp) : bas { } - public class FilterTests : ListDataSetFilesTests + public class FilterTests(TestApplicationFactory testApp) : ListDataSetFilesTests(testApp) { - public FilterTests(TestApplicationFactory testApp) : base(testApp) - { - } - [Fact] public async Task FilterByReleaseId_Success() { @@ -698,8 +694,12 @@ public async Task FilterByDataSetType_Api_ReturnsOnlyDataSetsWithAssociatedApiDa var releaseVersionFiles = _fixture.DefaultReleaseFile() .WithReleaseVersion(publication.ReleaseVersions[0]) .WithFiles(_fixture.DefaultFile() - .ForIndex(0, s => s.SetPublicApiDataSetId(Guid.NewGuid())) - .ForIndex(1, s => s.SetPublicApiDataSetId(Guid.NewGuid())) + .ForIndex(0, s => s + .SetPublicApiDataSetId(Guid.NewGuid()) + .SetPublicApiDataSetVersion(major: 1, minor: 0)) + .ForIndex(1, s => s + .SetPublicApiDataSetId(Guid.NewGuid()) + .SetPublicApiDataSetVersion(major: 2, minor: 0)) .GenerateList(5)) .GenerateList(); @@ -746,8 +746,12 @@ public async Task FilterByDataSetType_AllOrUnset_ReturnsAllDataSets(DataSetType? var releaseVersionFiles = _fixture.DefaultReleaseFile() .WithReleaseVersion(publication.ReleaseVersions[0]) .WithFiles(_fixture.DefaultFile() - .ForIndex(0, s => s.SetPublicApiDataSetId(Guid.NewGuid())) - .ForIndex(1, s => s.SetPublicApiDataSetId(Guid.NewGuid())) + .ForIndex(0, s => s + .SetPublicApiDataSetId(Guid.NewGuid()) + .SetPublicApiDataSetVersion(major: 1, minor: 1)) + .ForIndex(1, s => s + .SetPublicApiDataSetId(Guid.NewGuid()) + .SetPublicApiDataSetVersion(major: 2, minor: 0)) .GenerateList(5)) .GenerateList(); @@ -817,12 +821,8 @@ public async Task NoFilter_ReturnsAllResultsOrderedByTitleAscending() } } - public class SortByTests : ListDataSetFilesTests + public class SortByTests(TestApplicationFactory testApp) : ListDataSetFilesTests(testApp) { - public SortByTests(TestApplicationFactory testApp) : base(testApp) - { - } - [Theory] [InlineData(SortDirection.Asc)] [InlineData(null)] @@ -1238,12 +1238,9 @@ public async Task SortByRelevance_SortsByRelevanceInDescendingOrderAndIsDescendi } } - public class SupersededPublicationTests : ListDataSetFilesTests + public class SupersededPublicationTests(TestApplicationFactory testApp) + : ListDataSetFilesTests(testApp) { - public SupersededPublicationTests(TestApplicationFactory testApp) : base(testApp) - { - } - [Fact] public async Task PublicationIsSuperseded_DataSetsOfSupersededPublicationsAreExcluded() { @@ -1386,12 +1383,8 @@ public async Task SupersedingPublicationHasNoPublishedReleases_SupersededStatusI } } - public class ValidationTests : ListDataSetFilesTests + public class ValidationTests(TestApplicationFactory testApp) : ListDataSetFilesTests(testApp) { - public ValidationTests(TestApplicationFactory testApp) : base(testApp) - { - } - [Theory] [InlineData(0)] [InlineData(-1)] @@ -1588,12 +1581,8 @@ public async Task SortByRelevanceWithSearchTerm_Success() } } - public class MiscellaneousTests : ListDataSetFilesTests + public class MiscellaneousTests(TestApplicationFactory testApp) : ListDataSetFilesTests(testApp) { - public MiscellaneousTests(TestApplicationFactory testApp) : base(testApp) - { - } - [Fact] public async Task DataSetFileMetaCorrectlyReturned_Success() { @@ -1765,6 +1754,7 @@ private static void AssertResultsForExpectedReleaseFiles( var releaseVersion = releaseFile.ReleaseVersion; var publication = releaseVersion.Publication; var theme = publication.Topic.Theme; + var publicApiDataSetVersion = releaseFile.File.PublicApiDataSetVersion; Assert.Multiple( () => Assert.Equal(releaseFile.FileId, viewModel.FileId), @@ -1782,7 +1772,12 @@ private static void AssertResultsForExpectedReleaseFiles( () => Assert.Equal(releaseVersion.Id == publication.LatestPublishedReleaseVersionId, viewModel.LatestData), () => Assert.Equal(releaseFile.ReleaseVersion.Published!.Value, viewModel.Published), - () => Assert.Equal(releaseFile.File.PublicApiDataSetId.HasValue, viewModel.HasApiDataSet) + () => Assert.Equal(releaseFile.File.PublicApiDataSetId, viewModel.Api?.Id), + () => Assert.Equal( + publicApiDataSetVersion is not null + ? $"{publicApiDataSetVersion.Major}.{publicApiDataSetVersion.Minor}" + : null, + viewModel.Api?.Version) ); }); } @@ -1815,12 +1810,8 @@ private static Mock ContentDbContextMock( } } - public class GetDataSetFileTests : DataSetFilesControllerTests + public class GetDataSetFileTests(TestApplicationFactory testApp) : DataSetFilesControllerTests(testApp) { - public GetDataSetFileTests(TestApplicationFactory testApp) : base(testApp) - { - } - [Fact] public async Task FetchDataSetDetails_Success() { @@ -1834,7 +1825,10 @@ public async Task FetchDataSetDetails_Success() ReleaseFile releaseFile = _fixture.DefaultReleaseFile() .WithReleaseVersion(publication.ReleaseVersions[0]) .WithFile(_fixture.DefaultFile() - .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta())); + .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta()) + .WithPublicApiDataSetId(Guid.NewGuid()) + .WithPublicApiDataSetVersion(major: 1, minor: 0) + ); var client = BuildApp() .AddContentDbTestData(context => @@ -1869,6 +1863,12 @@ public async Task FetchDataSetDetails_Success() Assert.Equal(publication.Slug, viewModel.Release.Publication.Slug); Assert.Equal(publication.Topic.Theme.Title, viewModel.Release.Publication.ThemeTitle); + Assert.NotNull(viewModel.Api); + Assert.Equal(file.PublicApiDataSetId, viewModel.Api.Id); + Assert.Equal( + $"{file.PublicApiDataSetVersion!.Major}.{file.PublicApiDataSetVersion.Minor}", + viewModel.Api.Version); + var dataSetFileMeta = file.DataSetFileMeta; viewModel.Meta.GeographicLevels diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs index 7e7b2148d28..974a6215366 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs @@ -48,5 +48,9 @@ public class File : ICreatedTimestamp public User? CreatedBy { get; set; } public Guid? CreatedById { get; set; } + + public string? PublicApiVersionString => PublicApiDataSetVersion is not null + ? $"{PublicApiDataSetVersion.Major}.{PublicApiDataSetVersion.Minor}" + : null; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs index 1a55f399b9d..1f0168a5b37 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs @@ -22,6 +22,7 @@ using Microsoft.EntityFrameworkCore; using static GovUk.Education.ExploreEducationStatistics.Common.Model.SortDirection; using static GovUk.Education.ExploreEducationStatistics.Content.Requests.DataSetsListRequestSortBy; +using File = GovUk.Education.ExploreEducationStatistics.Content.Model.File; using ReleaseVersion = GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion; namespace GovUk.Education.ExploreEducationStatistics.Content.Services; @@ -118,7 +119,7 @@ private static Expression, DataSetFileSumm LatestData = result.Value.ReleaseVersionId == result.Value.ReleaseVersion.Publication.LatestPublishedReleaseVersionId, Published = result.Value.ReleaseVersion.Published!.Value, - HasApiDataSet = result.Value.File.PublicApiDataSetId.HasValue, + Api = BuildDataSetFileApiViewModel(result.Value.File), Meta = BuildDataSetFileMetaViewModel( result.Value.File.DataSetFileMeta, result.Value.FilterSequence, @@ -191,6 +192,7 @@ public async Task> GetDataSetFile( releaseFile.File.DataSetFileMeta, releaseFile.FilterSequence, releaseFile.IndicatorSequence), + Api = BuildDataSetFileApiViewModel(releaseFile.File) }; } @@ -245,6 +247,20 @@ private static List GetOrderedIndicators(List metaIndicat return indicators.Select(i => i.Label).ToList(); } + + private static DataSetFileApiViewModel? BuildDataSetFileApiViewModel(File file) + { + if (file.PublicApiDataSetId is null || file.PublicApiVersionString is null) + { + return null; + } + + return new DataSetFileApiViewModel + { + Id = file.PublicApiDataSetId.Value, + Version = file.PublicApiVersionString, + }; + } } internal static class FreeTextReleaseFileValueResultQueryableExtensions diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileApiViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileApiViewModel.cs new file mode 100644 index 00000000000..80cde64cc87 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileApiViewModel.cs @@ -0,0 +1,8 @@ +namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; + +public record DataSetFileApiViewModel +{ + public required Guid Id { get; init; } + + public required string Version { get; init; } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileSummaryViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileSummaryViewModel.cs index 14046676e44..438f1e5082d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileSummaryViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileSummaryViewModel.cs @@ -4,31 +4,31 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; public record DataSetFileSummaryViewModel { - public Guid Id { get; init; } + public required Guid Id { get; init; } - public Guid FileId { get; init; } + public required Guid FileId { get; init; } - public string Filename { get; init; } = string.Empty; + public required string Filename { get; init; } - public string FileSize { get; init; } = string.Empty; + public required string FileSize { get; init; } public string FileExtension => Path.GetExtension(Filename).TrimStart('.'); - public string Title { get; init; } = string.Empty; + public required string Title { get; init; } - public string Content { get; init; } = string.Empty; + public required string Content { get; init; } - public IdTitleViewModel Theme { get; init; } = null!; + public required IdTitleViewModel Theme { get; init; } - public IdTitleViewModel Publication { get; init; } = null!; + public required IdTitleViewModel Publication { get; init; } - public IdTitleViewModel Release { get; init; } = null!; + public required IdTitleViewModel Release { get; init; } - public bool LatestData { get; init; } + public required bool LatestData { get; init; } - public DateTime Published { get; init; } + public required DateTime Published { get; init; } - public bool HasApiDataSet { get; init; } + public required DataSetFileMetaViewModel Meta { get; init; } - public DataSetFileMetaViewModel Meta { get; init; } = null!; + public DataSetFileApiViewModel? Api { get; init; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs index 2f23a3a710b..21ae5414df0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs @@ -6,53 +6,55 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; public record DataSetFileViewModel { - public Guid Id { get; init; } + public required Guid Id { get; init; } - public string Title { get; init; } = string.Empty; + public required string Title { get; init; } - public string Summary { get; init; } = string.Empty; + public required string Summary { get; init; } - public DataSetFileFileViewModel File { get; init; } = null!; + public required DataSetFileFileViewModel File { get; init; } - public DataSetFileReleaseViewModel Release { get; init; } = null!; + public required DataSetFileReleaseViewModel Release { get; init; } - public DataSetFileMetaViewModel Meta { get; init; } = null!; + public required DataSetFileMetaViewModel Meta { get; init; } + + public DataSetFileApiViewModel? Api { get; set; } } public record DataSetFilePublicationViewModel { - public Guid Id { get; init; } + public required Guid Id { get; init; } - public string Title { get; init; } = string.Empty; + public required string Title { get; init; } - public string Slug { get; init; } = string.Empty; + public required string Slug { get; init; } - public string ThemeTitle { get; init; } = string.Empty; + public required string ThemeTitle { get; init; } } public record DataSetFileReleaseViewModel { - public Guid Id { get; init; } + public required Guid Id { get; init; } - public string Title { get; init; } = string.Empty; + public required string Title { get; init; } - public string Slug { get; init; } = string.Empty; + public required string Slug { get; init; } [JsonConverter(typeof(StringEnumConverter))] - public ReleaseType Type { get; init; } + public required ReleaseType Type { get; init; } - public bool IsLatestPublishedRelease { get; init; } + public required bool IsLatestPublishedRelease { get; init; } - public DateTime Published { get; init; } + public required DateTime Published { get; init; } - public DataSetFilePublicationViewModel Publication { get; init; } = null!; + public required DataSetFilePublicationViewModel Publication { get; init; } } public record DataSetFileFileViewModel { - public Guid Id { get; init; } + public required Guid Id { get; init; } - public string Name { get; init; } = string.Empty; + public required string Name { get; init; } - public string Size { get; init; } = string.Empty; + public required string Size { get; init; } } diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts index 647fe1830ae..160bc110803 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts @@ -5,7 +5,10 @@ import { export const testDataSetFileSummaries: DataSetFileSummary[] = [ { - hasApiDataSet: true, + api: { + id: 'api-data-set-id-1', + version: '1.0', + }, id: 'datasetfile-id-1', fileExtension: 'csv', fileId: 'file-id-1', @@ -39,7 +42,6 @@ export const testDataSetFileSummaries: DataSetFileSummary[] = [ title: 'Data set 1', }, { - hasApiDataSet: false, id: 'datasetfile-id-2', fileExtension: 'csv', fileId: 'file-id-2', diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileSummary.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileSummary.tsx index 5390ee19144..91454f64862 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileSummary.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileSummary.tsx @@ -48,7 +48,7 @@ export default function DataSetFileSummary({ geographicLevels = [], indicators = [], }, - hasApiDataSet, + api, latestData, publication, published, @@ -111,7 +111,7 @@ export default function DataSetFileSummary({ compact noBorder > - {(showLatestDataTag || hasApiDataSet) && ( + {(showLatestDataTag || api) && ( {showLatestDataTag && ( @@ -121,7 +121,7 @@ export default function DataSetFileSummary({ : 'This is not the latest data'} )} - {hasApiDataSet && Available by API} + {api && Available by API} )} diff --git a/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts b/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts index c0049faafb9..cc75be15edf 100644 --- a/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts +++ b/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts @@ -8,7 +8,6 @@ export interface DataSetFile { title: string; summary: string; file: { id: string; name: string; size: string }; - hasApiDataSet?: boolean; release: { id: string; isLatestPublishedRelease: boolean; @@ -23,6 +22,7 @@ export interface DataSetFile { title: string; type: ReleaseType; }; + api?: DataSetFileApi; meta: { geographicLevels: string[]; timePeriod: { @@ -41,7 +41,6 @@ export interface DataSetFileSummary { filename: string; fileSize: string; fileExtension: string; - hasApiDataSet?: boolean; title: string; content: string; theme: { @@ -58,6 +57,7 @@ export interface DataSetFileSummary { }; latestData: boolean; published: Date; + api?: DataSetFileApi; meta: { geographicLevels: string[]; timePeriod: { @@ -70,6 +70,11 @@ export interface DataSetFileSummary { }; } +export interface DataSetFileApi { + id: string; + version: string; +} + export const dataSetFileSortOptions = [ 'newest', 'oldest', From 5da8bcff40743f596215e8fabfdf243f9697686d Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Mon, 13 May 2024 23:37:47 +0100 Subject: [PATCH 13/66] EES-5139 Rename `DataSetFilesController.GetDataSetFile` --- .../Controllers/DataSetFilesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs index f27c40f03f3..51205e0c927 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs @@ -50,7 +50,7 @@ public async Task> GetDataSet( + public async Task> GetDataSetFile( Guid dataSetFileId) { return await _dataSetFileService From 01499169158d112e5317f179507ca2aa08de3990 Mon Sep 17 00:00:00 2001 From: jack-hive <148866614+jack-hive@users.noreply.github.com> Date: Tue, 14 May 2024 15:08:59 +0100 Subject: [PATCH 14/66] EES-4910 Create an endpoint for Creating an API Dataset (#4823) * EES-4910 Adding new Create endpoint in the admin for creating a new API data set * EES-4910 Adding integration tests * EES-4910 Refactoring validation into a fluent validation layer * EES-4910 Fixing tests * EES-4910 Changes as per PR * EES-4910 Fixing build * EES-4910 Changing use of `It.IsAny()` to `Guid.NewGuid()` * EES-4910 Removing unnecessary line * EES-4910 Changes as per PR * EES-4910 Renaming `CreateInitialDataSetVersion` to `CreateInitialDataSet` * EES-4910 Renaming `CreateInitialDataSet` to `CreateDataSet` * EES-4910 Renaming a variable --- .../Public.Data/DataSetsControllerTests.cs | 184 ++++++++++++++++++ ...loreEducationStatistics.Admin.Tests.csproj | 1 + .../Public.Data/ProcessorClientTests.cs | 125 ++++++++++++ .../appsettings.IntegrationTest.json | 3 + .../Api/Public.Data/DataSetsController.cs | 13 ++ ...on.ExploreEducationStatistics.Admin.csproj | 2 + .../Properties/AssemblyInfo.cs | 4 + .../DataSetVersionCreateRequest.cs | 28 +++ .../Interfaces/Public.Data/IDataSetService.cs | 4 + .../Public.Data/IDataSetVersionService.cs | 6 + .../Public.Data/IProcessorClient.cs | 16 ++ .../Services/Public.Data/DataSetService.cs | 115 ++++++----- .../Public.Data/DataSetVersionService.cs | 11 ++ .../Services/Public.Data/ProcessorClient.cs | 62 ++++++ .../Services/Public.Data/ReleaseService.cs | 2 +- .../Settings/PublicDataProcessorOptions.cs | 9 + .../Startup.cs | 21 +- .../Validators/ValidationMessages.cs | 12 ++ .../appsettings.Development.json | 5 +- ...reateInitialDataSetVersionFunctionTests.cs | 1 + ...eInitialDataSetVersionResponseViewModel.cs | 1 + .../CreateInitialDataSetVersionFunction.cs | 8 +- .../Services/DataSetService.cs | 4 +- .../Services/IDataSetService.cs | 2 +- 24 files changed, 576 insertions(+), 63 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Public.Data/ProcessorClientTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Properties/AssemblyInfo.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/Public.Data/DataSetVersionCreateRequest.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IProcessorClient.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ProcessorClient.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Settings/PublicDataProcessorOptions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationMessages.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs index 3147f92942c..8d0a4d4083d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Public.Data/DataSetsControllerTests.cs @@ -3,12 +3,19 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; +using System.Security.Claims; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Admin.Requests.Public.Data; using GovUk.Education.ExploreEducationStatistics.Admin.Security; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Public.Data; using GovUk.Education.ExploreEducationStatistics.Admin.Tests.Fixture; using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Public.Data; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; @@ -16,8 +23,12 @@ using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model.Tests.Fixtures; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.WebUtilities; +using Moq; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using ValidationMessages = GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationMessages; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Controllers.Api.Public.Data; @@ -896,4 +907,177 @@ private async Task GetDataSet(Guid dataSetId, HttpClient? c return await client.GetAsync(uri); } } + + public class CreateDataSetTests(TestApplicationFactory testApp) : DataSetsControllerTests(testApp) + { + public static IEnumerable AllDataSetVersionStatuses => + EnumUtil.GetEnums() + .Select(e => new object[] { e }); + + [Fact] + public async Task Success() + { + Publication publication = DataFixture + .DefaultPublication() + .WithReleases( + DataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true) + .Generate(1) + ); + + var draftReleaseVersion = publication.ReleaseVersions.Single(); + + ReleaseFile draftReleaseFile = DataFixture + .DefaultReleaseFile() + .WithFile(DataFixture.DefaultFile()) + .WithReleaseVersion(draftReleaseVersion); + + await TestApp.AddTestData(context => + { + context.Publications.Add(publication); + context.ReleaseFiles.Add(draftReleaseFile); + }); + + DataSet? dataSet = null; + DataSetVersion? dataSetVersion = null; + + var processorClient = new Mock(); + processorClient + .Setup(c => c.CreateInitialDataSetVersion( + draftReleaseFile.Id, + It.IsAny())) + .Returns(async () => + { + dataSet = DataFixture + .DefaultDataSet() + .WithStatusPublished() + .WithPublicationId(publication.Id); + + await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); + + dataSetVersion = DataFixture + .DefaultDataSetVersion() + .WithStatusProcessing() + .WithReleaseFileId(draftReleaseFile.Id) + .WithDataSet(dataSet) + .FinishWith(dsv => dsv.DataSet.LatestDraftVersion = dsv); + + await TestApp.AddTestData(context => + { + context.DataSetVersions.Add(dataSetVersion); + context.DataSets.Update(dataSet); + }); + + return new CreateInitialDataSetVersionResponseViewModel + { + DataSetId = dataSet!.Id, + DataSetVersionId = dataSetVersion!.Id, + InstanceId = Guid.NewGuid() + }; + }); + + var client = BuildApp(processorClient.Object).CreateClient(); + + var response = await CreateDataSetVersion(draftReleaseFile.Id, client); + + var content = response.AssertOk(); + + Assert.NotNull(content); + Assert.Equal(dataSet!.Id, content.Id); + Assert.Equal(dataSet.Title, content.Title); + Assert.Equal(dataSet.Status, content.Status); + Assert.Equal(dataSet.Summary, content.Summary); + Assert.Null(dataSet.LatestLiveVersion); + Assert.Equal(dataSetVersion!.Id, content.DraftVersion!.Id); + Assert.Equal(dataSetVersion.Version, content.DraftVersion!.Version); + Assert.Equal(dataSetVersion.Status, content.DraftVersion!.Status); + Assert.Equal(dataSetVersion.VersionType, content.DraftVersion!.Type); + Assert.Equal(draftReleaseFile.File.DataSetFileId!.Value, content.DraftVersion!.DataSetFileId); + Assert.Equal(draftReleaseVersion.Id, content.DraftVersion!.ReleaseVersion.Id); + Assert.Equal(draftReleaseVersion.Title, content.DraftVersion!.ReleaseVersion.Title); + Assert.Null(content.DraftVersion!.GeographicLevels); + Assert.Null(content.DraftVersion!.TimePeriods); + Assert.Null(content.DraftVersion!.Filters); + Assert.Null(content.DraftVersion!.Indicators); + } + + [Fact] + public async Task NotBauUser_Returns403() + { + var client = BuildApp(user: AuthenticatedUser()).CreateClient(); + + var response = await CreateDataSetVersion(Guid.NewGuid(), client); + + response.AssertForbidden(); + } + + [Theory] + [MemberData(nameof(AllDataSetVersionStatuses))] + public async Task DataSetVersionExistsForReleaseFile_Returns400(DataSetVersionStatus dataSetVersionStatus) + { + var releaseFileId = Guid.NewGuid(); + + DataSet dataSet = DataFixture + .DefaultDataSet() + .WithStatusPublished(); + + await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); + + DataSetVersion dataSetVersion = DataFixture + .DefaultDataSetVersion() + .WithStatus(dataSetVersionStatus) + .WithReleaseFileId(releaseFileId) + .WithDataSet(dataSet); + + await TestApp.AddTestData(context => + { + context.DataSetVersions.Add(dataSetVersion); + context.DataSets.Update(dataSet); + }); + + var response = await CreateDataSetVersion(releaseFileId); + + var validationProblem = response.AssertValidationProblem(); + + var error = validationProblem.AssertHasError("releaseFileId", ValidationMessages.FileHasApiDataSetVersion.Code); + + var errorDetail = error.GetDetail>(); + + Assert.Equal(releaseFileId.ToString(), errorDetail["value"].GetString()); + } + + [Fact] + public async Task ReleaseFileIdIsEmpty_Returns400() + { + var response = await CreateDataSetVersion(Guid.Empty); + + var validationProblem = response.AssertValidationProblem(); + + var error = validationProblem.AssertHasNotEmptyError("releaseFileId"); + } + + private WebApplicationFactory BuildApp( + IProcessorClient? processorClient = null, + ClaimsPrincipal? user = null) + { + return TestApp.ConfigureServices( + services => { services.ReplaceService(processorClient ?? Mock.Of()); } + ) + .SetUser(user ?? BauUser()); + } + + private async Task CreateDataSetVersion( + Guid releaseFileId, + HttpClient? client = null) + { + client ??= BuildApp().CreateClient(); + + var request = new DataSetVersionCreateRequest + { + ReleaseFileId = releaseFileId + }; + + return await client.PostAsJsonAsync(BaseUrl, request); + } + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj index d268f329c94..623639cfc71 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Public.Data/ProcessorClientTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Public.Data/ProcessorClientTests.cs new file mode 100644 index 00000000000..041db153bb8 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Public.Data/ProcessorClientTests.cs @@ -0,0 +1,125 @@ +#nullable enable +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Public.Data; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using RichardSzalay.MockHttp; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.Public.Data; + +public class ProcessorClientTests +{ + private static readonly Uri BaseUri = new("http://localhost"); + private readonly MockHttpMessageHandler _mockHttp; + private readonly ProcessorClient _processorClient; + + public ProcessorClientTests() + { + _mockHttp = new MockHttpMessageHandler(); + var client = _mockHttp.ToHttpClient(); + client.BaseAddress = BaseUri; + _processorClient = new ProcessorClient(Mock.Of>(), client); + } + + public class CreateInitialDataSetVersionTests : ProcessorClientTests + { + private static readonly Uri Uri = new(BaseUri, "api/CreateInitialDataSetVersion"); + + [Fact] + public async Task HttpClientSuccess() + { + var responseBody = new CreateInitialDataSetVersionResponseViewModel + { + DataSetId = Guid.NewGuid(), + DataSetVersionId = Guid.NewGuid(), + InstanceId = Guid.NewGuid() + }; + + _mockHttp.Expect(HttpMethod.Post, Uri.AbsoluteUri) + .Respond(HttpStatusCode.Accepted, "application/json", JsonConvert.SerializeObject(responseBody)); + + var response = await _processorClient.CreateInitialDataSetVersion(releaseFileId: Guid.NewGuid()); + + _mockHttp.VerifyNoOutstandingExpectation(); + + var right = response.AssertRight(); + Assert.Equal(responseBody.DataSetId, right.DataSetId); + Assert.Equal(responseBody.DataSetVersionId, right.DataSetVersionId); + Assert.Equal(responseBody.InstanceId, right.InstanceId); + } + + [Fact] + public async Task HttpClientBadRequest_ReturnsBadRequest() + { + _mockHttp.Expect(HttpMethod.Post, Uri.AbsoluteUri) + .Respond( + HttpStatusCode.BadRequest, + JsonContent.Create(new ValidationProblemViewModel + { + Errors = new ErrorViewModel[] + { + new() { + Code = Errors.Error1.ToString() + } + } + })); + + var response = await _processorClient.CreateInitialDataSetVersion(releaseFileId: Guid.NewGuid()); + + _mockHttp.VerifyNoOutstandingExpectation(); + + var left = response.AssertLeft(); + left.AssertValidationProblem(Errors.Error1); + } + + [Fact] + public async Task HttpClientNotFound_ReturnsNotFound() + { + _mockHttp.Expect(HttpMethod.Post, Uri.AbsoluteUri) + .Respond(HttpStatusCode.NotFound); + + var response = await _processorClient.CreateInitialDataSetVersion(releaseFileId: Guid.NewGuid()); + + _mockHttp.VerifyNoOutstandingExpectation(); + + var left = response.AssertLeft(); + left.AssertNotFoundResult(); + } + + [Theory] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.BadGateway)] + [InlineData(HttpStatusCode.Unauthorized)] + [InlineData(HttpStatusCode.Conflict)] + [InlineData(HttpStatusCode.Forbidden)] + [InlineData(HttpStatusCode.Gone)] + [InlineData(HttpStatusCode.NotAcceptable)] + public async Task HttpClientFailureStatusCode_ThrowsException( + HttpStatusCode responseStatusCode) + { + _mockHttp.Expect(HttpMethod.Post, Uri.AbsoluteUri) + .Respond(responseStatusCode); + + await Assert.ThrowsAsync(async () => + { + await _processorClient.CreateInitialDataSetVersion(releaseFileId: Guid.NewGuid()); + }); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + private enum Errors + { + Error1, + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTest.json b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTest.json index 015f0b633ea..6d88a931a0b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTest.json +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTest.json @@ -35,5 +35,8 @@ "PublishReleaseContentCronSchedule": "0 1-59/2 * * * *" }, "enableThemeDeletion": true, + "PublicDataProcessor": { + "Url": "http://localhost:7074" + }, "PublicDataDbExists": true } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs index f4d7986a385..745ffaac1f2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Public.Data/DataSetsController.cs @@ -44,4 +44,17 @@ public async Task> GetDataSet( cancellationToken: cancellationToken) .HandleFailuresOrOk(); } + + [HttpPost] + [Produces("application/json")] + public async Task> CreateDataSet( + [FromBody] DataSetVersionCreateRequest dataSetVersionCreateRequest, + CancellationToken cancellationToken) + { + return await dataSetService + .CreateDataSet( + releaseFileId: dataSetVersionCreateRequest.ReleaseFileId, + cancellationToken: cancellationToken) + .HandleFailuresOrOk(); + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/GovUk.Education.ExploreEducationStatistics.Admin.csproj b/src/GovUk.Education.ExploreEducationStatistics.Admin/GovUk.Education.ExploreEducationStatistics.Admin.csproj index 8417d02f821..ed31c63452f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/GovUk.Education.ExploreEducationStatistics.Admin.csproj +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/GovUk.Education.ExploreEducationStatistics.Admin.csproj @@ -117,6 +117,8 @@ + + diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Properties/AssemblyInfo.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..5677ed572fc --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("GovUk.Education.ExploreEducationStatistics.Admin.Tests"), + InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/Public.Data/DataSetVersionCreateRequest.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/Public.Data/DataSetVersionCreateRequest.cs new file mode 100644 index 00000000000..fba1bdd9739 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/Public.Data/DataSetVersionCreateRequest.cs @@ -0,0 +1,28 @@ +#nullable enable +using FluentValidation; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Public.Data; +using GovUk.Education.ExploreEducationStatistics.Admin.Validators; +using System; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Requests.Public.Data; + +public record DataSetVersionCreateRequest +{ + public required Guid ReleaseFileId { get; init; } + + public class Validator : AbstractValidator + { + public Validator(IDataSetVersionService dataSetVersionService) + { + RuleFor(request => request.ReleaseFileId) + .NotEmpty() + .MustAsync(async (releaseFileId, cancellationToken) => + await dataSetVersionService.FileHasVersion( + releaseFileId: releaseFileId, + cancellationToken: cancellationToken) + is false) + .WithErrorCode(ValidationMessages.FileHasApiDataSetVersion.Code) + .WithMessage(ValidationMessages.FileHasApiDataSetVersion.Message); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs index 17309018f04..e154dc48a5b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetService.cs @@ -20,4 +20,8 @@ Task>> List Task> GetDataSet( Guid dataSetId, CancellationToken cancellationToken = default); + + Task> CreateDataSet( + Guid releaseFileId, + CancellationToken cancellationToken = default); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetVersionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetVersionService.cs index 3fa9dff89b5..516994c5b85 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetVersionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IDataSetVersionService.cs @@ -1,5 +1,7 @@ +#nullable enable using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Public.Data; @@ -8,4 +10,8 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.P public interface IDataSetVersionService { Task> GetStatusesForReleaseVersion(Guid releaseVersionId); + + Task FileHasVersion( + Guid releaseFileId, + CancellationToken cancellationToken = default); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IProcessorClient.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IProcessorClient.cs new file mode 100644 index 00000000000..ef08b24186e --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Public.Data/IProcessorClient.cs @@ -0,0 +1,16 @@ +#nullable enable +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Public.Data; + +public interface IProcessorClient +{ + Task> CreateInitialDataSetVersion( + Guid releaseFileId, + CancellationToken cancellationToken = default); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs index f8866a91d5d..26e5f6fcf0c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetService.cs @@ -20,9 +20,10 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Public.Data; -public class DataSetService( +internal class DataSetService( ContentDbContext contentDbContext, PublicDataDbContext publicDataDbContext, + IProcessorClient processorClient, IUserService userService) : IDataSetService { @@ -65,17 +66,25 @@ public async Task> GetDataSet( Guid dataSetId, CancellationToken cancellationToken = default) { - return await CheckDataSetExists(dataSetId, cancellationToken) + return await QueryDataSet(dataSetId) + .SingleOrNotFoundAsync(cancellationToken) .OnSuccessDo(dataSet => CheckPublicationExists(dataSet.PublicationId, cancellationToken) .OnSuccess(userService.CheckCanViewPublication) ) - .OnSuccess(async dataSet => - { - var releaseFilesByDataSetVersionId = - await GetReleaseFilesByDataSetVersionId(dataSet, cancellationToken); + .OnSuccess(async dataSet => await MapDataSet(dataSet, cancellationToken)); + } - return MapDataSet(dataSet, releaseFilesByDataSetVersionId); - }); + public async Task> CreateDataSet( + Guid releaseFileId, + CancellationToken cancellationToken = default) + { + return await userService.CheckIsBauUser() + .OnSuccess(async _ => await processorClient.CreateInitialDataSetVersion( + releaseFileId: releaseFileId, + cancellationToken: cancellationToken)) + .OnSuccess(async processorResponse => await QueryDataSet(processorResponse.DataSetId) + .SingleAsync(cancellationToken)) + .OnSuccess(async dataSet => await MapDataSet(dataSet, cancellationToken)); } private static DataSetSummaryViewModel MapDataSetSummary(DataSet dataSet) @@ -119,10 +128,11 @@ private static DataSetSummaryViewModel MapDataSetSummary(DataSet dataSet) : null; } - private static DataSetViewModel MapDataSet( - DataSet dataSet, - IReadOnlyDictionary releaseFilesByDataSetVersionId) + private async Task MapDataSet(DataSet dataSet, CancellationToken cancellationToken) { + var releaseFilesByDataSetVersionId = + await GetReleaseFilesByDataSetVersionId(dataSet, cancellationToken); + var draftVersion = dataSet.LatestDraftVersion is null ? null : MapDraftVersion( @@ -149,6 +159,41 @@ private static DataSetViewModel MapDataSet( }; } + private async Task> GetReleaseFilesByDataSetVersionId( + DataSet dataSet, + CancellationToken cancellationToken) + { + if (dataSet.LatestDraftVersion is null && dataSet.LatestLiveVersion is null) + { + return new Dictionary(); + } + + var dataSetVersionIdsByReleaseFileId = new Dictionary(); + + if (dataSet.LatestDraftVersion is not null) + { + dataSetVersionIdsByReleaseFileId.Add( + dataSet.LatestDraftVersion.ReleaseFileId, + dataSet.LatestDraftVersionId!.Value + ); + } + + if (dataSet.LatestLiveVersion is not null) + { + dataSetVersionIdsByReleaseFileId.Add( + dataSet.LatestLiveVersion.ReleaseFileId, + dataSet.LatestLiveVersionId!.Value + ); + } + + return await contentDbContext.ReleaseFiles + .AsNoTracking() + .Where(rf => dataSetVersionIdsByReleaseFileId.Keys.Contains(rf.Id)) + .Include(rf => rf.ReleaseVersion) + .Include(rf => rf.File) + .ToDictionaryAsync(rf => dataSetVersionIdsByReleaseFileId[rf.Id], cancellationToken); + } + private static DataSetVersionViewModel MapDraftVersion( DataSetVersion dataSetVersion, ReleaseFile releaseFile) @@ -172,7 +217,7 @@ private static DataSetVersionViewModel MapDraftVersion( Indicators = dataSetVersion.MetaSummary?.Indicators ?? null, }; } - + private static DataSetLiveVersionViewModel MapLiveVersion( DataSetVersion dataSetVersion, ReleaseFile releaseFile) @@ -213,50 +258,12 @@ private async Task> CheckPublicationExists(Gui .FirstOrNotFoundAsync(p => p.Id == publicationId, cancellationToken: cancellationToken); } - private async Task> CheckDataSetExists( - Guid dataSetId, - CancellationToken cancellationToken) + private IQueryable QueryDataSet(Guid dataSetId) { - return await publicDataDbContext.DataSets + return publicDataDbContext.DataSets .AsNoTracking() - .Include(ds => ds.LatestDraftVersion) - .Include(ds => ds.LatestLiveVersion) .Where(ds => ds.Id == dataSetId) - .SingleOrNotFoundAsync(cancellationToken); - } - - private async Task> GetReleaseFilesByDataSetVersionId( - DataSet dataSet, - CancellationToken cancellationToken) - { - if (dataSet.LatestDraftVersion is null && dataSet.LatestLiveVersion is null) - { - return new Dictionary(); - } - - var dataSetVersionIdsByReleaseFileId = new Dictionary(); - - if (dataSet.LatestDraftVersion is not null) - { - dataSetVersionIdsByReleaseFileId.Add( - dataSet.LatestDraftVersion.ReleaseFileId, - dataSet.LatestDraftVersionId!.Value - ); - } - - if (dataSet.LatestLiveVersion is not null) - { - dataSetVersionIdsByReleaseFileId.Add( - dataSet.LatestLiveVersion.ReleaseFileId, - dataSet.LatestLiveVersionId!.Value - ); - } - - return await contentDbContext.ReleaseFiles - .AsNoTracking() - .Where(rf => dataSetVersionIdsByReleaseFileId.Keys.Contains(rf.Id)) - .Include(rf => rf.ReleaseVersion) - .Include(rf => rf.File) - .ToDictionaryAsync(rf => dataSetVersionIdsByReleaseFileId[rf.Id], cancellationToken); + .Include(ds => ds.LatestDraftVersion) + .Include(ds => ds.LatestLiveVersion); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetVersionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetVersionService.cs index a8a4e833997..06ca0aabb91 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetVersionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/DataSetVersionService.cs @@ -1,6 +1,8 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Public.Data; using GovUk.Education.ExploreEducationStatistics.Common.Model; @@ -35,6 +37,15 @@ public async Task> GetStatusesForReleaseVersio ) .ToListAsync(); } + + public async Task FileHasVersion( + Guid releaseFileId, + CancellationToken cancellationToken = default) + { + return await publicDataDbContext.DataSetVersions + .AsNoTracking() + .AnyAsync(dsv => dsv.ReleaseFileId == releaseFileId, cancellationToken); + } } public record DataSetVersionStatusSummary(Guid Id, string Title, DataSetVersionStatus Status); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ProcessorClient.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ProcessorClient.cs new file mode 100644 index 00000000000..b7b58fe6d6b --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ProcessorClient.cs @@ -0,0 +1,62 @@ +#nullable enable +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Public.Data; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.Requests; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Public.Data; + +internal class ProcessorClient(ILogger logger, HttpClient httpClient) : IProcessorClient +{ + public async Task> CreateInitialDataSetVersion( + Guid releaseFileId, + CancellationToken cancellationToken = default) + { + var request = new InitialDataSetVersionCreateRequest + { + ReleaseFileId = releaseFileId, + }; + + var response = await httpClient + .PostAsJsonAsync("api/CreateInitialDataSetVersion", request, cancellationToken: cancellationToken); + + if (!response.IsSuccessStatusCode) + { + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest: + return new BadRequestObjectResult( + await response.Content + .ReadFromJsonAsync(cancellationToken: cancellationToken) + ); + case HttpStatusCode.NotFound: + return new NotFoundResult(); + default: + var message = await response.Content.ReadAsStringAsync(cancellationToken); + logger.LogError($""" + Failed to create data set version with status code: {response.StatusCode}. Message: + {message} + """); + response.EnsureSuccessStatusCode(); + break; + } + } + + var content = await response.Content.ReadFromJsonAsync( + cancellationToken: cancellationToken + ); + + return content + ?? throw new Exception("Could not deserialize the response from the Public Data Processor."); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs index ba4b1db2fd8..293c5607650 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Public.Data/ReleaseService.cs @@ -17,7 +17,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.Public.Data; -public class ReleaseService( +internal class ReleaseService( ContentDbContext contentDbContext, IUserService userService) : IReleaseService diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Settings/PublicDataProcessorOptions.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Settings/PublicDataProcessorOptions.cs new file mode 100644 index 00000000000..fe273855382 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Settings/PublicDataProcessorOptions.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace GovUk.Education.ExploreEducationStatistics.Admin.Settings; + +internal class PublicDataProcessorOptions +{ + public static readonly string Section = "PublicDataProcessor"; + + public string Url { get; init; } = string.Empty; +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 4b46ed667b5..580848811cb 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -22,6 +22,7 @@ using GovUk.Education.ExploreEducationStatistics.Admin.Services.ManageContent; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Methodologies; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Public.Data; +using GovUk.Education.ExploreEducationStatistics.Admin.Settings; using GovUk.Education.ExploreEducationStatistics.Common.Cache; using GovUk.Education.ExploreEducationStatistics.Common.Cancellation; using GovUk.Education.ExploreEducationStatistics.Common.Config; @@ -65,6 +66,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; using Newtonsoft.Json; @@ -102,7 +104,10 @@ using ReleaseFileService = GovUk.Education.ExploreEducationStatistics.Admin.Services.ReleaseFileService; using ReleaseService = GovUk.Education.ExploreEducationStatistics.Admin.Services.ReleaseService; using ReleaseVersionRepository = GovUk.Education.ExploreEducationStatistics.Admin.Services.ReleaseVersionRepository; +using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode; using ThemeService = GovUk.Education.ExploreEducationStatistics.Admin.Services.ThemeService; +using HeaderNames = Microsoft.Net.Http.Headers.HeaderNames; +using System.Threading; namespace GovUk.Education.ExploreEducationStatistics.Admin { @@ -354,6 +359,7 @@ public virtual void ConfigureServices(IServiceCollection services) * Configuration options */ + services.Configure(configuration.GetRequiredSection(PublicDataProcessorOptions.Section)); services.Configure(configuration); services.Configure(configuration.GetRequiredSection(LocationsOptions.Locations)); services.Configure( @@ -461,6 +467,13 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); + services.AddHttpClient((provider, httpClient) => + { + var options = provider.GetRequiredService>(); + httpClient.BaseAddress = new Uri(options.Value.Url); + httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "EES Admin"); + }); + if (publicDataDbExists) { services.AddTransient(); @@ -473,6 +486,7 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddTransient(provider => new DataSetService(provider.GetRequiredService(), provider.GetService(), + provider.GetRequiredService(), provider.GetRequiredService())); services.AddTransient(); @@ -762,6 +776,11 @@ internal class NoOpDataSetVersionService : IDataSetVersionService public Task> GetStatusesForReleaseVersion(Guid releaseVersionId) { return Task.FromResult(new List()); - } + } + + public Task FileHasVersion(Guid releaseFileId, CancellationToken cancellationToken = default) + { + return Task.FromResult(false); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationMessages.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationMessages.cs new file mode 100644 index 00000000000..f665a342fca --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationMessages.cs @@ -0,0 +1,12 @@ +#nullable enable +using GovUk.Education.ExploreEducationStatistics.Common.Model; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Validators; + +public static class ValidationMessages +{ + public static readonly LocalizableMessage FileHasApiDataSetVersion = new( + Code: "FileHasApiDataSetVersion", + Message: "The file has already been used for an API data set version." + ); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/appsettings.Development.json b/src/GovUk.Education.ExploreEducationStatistics.Admin/appsettings.Development.json index 46b50efc808..9591e664821 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/appsettings.Development.json +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/appsettings.Development.json @@ -39,5 +39,8 @@ "PublishReleasesCronSchedule": "0 0-58/2 * * * *", "PublishReleaseContentCronSchedule": "0 1-59/2 * * * *" }, - "PublicDataDbExists": false + "PublicDataDbExists": false, + "PublicDataProcessor": { + "Url": "http://localhost:7074" + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.Tests/Functions/CreateInitialDataSetVersionFunctionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.Tests/Functions/CreateInitialDataSetVersionFunctionTests.cs index 9058943281c..6cd05a7f5fa 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.Tests/Functions/CreateInitialDataSetVersionFunctionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.Tests/Functions/CreateInitialDataSetVersionFunctionTests.cs @@ -110,6 +110,7 @@ await AddTestData(context => Assert.Equal(DataSetVersionImportStage.Pending, dataSetVersionImport.Stage); // Assert the response view model values match the created data set version and import + Assert.Equal(dataSet.Id, responseViewModel.DataSetId); Assert.Equal(dataSetVersion.Id, responseViewModel.DataSetVersionId); Assert.Equal(dataSetVersionImport.InstanceId, responseViewModel.InstanceId); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels/CreateInitialDataSetVersionResponseViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels/CreateInitialDataSetVersionResponseViewModel.cs index 1b733726263..b8e35ac289e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels/CreateInitialDataSetVersionResponseViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewModels/CreateInitialDataSetVersionResponseViewModel.cs @@ -2,6 +2,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.ViewM public record CreateInitialDataSetVersionResponseViewModel { + public required Guid DataSetId { get; init; } public required Guid DataSetVersionId { get; init; } public required Guid InstanceId { get; init; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Functions/CreateInitialDataSetVersionFunction.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Functions/CreateInitialDataSetVersionFunction.cs index 81cab5e52d4..f7d1a6f0c01 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Functions/CreateInitialDataSetVersionFunction.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Functions/CreateInitialDataSetVersionFunction.cs @@ -35,17 +35,19 @@ public async Task CreateInitialDataSetVersion( instanceId, cancellationToken: cancellationToken )) - .OnSuccess(async dataSetVersionId => + .OnSuccess(async tuple => { await ProcessInitialDataSetVersion( client, - dataSetVersionId: dataSetVersionId, + dataSetVersionId: tuple.dataSetVersionId, instanceId: instanceId, cancellationToken); return new CreateInitialDataSetVersionResponseViewModel { - DataSetVersionId = dataSetVersionId, InstanceId = instanceId + DataSetId = tuple.dataSetId, + DataSetVersionId = tuple.dataSetVersionId, + InstanceId = instanceId }; }) .HandleFailuresOr(result => new OkObjectResult(result)); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs index 7a9f392d61e..97ae7b4dbaf 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/DataSetService.cs @@ -20,7 +20,7 @@ public class DataSetService( PublicDataDbContext publicDataDbContext ) : IDataSetService { - public async Task> CreateDataSetVersion( + public async Task> CreateDataSetVersion( InitialDataSetVersionCreateRequest request, Guid instanceId, CancellationToken cancellationToken = default) @@ -34,7 +34,7 @@ await CreateDataSetVersion(dataSet, releaseFile, cancellationToken)) await CreateDataSetVersionImport(dataSetVersion, instanceId, cancellationToken)) .OnSuccessDo(async dataSetVersion => await UpdateFilePublicDataSetVersionId(releaseFile, dataSetVersion, cancellationToken)) - .OnSuccess(dataSetVersion => dataSetVersion.Id)); + .OnSuccess(dataSetVersion => (dataSetId: dataSetVersion.DataSetId, dataSetVersionId: dataSetVersion.Id))); } private async Task> GetReleaseFile( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/IDataSetService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/IDataSetService.cs index dc06c59e542..3b2e1834199 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/IDataSetService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Processor/Services/IDataSetService.cs @@ -6,7 +6,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Processor.Servi public interface IDataSetService { - Task> CreateDataSetVersion( + Task> CreateDataSetVersion( InitialDataSetVersionCreateRequest request, Guid instanceId, CancellationToken cancellationToken = default); From 77e22c3e668ebf72ac4a43e85f88dde34b47990d Mon Sep 17 00:00:00 2001 From: SaicharanMuthyapwar Date: Tue, 14 May 2024 15:17:53 +0100 Subject: [PATCH 15/66] EES-5142 : Actioned as per PR comments --- .../general-public/pages/FindStatisticsPage.ts | 5 ++++- tests/playwright-tests/general-public/pages/ReleasePage.ts | 4 ++-- .../tests/admin-and-public/publishPublication.spec.ts | 3 ++- tests/playwright-tests/utils/slugFromTitle.ts | 4 ++++ 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 tests/playwright-tests/utils/slugFromTitle.ts diff --git a/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts b/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts index c677440c888..e413ddfa658 100644 --- a/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts +++ b/tests/playwright-tests/general-public/pages/FindStatisticsPage.ts @@ -1,4 +1,5 @@ import { Locator, Page } from '@playwright/test'; +import slugFromTitle from '@util/slugFromTitle'; export default class FindStatisticsPage { readonly page: Page; @@ -12,6 +13,8 @@ export default class FindStatisticsPage { async navigateToPublicReleasePage(publicationName: string) { await this.releaseLink(publicationName).click({ force: true }); - await this.page.waitForNavigation(); + await this.page.waitForURL( + `**/find-statistics/${slugFromTitle(publicationName)}`, + ); } } diff --git a/tests/playwright-tests/general-public/pages/ReleasePage.ts b/tests/playwright-tests/general-public/pages/ReleasePage.ts index 3ba28b75103..ff31b33fae9 100644 --- a/tests/playwright-tests/general-public/pages/ReleasePage.ts +++ b/tests/playwright-tests/general-public/pages/ReleasePage.ts @@ -2,11 +2,11 @@ import { Locator, Page } from '@playwright/test'; export default class ReleasePage { readonly page: Page; - readonly pageTitle: Locator; + readonly pageTitle: (text: string) => Locator; constructor(page: Page) { this.page = page; // Locators - this.pageTitle = page.locator('h1[data-testid="page-title"]'); + this.pageTitle = (text: string) => page.locator(`//h1[text()="${text}"]`); } } diff --git a/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts b/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts index 0a62c382417..2d6faf1a39c 100644 --- a/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts +++ b/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts @@ -69,6 +69,7 @@ test('Verify that user is able to create a release via admin', async ({ test('Validate that user is able to see the created release in public and title is same as publication name', async () => { await homePage.navigateToExploreFindStatisticsPage(); await findStatisticsPage.navigateToPublicReleasePage(publicationName); - const title = await releasePage.pageTitle.textContent(); + + const title = await releasePage.pageTitle(publicationName).innerText(); await expect(title).toContain(publicationName); }); diff --git a/tests/playwright-tests/utils/slugFromTitle.ts b/tests/playwright-tests/utils/slugFromTitle.ts new file mode 100644 index 00000000000..69874534a08 --- /dev/null +++ b/tests/playwright-tests/utils/slugFromTitle.ts @@ -0,0 +1,4 @@ +export default function slugFromTitle(title: string) { + return title.replace(/\W+/g, ' ').trim().toLowerCase().replace(/\s+/g, '-'); + } + \ No newline at end of file From bcf8cc89bd364d0e691ad6dd4bbb66637ee83a78 Mon Sep 17 00:00:00 2001 From: SaicharanMuthyapwar Date: Tue, 14 May 2024 15:58:49 +0100 Subject: [PATCH 16/66] EES-5142 : Actioned as per PR comments --- .../tests/admin-and-public/publishPublication.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts b/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts index 2d6faf1a39c..47281f7f335 100644 --- a/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts +++ b/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts @@ -69,7 +69,9 @@ test('Verify that user is able to create a release via admin', async ({ test('Validate that user is able to see the created release in public and title is same as publication name', async () => { await homePage.navigateToExploreFindStatisticsPage(); await findStatisticsPage.navigateToPublicReleasePage(publicationName); - - const title = await releasePage.pageTitle(publicationName).innerText(); - await expect(title).toContain(publicationName); + + await expect(releasePage.pageTitle(publicationName)).toHaveText( + publicationName, + { useInnerText: true }, + ); }); From e414c043ff41cf86a94f0c17b96b7f755b46cbc7 Mon Sep 17 00:00:00 2001 From: SaicharanMuthyapwar Date: Tue, 14 May 2024 16:23:39 +0100 Subject: [PATCH 17/66] EES-5142 Tidyup the code --- .../tests/admin-and-public/publishPublication.spec.ts | 2 +- tests/playwright-tests/utils/slugFromTitle.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts b/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts index 47281f7f335..d1717cdc3be 100644 --- a/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts +++ b/tests/playwright-tests/tests/admin-and-public/publishPublication.spec.ts @@ -73,5 +73,5 @@ test('Validate that user is able to see the created release in public and title await expect(releasePage.pageTitle(publicationName)).toHaveText( publicationName, { useInnerText: true }, - ); + ); }); diff --git a/tests/playwright-tests/utils/slugFromTitle.ts b/tests/playwright-tests/utils/slugFromTitle.ts index 69874534a08..de3b4d00676 100644 --- a/tests/playwright-tests/utils/slugFromTitle.ts +++ b/tests/playwright-tests/utils/slugFromTitle.ts @@ -1,4 +1,3 @@ export default function slugFromTitle(title: string) { - return title.replace(/\W+/g, ' ').trim().toLowerCase().replace(/\s+/g, '-'); - } - \ No newline at end of file + return title.replace(/\W+/g, ' ').trim().toLowerCase().replace(/\s+/g, '-'); +} From 704d925da10c0f515198bc4f2cf445d57c8c899f Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Wed, 15 May 2024 14:52:18 +0100 Subject: [PATCH 18/66] Update 'Backend' build job to use xlarge agents This attempts to fix what appear to be deadlock issues with running the integration tests. --- azure-pipelines.dfe.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.dfe.yml b/azure-pipelines.dfe.yml index 5cc2145a7f5..465e228f496 100644 --- a/azure-pipelines.dfe.yml +++ b/azure-pipelines.dfe.yml @@ -27,7 +27,7 @@ pr: jobs: - job: 'Backend' - pool: 'ees-ubuntu2204-large' + pool: 'ees-ubuntu2204-xlarge' workspace: clean: all steps: @@ -138,7 +138,7 @@ jobs: repository: 'ees-public-api/api' command: 'push' tags: $(Build.BuildNumber) - + - task: 'DotNetCoreCLI@2' displayName: 'Publish Public API - Data Processor Function' inputs: @@ -154,7 +154,7 @@ jobs: inputs: artifactName: 'public-api-data-processor-$(Build.BuildNumber)' targetPath: '$(Build.ArtifactStagingDirectory)/public-api-data-processor' - + - task: 'DotNetCoreCLI@2' displayName: 'Publish Notifier Function' inputs: @@ -356,7 +356,7 @@ jobs: containerRegistry: '$(AcrServiceConnection)' repository: 'ees-public-frontend' command: 'push' - tags: $(Build.BuildNumber) + tags: $(Build.BuildNumber) - job: 'MiscellaneousArtifacts' pool: From 7195075fa9aeab3a5ac8d2265b71e596f743ae9d Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Wed, 15 May 2024 14:14:34 +0100 Subject: [PATCH 19/66] Add missing CORS headers for public frontend locally Signed-off-by: Nicholas Tsim --- .../Startup.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Startup.cs index de76344c6b8..fc4b6003fad 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Startup.cs @@ -221,6 +221,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseResponseCaching(); app.UseResponseCompression(); + // CORS + + app.UseCors(options => options + .WithOrigins( + "http://localhost:3000", + "http://localhost:3001", + "https://localhost:3000", + "https://localhost:3001") + .AllowAnyMethod() + .AllowAnyHeader()); + // Routing / endpoints app.UseRouting(); @@ -228,6 +239,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { builder.MapControllers(); }); + app.UseHealthChecks("/api/health"); } From 38ac6eced0889ca1d5d6153cb9d42c0c2a34d746 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Wed, 15 May 2024 15:58:00 +0100 Subject: [PATCH 20/66] EES-5144 - allowing apostrophes in AspNetUsers.Username column, which previously was stopping newly invited people with apostrophes in their email addresses from signing in for the first time --- .../Startup.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 4b46ed667b5..04211b7280f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -279,6 +279,14 @@ public virtual void ConfigureServices(IServiceCollection services) .AddRoleStore>() .AddEntityFrameworkStores(); + services.Configure(options => + { + // Allow special characters such as apostrophes and @ symbols to be permitted in AspNetUsers' + // "Username" column. This allows us to store email addresses as Usernames when newly invited users + // sign in. + options.User.AllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+'"; + }); // This service helps to add additional information to the ClaimsPrincipal on the HttpContext after // Identity Framework has verified that the incoming JWTs are valid (and has created the basic // ClaimsPrincipal already from information in the JWT). From 1c8b69cc9ca5d84b665f6a9d8d6219cd62ecf21f Mon Sep 17 00:00:00 2001 From: dfe-sdt Date: Thu, 16 May 2024 08:41:39 +0000 Subject: [PATCH 21/66] chore(tests): update test snapshots --- .../tests/snapshots/find_statistics_snapshot.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json b/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json index fb9aea29f4d..dd71da71728 100644 --- a/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json +++ b/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json @@ -79,7 +79,7 @@ { "publication_summary": "Children accommodated in secure children's homes in England and Wales including approved places, availability, occupancy, sex, age, ethnicity, length of stay", "publication_title": "Children accommodated in secure children's homes", - "published": "18 May 2023", + "published": "16 May 2024", "release_type": "National statistics", "theme": "Children's social care" }, @@ -506,14 +506,14 @@ { "publication_summary": "Pupil absence, including overall, authorised and unauthorised absence and persistent absence by reason and pupil characteristics for the full academic year.", "publication_title": "Pupil absence in schools in England", - "published": "21 Mar 2024", + "published": "16 May 2024", "release_type": "National statistics", "theme": "Pupils and schools" }, { "publication_summary": "Pupil attendance and absence data including termly national statistics and fortnightly experimental statistics derived from DfE\u2019s regular attendance data", "publication_title": "Pupil attendance in schools", - "published": "2 May 2024", + "published": "16 May 2024", "release_type": "Official statistics in development", "theme": "Pupils and schools" }, From 088c9f9a99673a11f02060e5a65d31c9ef866c33 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Tue, 16 Apr 2024 00:11:12 +0100 Subject: [PATCH 22/66] EES-4722 Refactor page, pageSize and debug into `DataSetQueryRequest` --- .../Requests/DataSetQueryRequest.cs | 22 ++++++++++++ .../Services/DataSetQueryService.cs | 34 +++++++++---------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs index 35c52199e77..53e57c4ad3e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; + namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; /// @@ -22,4 +24,24 @@ public record DataSetQueryRequest /// By default, results are sorted by time period in descending order. /// public IReadOnlyList? Sorts { get; init; } + + /// + /// Enable debug mode. Results will be formatted with human-readable + /// labels to assist in identification. + /// + /// This **should not** be enabled in a production environment. + /// + public bool Debug { get; init; } + + /// + /// The page of results to fetch. + /// + [DefaultValue(1)] + public int Page { get; init; } = 1; + + /// + /// The maximum number of results per page. + /// + [DefaultValue(1000)] + public int PageSize { get; init; } = 1000; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs index fc8babd729c..1c4c802debf 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs @@ -74,16 +74,16 @@ public async Task> Q }, Indicators = request.Indicators, Sorts = request.Sorts?.Select(DataSetQuerySort.Parse).ToList(), + Debug = request.Debug, + Page = request.Page, + PageSize = request.PageSize, }; return await FindDataSetVersion(dataSetId, dataSetVersion, cancellationToken) .OnSuccessDo(userService.CheckCanQueryDataSetVersion) .OnSuccess(dsv => RunQuery( dataSetVersion: dsv, - request: query, - page: request.Page, - pageSize: request.PageSize, - debug: request.Debug, + query: query, cancellationToken: cancellationToken )) .OnSuccess(results => results with @@ -130,10 +130,7 @@ private async Task> FindDataSetVersion( private async Task> RunQuery( DataSetVersion dataSetVersion, - DataSetQueryRequest request, - int page, - int pageSize, - bool debug, + DataSetQueryRequest query, CancellationToken cancellationToken) { using var _ = MiniProfiler.Current @@ -143,10 +140,10 @@ private async Task> var whereBuilder = new DuckDbSqlBuilder(); - if (request.Criteria is not null) + if (query.Criteria is not null) { whereBuilder += await dataSetQueryParser.ParseCriteria( - request.Criteria, + query.Criteria, dataSetVersion, queryState, cancellationToken @@ -158,10 +155,10 @@ private async Task> await Task.WhenAll(columnsTask, indicatorsTask); - var indicators = GetIndicators(request, indicatorsTask.Result, queryState); + var indicators = GetIndicators(query, indicatorsTask.Result, queryState); var sorts = GetSorts( - request: request, + request: query, columns: columnsTask.Result, indicators: indicators, queryState: queryState); @@ -192,13 +189,13 @@ private async Task> ], where: whereSql, sorts: sorts, - page: page, - pageSize: pageSize, + page: query.Page, + pageSize: query.PageSize, cancellationToken: cancellationToken); await Task.WhenAll(countTask, rowsTask); - if (debug) + if (query.Debug) { queryState.Warnings.Add(new WarningViewModel { @@ -218,12 +215,15 @@ private async Task> return new DataSetQueryPaginatedResultsViewModel { - Paging = new PagingViewModel(page, pageSize, (int)countTask.Result), + Paging = new PagingViewModel( + page: query.Page, + pageSize: query.PageSize, + totalResults: (int)countTask.Result), Results = await MapQueryResults( rows: rowsTask.Result, dataSetVersion: dataSetVersion, columnsByType: columnsByType, - debug: debug, + debug: query.Debug, cancellationToken: cancellationToken), Warnings = queryState.Warnings }; From 0d4c42bc8f8a945fa5e51008c86e437eeaa128cd Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Sun, 28 Apr 2024 02:36:11 +0100 Subject: [PATCH 23/66] EES-4722 Add validators for `DataSetQueryCriteria` request models As part of this, `DataSetGetQueryFilters` and `DataSetGetQueryLocations` now extend their respective criteria model, with the exception of properties where annotations are required for the GET query. --- .../DataSetGetQueryFiltersValidatorTests.cs | 131 +++---- ...tGetQueryGeographicLevelsValidatorTests.cs | 154 ++++---- .../DataSetGetQueryLocationsValidatorTests.cs | 208 ++++++---- ...ataSetGetQueryTimePeriodsValidatorTests.cs | 308 ++++++++++----- ...taSetQueryCriteriaFiltersValidatorTests.cs | 257 +++++++++++++ ...yCriteriaGeographicLevelsValidatorTests.cs | 243 ++++++++++++ ...SetQueryCriteriaLocationsValidatorTests.cs | 262 +++++++++++++ ...tQueryCriteriaTimePeriodsValidatorTests.cs | 357 ++++++++++++++++++ .../Requests/DataSetGetQueryFilters.cs | 53 +-- .../DataSetGetQueryGeographicLevels.cs | 51 +-- .../Requests/DataSetQueryCriteriaFilters.cs | 42 ++- .../DataSetQueryCriteriaGeographicLevels.cs | 62 ++- .../Requests/DataSetQueryCriteriaLocations.cs | 45 +++ .../DataSetQueryCriteriaTimePeriods.cs | 48 +++ .../Query/GeographicLevelFacetsParser.cs | 8 +- 15 files changed, 1773 insertions(+), 456 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFiltersValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaGeographicLevelsValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaLocationsValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaTimePeriodsValidatorTests.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryFiltersValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryFiltersValidatorTests.cs index c5f8646fa96..96837ed009c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryFiltersValidatorTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryFiltersValidatorTests.cs @@ -8,26 +8,11 @@ public abstract class DataSetGetQueryFiltersValidatorTests { private readonly DataSetGetQueryFilters.Validator _validator = new(); - public static IEnumerable ValidFiltersSingle() - { - return - [ - [null], - ["abc"], - ["12345"], - ["123456789"], - ["1234567890"], - ]; - } + public static readonly TheoryData ValidFiltersSingle = + DataSetQueryCriteriaFiltersValidatorTests.ValidFiltersSingle; - public static IEnumerable ValidFiltersMultiple() - { - return - [ - ["abc", "12345", "123456789"], - ["1234567890", "123", "abcde"], - ]; - } + public static readonly TheoryData ValidFiltersMultiple = + DataSetQueryCriteriaFiltersValidatorTests.ValidFiltersMultiple; public class EqTests : DataSetGetQueryFiltersValidatorTests { @@ -35,10 +20,7 @@ public class EqTests : DataSetGetQueryFiltersValidatorTests [MemberData(nameof(ValidFiltersSingle))] public void Success(string? filter) { - var query = new DataSetGetQueryFilters - { - Eq = filter - }; + var query = new DataSetGetQueryFilters { Eq = filter }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } @@ -46,27 +28,23 @@ public void Success(string? filter) [Fact] public void Failure_Empty() { - var query = new DataSetGetQueryFilters - { - Eq = "" - }; + var query = new DataSetGetQueryFilters { Eq = "" }; _validator.TestValidate(query) .ShouldHaveValidationErrorFor(q => q.Eq) - .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); } [Fact] public void Failure_MaxLength() { - var query = new DataSetGetQueryFilters - { - Eq = "12345678901" - }; + var query = new DataSetGetQueryFilters { Eq = "12345678901" }; _validator.TestValidate(query) .ShouldHaveValidationErrorFor(q => q.Eq) - .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator) + .Only(); } } @@ -76,10 +54,7 @@ public class NotEqTests : DataSetGetQueryFiltersValidatorTests [MemberData(nameof(ValidFiltersSingle))] public void Success(string? filter) { - var query = new DataSetGetQueryFilters - { - NotEq = filter - }; + var query = new DataSetGetQueryFilters { NotEq = filter }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } @@ -87,27 +62,23 @@ public void Success(string? filter) [Fact] public void Failure_Empty() { - var query = new DataSetGetQueryFilters - { - NotEq = "" - }; + var query = new DataSetGetQueryFilters { NotEq = "" }; _validator.TestValidate(query) .ShouldHaveValidationErrorFor(q => q.NotEq) - .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); } [Fact] public void Failure_MaxLength() { - var query = new DataSetGetQueryFilters - { - NotEq = "12345678901" - }; + var query = new DataSetGetQueryFilters { NotEq = "12345678901" }; _validator.TestValidate(query) .ShouldHaveValidationErrorFor(q => q.NotEq) - .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator) + .Only(); } } @@ -117,10 +88,7 @@ public class InTests : DataSetGetQueryFiltersValidatorTests [MemberData(nameof(ValidFiltersMultiple))] public void Success(params string[] filters) { - var query = new DataSetGetQueryFilters - { - In = filters - }; + var query = new DataSetGetQueryFilters { In = filters }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } @@ -128,30 +96,40 @@ public void Success(params string[] filters) [Fact] public void Success_Null() { - var query = new DataSetGetQueryFilters - { - In = null - }; + var query = new DataSetGetQueryFilters { In = null }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Fact] - public void Failure_EmptyStrings() + public void Failure_Empty() + { + var query = new DataSetGetQueryFilters { In = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_EmptyValues() { var query = new DataSetGetQueryFilters { - In = ["", ""] + In = ["", " ", null!] }; var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.In); + Assert.Equal(query.In.Count, result.Errors.Count); result.ShouldHaveValidationErrorFor("In[0]") .WithErrorCode(FluentValidationKeys.NotEmptyValidator); result.ShouldHaveValidationErrorFor("In[1]") .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor("In[2]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); } [Fact] @@ -164,7 +142,7 @@ public void Failure_MaxLengths() var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.In); + Assert.Equal(query.In.Count, result.Errors.Count); result.ShouldHaveValidationErrorFor("In[0]") .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); @@ -179,10 +157,7 @@ public class NotInTests : DataSetGetQueryFiltersValidatorTests [MemberData(nameof(ValidFiltersMultiple))] public void Success(params string[] filters) { - var query = new DataSetGetQueryFilters - { - NotIn = filters - }; + var query = new DataSetGetQueryFilters { NotIn = filters }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } @@ -190,30 +165,40 @@ public void Success(params string[] filters) [Fact] public void Success_Null() { - var query = new DataSetGetQueryFilters - { - NotIn = null - }; + var query = new DataSetGetQueryFilters { NotIn = null }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Fact] - public void Failure_EmptyStrings() + public void Failure_Empty() + { + var query = new DataSetGetQueryFilters { NotIn = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_EmptyValues() { var query = new DataSetGetQueryFilters { - NotIn = ["", ""] + NotIn = ["", " ", null!] }; var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.NotIn); + Assert.Equal(query.NotIn.Count, result.Errors.Count); result.ShouldHaveValidationErrorFor("NotIn[0]") .WithErrorCode(FluentValidationKeys.NotEmptyValidator); result.ShouldHaveValidationErrorFor("NotIn[1]") .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor("NotIn[2]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); } [Fact] @@ -226,7 +211,7 @@ public void Failure_MaxLengths() var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.NotIn); + Assert.Equal(query.NotIn.Count, result.Errors.Count); result.ShouldHaveValidationErrorFor("NotIn[0]") .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); @@ -250,6 +235,8 @@ public void AllEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(4, result.Errors.Count); + result.ShouldHaveValidationErrorFor(q => q.Eq) .WithErrorCode(FluentValidationKeys.NotEmptyValidator); result.ShouldHaveValidationErrorFor(q => q.NotEq) @@ -273,6 +260,8 @@ public void SomeEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(2, result.Errors.Count); + result.ShouldNotHaveValidationErrorFor(q => q.Eq); result.ShouldNotHaveValidationErrorFor(q => q.In); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryGeographicLevelsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryGeographicLevelsValidatorTests.cs index 35ca69b0211..1d757da9396 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryGeographicLevelsValidatorTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryGeographicLevelsValidatorTests.cs @@ -1,4 +1,6 @@ using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Validators; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Validators; using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; @@ -9,50 +11,17 @@ public abstract class DataSetGetQueryGeographicLevelsValidatorTests { private readonly DataSetGetQueryGeographicLevels.Validator _validator = new(); - public static IEnumerable ValidGeographicLevelsSingle() - { - return - [ - ["NAT"], - ["LA"], - [null], - ]; - } + public static readonly TheoryData ValidGeographicLevelsSingle = + DataSetQueryCriteriaGeographicLevelsValidatorTests.ValidGeographicLevelsSingle; - public static IEnumerable ValidGeographicLevelsMultiple() - { - return - [ - ["NAT", "LA", "REG"], - ["SCH", "NAT"], - ["PROV"], - ]; - } + public static readonly TheoryData ValidGeographicLevelsMultiple = + DataSetQueryCriteriaGeographicLevelsValidatorTests.ValidGeographicLevelsMultiple; - public static IEnumerable InvalidGeographicLevelsSingle() - { - return - [ - [""], - ["Invalid"], - ["National"], - ["nat"], - ["la"], - ["1"], - ]; - } + public static readonly TheoryData InvalidGeographicLevelsSingle = + DataSetQueryCriteriaGeographicLevelsValidatorTests.InvalidGeographicLevelsSingle; - public static IEnumerable InvalidGeographicLevelsMultiple() - { - return - [ - ["Invalid1", "Invalid2"], - ["National", "LocalAuthority"], - ["nat", "la"], - ["Local authority"], - ["National"], - ]; - } + public static readonly TheoryData InvalidGeographicLevelsMultiple = + DataSetQueryCriteriaGeographicLevelsValidatorTests.InvalidGeographicLevelsMultiple; public class EqTests : DataSetGetQueryGeographicLevelsValidatorTests { @@ -71,20 +40,15 @@ public void Failure_NotAllowed(string geographicLevel) { var query = new DataSetGetQueryGeographicLevels { Eq = geographicLevel }; - var result = _validator.Validate(query); - - Assert.False(result.IsValid); - - var error = Assert.Single(result.Errors); - - Assert.Equal(nameof(DataSetGetQueryGeographicLevels.Eq), error.PropertyName); - Assert.Equal(ValidationMessages.AllowedValue.Code, error.ErrorCode); - Assert.Equal(ValidationMessages.AllowedValue.Message, error.ErrorMessage); - - var state = Assert.IsType>(error.CustomState); + var result = _validator.TestValidate(query); - Assert.Equal(geographicLevel, state.Value); - Assert.Equal(GeographicLevelUtils.OrderedCodes, state.Allowed); + result.ShouldHaveValidationErrorFor(g => g.Eq) + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => s.Value == geographicLevel) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)) + .Only(); } } @@ -105,20 +69,15 @@ public void Failure_NotAllowed(string geographicLevel) { var query = new DataSetGetQueryGeographicLevels { NotEq = geographicLevel }; - var result = _validator.Validate(query); - - Assert.False(result.IsValid); - - var error = Assert.Single(result.Errors); - - Assert.Equal(nameof(DataSetGetQueryGeographicLevels.NotEq), error.PropertyName); - Assert.Equal(ValidationMessages.AllowedValue.Code, error.ErrorCode); - Assert.Equal(ValidationMessages.AllowedValue.Message, error.ErrorMessage); - - var state = Assert.IsType>(error.CustomState); + var result = _validator.TestValidate(query); - Assert.Equal(geographicLevel, state.Value); - Assert.Equal(GeographicLevelUtils.OrderedCodes, state.Allowed); + result.ShouldHaveValidationErrorFor(g => g.NotEq) + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => s.Value == geographicLevel) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)) + .Only(); } } @@ -141,27 +100,37 @@ public void Success_Null() _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } + [Fact] + public void Failure_Empty() + { + var query = new DataSetGetQueryGeographicLevels { In = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + [Theory] [MemberData(nameof(InvalidGeographicLevelsMultiple))] public void Failure_NotAllowed(params string[] geographicLevels) { var query = new DataSetGetQueryGeographicLevels { In = geographicLevels }; - var result = _validator.Validate(query); + var result = _validator.TestValidate(query); - Assert.False(result.IsValid); + Assert.Equal(geographicLevels.Length, result.Errors.Count); - Assert.All(result.Errors, (error, index) => + foreach (var (error, index) in result.Errors.WithIndex()) { - Assert.Equal($"In[{index}]", error.PropertyName); - Assert.Equal(ValidationMessages.AllowedValue.Code, error.ErrorCode); - Assert.Equal(ValidationMessages.AllowedValue.Message, error.ErrorMessage); - - var state = Assert.IsType>(error.CustomState); - - Assert.Equal(error.AttemptedValue, state.Value); - Assert.Equal(GeographicLevelUtils.OrderedCodes, state.Allowed); - }); + result.ShouldHaveValidationErrorFor($"In[{index}]") + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => + s.Value == (string)error.AttemptedValue) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)); + } } } @@ -191,21 +160,20 @@ public void Failure_NotAllowed(params string[] geographicLevels) { var query = new DataSetGetQueryGeographicLevels { In = geographicLevels }; - var result = _validator.Validate(query); + var result = _validator.TestValidate(query); - Assert.False(result.IsValid); + Assert.Equal(geographicLevels.Length, result.Errors.Count); - Assert.All(result.Errors, (error, index) => + foreach (var (error, index) in result.Errors.WithIndex()) { - Assert.Equal($"In[{index}]", error.PropertyName); - Assert.Equal(ValidationMessages.AllowedValue.Code, error.ErrorCode); - Assert.Equal(ValidationMessages.AllowedValue.Message, error.ErrorMessage); - - var state = Assert.IsType>(error.CustomState); - - Assert.Equal(error.AttemptedValue, state.Value); - Assert.Equal(GeographicLevelUtils.OrderedCodes, state.Allowed); - }); + result.ShouldHaveValidationErrorFor($"In[{index}]") + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => + s.Value == (string)error.AttemptedValue) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)); + } } } @@ -224,6 +192,8 @@ public void AllEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(4, result.Errors.Count); + result.ShouldHaveValidationErrorFor(q => q.Eq) .WithErrorCode(ValidationMessages.AllowedValue.Code); result.ShouldHaveValidationErrorFor(q => q.NotEq) @@ -247,6 +217,8 @@ public void SomeEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(2, result.Errors.Count); + result.ShouldNotHaveValidationErrorFor(q => q.Eq); result.ShouldNotHaveValidationErrorFor(q => q.In); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryLocationsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryLocationsValidatorTests.cs index 6eb9cbbe91a..e4c626282a1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryLocationsValidatorTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryLocationsValidatorTests.cs @@ -9,99 +9,81 @@ public abstract class DataSetGetQueryLocationsValidatorTests { private readonly DataSetGetQueryLocations.Validator _validator = new(); - public static IEnumerable ValidLocationStringsSingle() - { - return - [ - ["NAT|id|12345"], - ["NAT|code|E92000001"], - ["REG|id|12345"], - ["REG|code|E12000003"], - ["LA|id|12345"], - ["LA|code|E08000019"], - ["LA|code|E09000021 / E09000027"], - ["LA|oldCode|373"], - ["LA|oldCode|314 / 318"], - ["SCH|id|12345"], - ["SCH|urn|107029"], - ["SCH|laEstab|3732060"], - ["PROV|id|12345"], - ["PROV|ukprn|10066874"], - ]; - } + public static readonly TheoryData ValidLocationQueriesSingle = + DataSetQueryCriteriaLocationsValidatorTests.ValidLocationsSingle; - public static IEnumerable ValidLocationStringsMultiple() - { - return - [ - ["NAT|id|12345", "NAT|code|E92000001"], - ["REG|id|12345", "REG|code|E12000003"], - ["LA|id|12345", "LA|code|E08000019", "LA|oldCode|373"], - ["SCH|id|12345", "SCH|urn|107029", "SCH|laEstab|3732060"], - ["PROV|id|12345", "PROV|ukprn|10066874"], - ]; - } + public static readonly TheoryData ValidLocationQueriesMultiple = + DataSetQueryCriteriaLocationsValidatorTests.ValidLocationsMultiple; - public static IEnumerable InvalidLocationStringsSingle() + public static readonly TheoryData InvalidLocationFormatsSingle = new() { - return - [ - [""], - ["Invalid"], - ["NA|id|12345"], - ["NAT|urn|12345"], - ["NAT|id|99999999999999999999999999"], - ]; - } - - public static IEnumerable InvalidLocationStringsMultiple() + "", + " ", + "Invalid", + "NAT|", + "|", + "||" + }; + + public static readonly TheoryData InvalidLocationFormatsMultiple = new() { - return - [ - [], - ["", ""], - ["Invalid", "NAT|id", "NAT|code|"], - ["NA|id|12345", "la|code|12345"], - ["NAT|urn|12345", "REG|ukprn|12345", "RSC|code|12345"], - ["NAT|id|99999999999999999999999999", "NAT|code|99999999999999999999999999"], - ]; - } + new [] { "", " ", null! }, + new [] { "Invalid", "NAT|", "|", "||" }, + }; + + public static readonly TheoryData InvalidLocationQueriesSingle = + DataSetQueryCriteriaLocationsValidatorTests.InvalidLocationsSingle; + public static readonly TheoryData InvalidLocationQueriesMultiple = + DataSetQueryCriteriaLocationsValidatorTests.InvalidLocationsMultiple; + public class EqTests : DataSetGetQueryLocationsValidatorTests { [Theory] - [MemberData(nameof(ValidLocationStringsSingle))] - public void Success(string location) + [MemberData(nameof(ValidLocationQueriesSingle))] + public void Success(DataSetQueryLocation location) { - var query = new DataSetGetQueryLocations { Eq = location }; + var query = new DataSetGetQueryLocations { Eq = location.ToLocationString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidLocationStringsSingle))] + [MemberData(nameof(InvalidLocationFormatsSingle))] public void Failure_InvalidString(string location) { var query = new DataSetGetQueryLocations { Eq = location }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.Eq); + .ShouldHaveValidationErrorFor(q => q.Eq) + .Only(); + } + + [Theory] + [MemberData(nameof(InvalidLocationQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryLocation location) + { + var query = new DataSetGetQueryLocations { Eq = location.ToLocationString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Eq) + .Only(); } } public class NotEqTests : DataSetGetQueryLocationsValidatorTests { [Theory] - [MemberData(nameof(ValidLocationStringsSingle))] - public void Success(string location) + [MemberData(nameof(ValidLocationQueriesSingle))] + public void Success(DataSetQueryLocation location) { - var query = new DataSetGetQueryLocations { NotEq = location }; + var query = new DataSetGetQueryLocations { NotEq = location.ToLocationString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidLocationStringsSingle))] + [MemberData(nameof(InvalidLocationFormatsSingle))] public void Failure_InvalidString(string location) { var query = new DataSetGetQueryLocations { NotEq = location }; @@ -109,28 +91,68 @@ public void Failure_InvalidString(string location) _validator.TestValidate(query) .ShouldHaveValidationErrorFor(q => q.NotEq); } + + [Theory] + [MemberData(nameof(InvalidLocationQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryLocation location) + { + var query = new DataSetGetQueryLocations { NotEq = location.ToLocationString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.NotEq); + } } public class InTests : DataSetGetQueryLocationsValidatorTests { [Theory] - [MemberData(nameof(ValidLocationStringsMultiple))] - public void Success(params string[] locations) + [MemberData(nameof(ValidLocationQueriesMultiple))] + public void Success(DataSetQueryLocation[] locations) { - var testObj = new DataSetGetQueryLocations { In = locations }; + var query = new DataSetGetQueryLocations + { + In = locations.Select(l => l.ToLocationString()).ToList() + }; - _validator.TestValidate(testObj).ShouldNotHaveAnyValidationErrors(); + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetGetQueryLocations { In = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); } [Theory] - [MemberData(nameof(InvalidLocationStringsMultiple))] - public void Failure_InvalidStrings(params string[] locations) + [MemberData(nameof(InvalidLocationFormatsMultiple))] + public void Failure_InvalidFormats(string[] locations) { var query = new DataSetGetQueryLocations { In = locations }; var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.In); + Assert.Equal(locations.Length, result.Errors.Count); + + locations.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"In[{index}]")); + } + + [Theory] + [MemberData(nameof(InvalidLocationQueriesMultiple))] + public void Failure_InvalidQueries(DataSetQueryLocation[] locations) + { + var query = new DataSetGetQueryLocations + { + In = [..locations.Select(l => l.ToLocationString())] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(locations.Length, result.Errors.Count); locations.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"In[{index}]")); } @@ -139,23 +161,53 @@ public void Failure_InvalidStrings(params string[] locations) public class NotInTests : DataSetGetQueryLocationsValidatorTests { [Theory] - [MemberData(nameof(ValidLocationStringsMultiple))] - public void Success(params string[] locations) + [MemberData(nameof(ValidLocationQueriesMultiple))] + public void Success(DataSetQueryLocation[] locations) { - var testObj = new DataSetGetQueryLocations { NotIn = locations }; + var query = new DataSetGetQueryLocations + { + NotIn = locations.Select(l => l.ToLocationString()).ToList() + }; - _validator.TestValidate(testObj).ShouldNotHaveAnyValidationErrors(); + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetGetQueryLocations { NotIn = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); } [Theory] - [MemberData(nameof(InvalidLocationStringsMultiple))] - public void Failure_InvalidStrings(params string[] locations) + [MemberData(nameof(InvalidLocationFormatsMultiple))] + public void Failure_InvalidFormats(string[] locations) { var query = new DataSetGetQueryLocations { NotIn = locations }; var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.NotIn); + Assert.Equal(locations.Length, result.Errors.Count); + + locations.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"NotIn[{index}]")); + } + + [Theory] + [MemberData(nameof(InvalidLocationQueriesMultiple))] + public void Failure_InvalidQueries(DataSetQueryLocation[] locations) + { + var query = new DataSetGetQueryLocations + { + NotIn = [..locations.Select(l => l.ToLocationString())] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(locations.Length, result.Errors.Count); locations.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"NotIn[{index}]")); } @@ -176,6 +228,8 @@ public void AllEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(4, result.Errors.Count); + result.ShouldHaveValidationErrorFor(q => q.Eq) .WithErrorCode(FluentValidationKeys.NotEmptyValidator); result.ShouldHaveValidationErrorFor(q => q.NotEq) @@ -199,6 +253,8 @@ public void SomeEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(2, result.Errors.Count); + result.ShouldNotHaveValidationErrorFor(q => q.Eq); result.ShouldNotHaveValidationErrorFor(q => q.In); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryTimePeriodsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryTimePeriodsValidatorTests.cs index f9b8b4e954c..675921542c3 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryTimePeriodsValidatorTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetGetQueryTimePeriodsValidatorTests.cs @@ -9,126 +9,153 @@ public abstract class DataSetGetQueryTimePeriodsValidatorTests { private readonly DataSetGetQueryTimePeriods.Validator _validator = new(); - public static IEnumerable ValidTimePeriodStringsSingle() - { - return - [ - ["2020|AY"], - ["2020/2021|AY"], - ["2020|FY"], - ["2021|M1"], - ["2021|W40"], - ["2021|T3"], - ["2021/2022|T3"], - ["2019|FYQ4"], - ["2019/2020|FYQ4"], - ]; - } + public static readonly TheoryData ValidTimePeriodQueriesSingle = + DataSetQueryCriteriaTimePeriodsValidatorTests.ValidTimePeriodsSingle; - public static IEnumerable ValidTimePeriodStringsMultiple() - { - return - [ - ["2020|AY", "2020/2021|AY", "2020|FY"], - ["2021|M1", "2021|W40", "2021|T3"], - ["2021/2022|T3", "2019|FYQ4", "2019/2020|FYQ4"], - ]; - } + public static readonly TheoryData ValidTimePeriodQueriesMultiple = + DataSetQueryCriteriaTimePeriodsValidatorTests.ValidTimePeriodsMultiple; - public static IEnumerable InvalidTimePeriodStringsSingle() + public static readonly TheoryData InvalidTimePeriodFormatsSingle = new() { - return - [ - [""], - ["Invalid"], - ["2022"], - ["2022/2023"], - ["20222"], - ["2022|ay"], - ["2022/2020|AY"], - ["2000/1999|AY"], - ["2022|YY"], - ["2022|WEEK12"], - ]; - } - - public static IEnumerable InvalidTimePeriodStringsMultiple() + "", + "Invalid", + "2022", + "2022/2023", + "20222", + "|" + }; + + public static readonly TheoryData InvalidTimePeriodFormatsMultiple = new() { - return - [ - [], - ["", ""], - ["Invalid", "2022", "2022/2023"], - ["20222", "2022|ay"], - ["2022/2020|AY", "2000/1999|AY"], - ["2022|YY", "2022|WEEK12", "2022/2023|ZZ"] - ]; - } + new[] { "", " ", null! }, + new[] { "2022", "2022/2023" }, + new[] { "Invalid", "|" }, + }; + + public static readonly TheoryData InvalidTimePeriodQueriesSingle = + DataSetQueryCriteriaTimePeriodsValidatorTests.InvalidTimePeriodsSingle; + + public static readonly TheoryData InvalidTimePeriodQueriesMultiple = + DataSetQueryCriteriaTimePeriodsValidatorTests.InvalidTimePeriodsMultiple; public class EqTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsSingle))] - public void Success(string timePeriod) + [MemberData(nameof(ValidTimePeriodQueriesSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) { - var query = new DataSetGetQueryTimePeriods { Eq = timePeriod }; + var query = new DataSetGetQueryTimePeriods { Eq = timePeriod.ToTimePeriodString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsSingle))] - public void Failure_InvalidString(string timePeriod) + [MemberData(nameof(InvalidTimePeriodFormatsSingle))] + public void Failure_InvalidFormat(string timePeriod) { var query = new DataSetGetQueryTimePeriods { Eq = timePeriod }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.Eq); + .ShouldHaveValidationErrorFor(q => q.Eq) + .Only(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetGetQueryTimePeriods { Eq = timePeriod.ToTimePeriodString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Eq) + .Only(); } } public class NotEqTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsSingle))] - public void Success(string timePeriod) + [MemberData(nameof(ValidTimePeriodQueriesSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) { - var query = new DataSetGetQueryTimePeriods { NotEq = timePeriod }; + var query = new DataSetGetQueryTimePeriods { NotEq = timePeriod.ToTimePeriodString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsSingle))] - public void Failure_InvalidString(string timePeriod) + [MemberData(nameof(InvalidTimePeriodFormatsSingle))] + public void Failure_InvalidFormat(string timePeriod) { var query = new DataSetGetQueryTimePeriods { NotEq = timePeriod }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.NotEq); + .ShouldHaveValidationErrorFor(q => q.NotEq) + .Only(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetGetQueryTimePeriods { NotEq = timePeriod.ToTimePeriodString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.NotEq) + .Only(); } } public class InTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsMultiple))] - public void Success(params string[] timePeriods) + [MemberData(nameof(ValidTimePeriodQueriesMultiple))] + public void Success(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetGetQueryTimePeriods + { + In = [..timePeriods.Select(t => t.ToTimePeriodString())] + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() { - var testObj = new DataSetGetQueryTimePeriods { In = timePeriods }; + var query = new DataSetGetQueryTimePeriods { In = [] }; - _validator.TestValidate(testObj).ShouldNotHaveAnyValidationErrors(); + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsMultiple))] - public void Failure_InvalidStrings(params string[] timePeriods) + [MemberData(nameof(InvalidTimePeriodFormatsMultiple))] + public void Failure_InvalidFormats(string[] timePeriods) { var query = new DataSetGetQueryTimePeriods { In = timePeriods }; var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.In); + Assert.Equal(timePeriods.Length, result.Errors.Count); + + timePeriods.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"In[{index}]")); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesMultiple))] + public void Failure_InvalidQueries(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetGetQueryTimePeriods + { + In = [..timePeriods.Select(t => t.ToTimePeriodString())] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(timePeriods.Length, result.Errors.Count); timePeriods.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"In[{index}]")); } @@ -137,23 +164,47 @@ public void Failure_InvalidStrings(params string[] timePeriods) public class NotInTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsMultiple))] - public void Success(params string[] timePeriods) + [MemberData(nameof(ValidTimePeriodQueriesMultiple))] + public void Success(DataSetQueryTimePeriod[] timePeriods) { - var testObj = new DataSetGetQueryTimePeriods { NotIn = timePeriods }; + var query = new DataSetGetQueryTimePeriods { NotIn = [..timePeriods.Select(t => t.ToTimePeriodString())] }; - _validator.TestValidate(testObj).ShouldNotHaveAnyValidationErrors(); + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetGetQueryTimePeriods { NotIn = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsMultiple))] - public void Failure_InvalidStrings(params string[] timePeriods) + [MemberData(nameof(InvalidTimePeriodFormatsMultiple))] + public void Failure_InvalidFormats(string[] timePeriods) { var query = new DataSetGetQueryTimePeriods { NotIn = timePeriods }; var result = _validator.TestValidate(query); - result.ShouldHaveValidationErrorFor(q => q.NotIn); + Assert.Equal(timePeriods.Length, result.Errors.Count); + + timePeriods.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"NotIn[{index}]")); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesMultiple))] + public void Failure_InvalidQueries(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetGetQueryTimePeriods { NotIn = [..timePeriods.Select(t => t.ToTimePeriodString())] }; + + var result = _validator.TestValidate(query); + + Assert.Equal(timePeriods.Length, result.Errors.Count); timePeriods.ForEach((_, index) => result.ShouldHaveValidationErrorFor($"NotIn[{index}]")); } @@ -162,88 +213,139 @@ public void Failure_InvalidStrings(params string[] timePeriods) public class GtTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsSingle))] - public void Success(string timePeriod) + [MemberData(nameof(ValidTimePeriodQueriesSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) { - var query = new DataSetGetQueryTimePeriods { Gt = timePeriod }; + var query = new DataSetGetQueryTimePeriods { Gt = timePeriod.ToTimePeriodString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsSingle))] - public void Failure_InvalidString(string timePeriod) + [MemberData(nameof(InvalidTimePeriodFormatsSingle))] + public void Failure_InvalidFormat(string timePeriod) { var query = new DataSetGetQueryTimePeriods { Gt = timePeriod }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.Gt); + .ShouldHaveValidationErrorFor(q => q.Gt) + .Only(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetGetQueryTimePeriods { Gt = timePeriod.ToTimePeriodString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Gt) + .Only(); } } public class GteTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsSingle))] - public void Success(string timePeriod) + [MemberData(nameof(ValidTimePeriodQueriesSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) { - var query = new DataSetGetQueryTimePeriods { Gte = timePeriod }; + var query = new DataSetGetQueryTimePeriods { Gte = timePeriod.ToTimePeriodString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsSingle))] - public void Failure_InvalidString(string timePeriod) + [MemberData(nameof(InvalidTimePeriodFormatsSingle))] + public void Failure_InvalidFormat(string timePeriod) { var query = new DataSetGetQueryTimePeriods { Gte = timePeriod }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.Gte); + .ShouldHaveValidationErrorFor(q => q.Gte) + .Only(); + } + + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetGetQueryTimePeriods { Gte = timePeriod.ToTimePeriodString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Gte) + .Only(); } } public class LtTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsSingle))] - public void Success(string timePeriod) + [MemberData(nameof(ValidTimePeriodQueriesSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) { - var query = new DataSetGetQueryTimePeriods { Lt = timePeriod }; + var query = new DataSetGetQueryTimePeriods { Lt = timePeriod.ToTimePeriodString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsSingle))] - public void Failure_InvalidString(string timePeriod) + [MemberData(nameof(InvalidTimePeriodFormatsSingle))] + public void Failure_InvalidFormat(string timePeriod) { var query = new DataSetGetQueryTimePeriods { Lt = timePeriod }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.Lt); + .ShouldHaveValidationErrorFor(q => q.Lt) + .Only(); + } + + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetGetQueryTimePeriods { Lt = timePeriod.ToTimePeriodString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Lt) + .Only(); } } public class LteTests : DataSetGetQueryTimePeriodsValidatorTests { [Theory] - [MemberData(nameof(ValidTimePeriodStringsSingle))] - public void Success(string timePeriod) + [MemberData(nameof(ValidTimePeriodQueriesSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) { - var query = new DataSetGetQueryTimePeriods { Lte = timePeriod }; + var query = new DataSetGetQueryTimePeriods { Lte = timePeriod.ToTimePeriodString() }; _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); } [Theory] - [MemberData(nameof(InvalidTimePeriodStringsSingle))] - public void Failure_InvalidString(string timePeriod) + [MemberData(nameof(InvalidTimePeriodFormatsSingle))] + public void Failure_InvalidFormat(string timePeriod) { var query = new DataSetGetQueryTimePeriods { Lte = timePeriod }; _validator.TestValidate(query) - .ShouldHaveValidationErrorFor(q => q.Lte); + .ShouldHaveValidationErrorFor(q => q.Lte) + .Only(); + } + + + [Theory] + [MemberData(nameof(InvalidTimePeriodQueriesSingle))] + public void Failure_InvalidQuery(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetGetQueryTimePeriods { Lte = timePeriod.ToTimePeriodString() }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Lte) + .Only(); } } @@ -266,6 +368,8 @@ public void AllEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(8, result.Errors.Count); + result.ShouldHaveValidationErrorFor(q => q.Eq) .WithErrorCode(FluentValidationKeys.NotEmptyValidator); result.ShouldHaveValidationErrorFor(q => q.NotEq) @@ -301,6 +405,8 @@ public void SomeEmpty_Failure() var result = _validator.TestValidate(query); + Assert.Equal(4, result.Errors.Count); + result.ShouldNotHaveValidationErrorFor(q => q.Eq); result.ShouldNotHaveValidationErrorFor(q => q.In); result.ShouldNotHaveValidationErrorFor(q => q.Gt); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFiltersValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFiltersValidatorTests.cs new file mode 100644 index 00000000000..9c7920093bf --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFiltersValidatorTests.cs @@ -0,0 +1,257 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryCriteriaFiltersValidatorTests +{ + private readonly DataSetQueryCriteriaFilters.Validator _validator = new(); + + public static readonly TheoryData ValidFiltersSingle = new() + { + null, + "abc", + "12345", + "123456789", + "1234567890", + }; + + public static readonly TheoryData ValidFiltersMultiple = new() + { + new [] { "abc", "12345", "123456789" }, + new [] { "1234567890", "123", "abcde" }, + }; + + public class EqTests : DataSetQueryCriteriaFiltersValidatorTests + { + [Theory] + [MemberData(nameof(ValidFiltersSingle))] + public void Success(string? filter) + { + var query = new DataSetQueryCriteriaFilters { Eq = filter }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryCriteriaFilters { Eq = "" }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Eq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_MaxLength() + { + var query = new DataSetQueryCriteriaFilters { Eq = "12345678901" }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Eq) + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + } + } + + public class NotEqTests : DataSetQueryCriteriaFiltersValidatorTests + { + [Theory] + [MemberData(nameof(ValidFiltersSingle))] + public void Success(string? filter) + { + var query = new DataSetQueryCriteriaFilters { NotEq = filter }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryCriteriaFilters { NotEq = "" }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.NotEq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_MaxLength() + { + var query = new DataSetQueryCriteriaFilters { NotEq = "12345678901" }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.NotEq) + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + } + } + + public class InTests : DataSetQueryCriteriaFiltersValidatorTests + { + [Theory] + [MemberData(nameof(ValidFiltersMultiple))] + public void Success(params string[] filters) + { + var query = new DataSetQueryCriteriaFilters { In = filters }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Success_Null() + { + var query = new DataSetQueryCriteriaFilters { In = null }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_EmptyValues() + { + var query = new DataSetQueryCriteriaFilters + { + In = ["", " ", null!] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.In.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("In[0]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor("In[1]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor("In[2]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_MaxLengths() + { + var query = new DataSetQueryCriteriaFilters + { + In = ["12345678901", "999999999999999"] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.In.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("In[0]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + result.ShouldHaveValidationErrorFor("In[1]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + } + } + + public class NotInTests : DataSetQueryCriteriaFiltersValidatorTests + { + [Theory] + [MemberData(nameof(ValidFiltersMultiple))] + public void Success(params string[] filters) + { + var query = new DataSetQueryCriteriaFilters { NotIn = filters }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Success_Null() + { + var query = new DataSetQueryCriteriaFilters { NotIn = null }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_EmptyValues() + { + var query = new DataSetQueryCriteriaFilters + { + NotIn = ["", " ", null!] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.NotIn.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("NotIn[0]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor("NotIn[1]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor("NotIn[2]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_MaxLengths() + { + var query = new DataSetQueryCriteriaFilters + { + NotIn = ["12345678901", "999999999999999"] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.NotIn.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("NotIn[0]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + result.ShouldHaveValidationErrorFor("NotIn[1]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + } + } + + public class EmptyTests : DataSetQueryCriteriaFiltersValidatorTests + { + [Fact] + public void AllEmpty_Failure() + { + var query = new DataSetQueryCriteriaFilters + { + Eq = "", + NotEq = "", + In = [], + NotIn = [] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(4, result.Errors.Count); + + result.ShouldHaveValidationErrorFor(q => q.Eq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor(q => q.NotEq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void SomeEmpty_Failure() + { + var query = new DataSetQueryCriteriaFilters + { + Eq = "123", + NotEq = "", + In = ["456"], + NotIn = [] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldNotHaveValidationErrorFor(q => q.Eq); + result.ShouldNotHaveValidationErrorFor(q => q.In); + + result.ShouldHaveValidationErrorFor(q => q.NotEq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaGeographicLevelsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaGeographicLevelsValidatorTests.cs new file mode 100644 index 00000000000..8a7d5bf721e --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaGeographicLevelsValidatorTests.cs @@ -0,0 +1,243 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Validators; +using GovUk.Education.ExploreEducationStatistics.Common.Utils; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public abstract class DataSetQueryCriteriaGeographicLevelsValidatorTests +{ + private readonly DataSetQueryCriteriaGeographicLevels.Validator _validator = new(); + + public static readonly TheoryData ValidGeographicLevelsSingle = new() + { + "NAT", + "LA", + null, + }; + + public static readonly TheoryData ValidGeographicLevelsMultiple = new() + { + new [] { "NAT", "LA", "REG" }, + new [] { "SCH", "NAT" }, + new [] { "PROV" }, + }; + + public static readonly TheoryData InvalidGeographicLevelsSingle = new() + { + "", + " ", + "Invalid", + "National", + "nat", + "la", + "1", + }; + + public static readonly TheoryData InvalidGeographicLevelsMultiple = new() + { + new [] { "", " ", null! }, + new [] { "Invalid1", "Invalid2" }, + new [] { "National", "LocalAuthority" }, + new [] { "nat", "la" }, + new [] { "Local authority" }, + new [] { "National" }, + }; + + public class EqTests : DataSetQueryCriteriaGeographicLevelsValidatorTests + { + [Theory] + [MemberData(nameof(ValidGeographicLevelsSingle))] + public void Success(string? geographicLevel) + { + var query = new DataSetQueryCriteriaGeographicLevels { Eq = geographicLevel }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidGeographicLevelsSingle))] + public void Failure_NotAllowed(string geographicLevel) + { + var query = new DataSetQueryCriteriaGeographicLevels { Eq = geographicLevel }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(g => g.Eq) + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => s.Value == geographicLevel) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)) + .Only(); + } + } + + public class NotEqTests : DataSetQueryCriteriaGeographicLevelsValidatorTests + { + [Theory] + [MemberData(nameof(ValidGeographicLevelsSingle))] + public void Success(string? geographicLevel) + { + var query = new DataSetQueryCriteriaGeographicLevels { NotEq = geographicLevel }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidGeographicLevelsSingle))] + public void Failure_NotAllowed(string geographicLevel) + { + var query = new DataSetQueryCriteriaGeographicLevels { NotEq = geographicLevel }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(g => g.NotEq) + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => s.Value == geographicLevel) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)) + .Only(); + } + } + + public class InTests : DataSetQueryCriteriaGeographicLevelsValidatorTests + { + [Theory] + [MemberData(nameof(ValidGeographicLevelsMultiple))] + public void Success(params string[] geographicLevels) + { + var query = new DataSetQueryCriteriaGeographicLevels { In = geographicLevels }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Success_Null() + { + var query = new DataSetQueryCriteriaGeographicLevels { In = null }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidGeographicLevelsMultiple))] + public void Failure_NotAllowed(params string[] geographicLevels) + { + var query = new DataSetQueryCriteriaGeographicLevels { In = geographicLevels }; + + var result = _validator.TestValidate(query); + + Assert.Equal(geographicLevels.Length, result.Errors.Count); + + foreach (var (error, index) in result.Errors.WithIndex()) + { + result.ShouldHaveValidationErrorFor($"In[{index}]") + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => + s.Value == (string)error.AttemptedValue) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)); + } + } + } + + public class NotInTests : DataSetQueryCriteriaGeographicLevelsValidatorTests + { + [Theory] + [MemberData(nameof(ValidGeographicLevelsMultiple))] + public void Success(params string[] geographicLevels) + { + var query = new DataSetQueryCriteriaGeographicLevels { NotIn = geographicLevels }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Success_Null() + { + var query = new DataSetQueryCriteriaGeographicLevels { NotIn = null }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + + [Theory] + [MemberData(nameof(InvalidGeographicLevelsMultiple))] + public void Failure_NotAllowed(params string[] geographicLevels) + { + var query = new DataSetQueryCriteriaGeographicLevels { NotIn = geographicLevels }; + + var result = _validator.TestValidate(query); + + Assert.Equal(geographicLevels.Length, result.Errors.Count); + + foreach (var (error, index) in result.Errors.WithIndex()) + { + result.ShouldHaveValidationErrorFor($"NotIn[{index}]") + .WithErrorCode(ValidationMessages.AllowedValue.Code) + .WithErrorMessage(ValidationMessages.AllowedValue.Message) + .WithCustomState>(s => + s.Value == (string)error.AttemptedValue) + .WithCustomState>(s => + s.Allowed.SequenceEqual(GeographicLevelUtils.OrderedCodes)); + } + } + } + + public class EmptyTests : DataSetQueryCriteriaGeographicLevelsValidatorTests + { + [Fact] + public void AllEmpty_Failure() + { + var query = new DataSetQueryCriteriaGeographicLevels + { + Eq = "", + NotEq = "", + In = [], + NotIn = [] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(4, result.Errors.Count); + + result.ShouldHaveValidationErrorFor(q => q.Eq) + .WithErrorCode(ValidationMessages.AllowedValue.Code); + result.ShouldHaveValidationErrorFor(q => q.NotEq) + .WithErrorCode(ValidationMessages.AllowedValue.Code); + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void SomeEmpty_Failure() + { + var query = new DataSetQueryCriteriaGeographicLevels + { + Eq = "NAT", + NotEq = "", + In = ["LA"], + NotIn = [] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldNotHaveValidationErrorFor(q => q.Eq); + result.ShouldNotHaveValidationErrorFor(q => q.In); + + result.ShouldHaveValidationErrorFor(q => q.NotEq) + .WithErrorCode(ValidationMessages.AllowedValue.Code); + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaLocationsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaLocationsValidatorTests.cs new file mode 100644 index 00000000000..e424914b33e --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaLocationsValidatorTests.cs @@ -0,0 +1,262 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryCriteriaLocationsValidatorTests +{ + private readonly DataSetQueryCriteriaLocations.Validator _validator = new(); + + public static readonly TheoryData ValidLocationsSingle = new() + { + new DataSetQueryLocationId { Level = "NAT", Id = "12345" }, + new DataSetQueryLocationId { Level = "REG", Id = "12345" }, + new DataSetQueryLocationId { Level = "LA", Id = "12345" }, + new DataSetQueryLocationId { Level = "SCH", Id = "12345" }, + new DataSetQueryLocationId { Level = "PROV", Id = "12345" }, + new DataSetQueryLocationCode { Level = "NAT", Code = "E92000001" }, + new DataSetQueryLocationCode { Level = "REG", Code = "E12000003" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000019" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E09000021 / E09000027" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "373" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "314 / 318" }, + new DataSetQueryLocationSchoolUrn { Urn = "107029" }, + new DataSetQueryLocationSchoolLaEstab { LaEstab = "3732060" }, + new DataSetQueryLocationProviderUkprn { Ukprn = "10066874" }, + }; + + public static readonly TheoryData ValidLocationsMultiple = new() + { + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "NAT", Id = "12345" }, + new DataSetQueryLocationCode { Level = "NAT", Code = "E92000001" }, + }, + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "REG", Id = "12345" }, + new DataSetQueryLocationCode { Level = "REG", Code = "E12000003" }, + }, + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "LA", Id = "12345" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000019" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "373" }, + }, + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "SCH", Id = "12345" }, + new DataSetQueryLocationSchoolUrn { Urn = "107029" }, + new DataSetQueryLocationSchoolLaEstab { LaEstab = "3732060" }, + }, + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "PROV", Id = "12345" }, + new DataSetQueryLocationProviderUkprn { Ukprn = "10066874" }, + } + }; + + public static readonly TheoryData InvalidLocationsSingle = new() + { + new DataSetQueryLocationId { Level = "", Id = "" }, + new DataSetQueryLocationId { Level = "NAT", Id = "" }, + new DataSetQueryLocationCode { Level = "REG", Code = "" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "" }, + new DataSetQueryLocationId { Level = "NA", Id = "12345" }, + new DataSetQueryLocationId { Level = "la", Id = "12345" }, + new DataSetQueryLocationId { Level = "", Id = "12345" }, + new DataSetQueryLocationId { Level = "NAT", Id = new string('x', 26) }, + new DataSetQueryLocationCode { Level = "REG", Code = new string('x', 26) }, + new DataSetQueryLocationProviderUkprn { Ukprn = new string('x', 9) }, + new DataSetQueryLocationSchoolUrn { Urn = new string('x', 7) }, + }; + + public static readonly TheoryData InvalidLocationsMultiple = new() + { + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "", Id = "" }, + new DataSetQueryLocationId { Level = "NAT", Id = "" }, + new DataSetQueryLocationCode { Level = "REG", Code = "" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "" }, + }, + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "NA", Id = "12345" }, + new DataSetQueryLocationId { Level = "la", Id = "12345" }, + new DataSetQueryLocationId { Level = "", Id = "12345" }, + }, + new DataSetQueryLocation[] + { + new DataSetQueryLocationId { Level = "NAT", Id = new string('x', 26) }, + new DataSetQueryLocationCode { Level = "REG", Code = new string('x', 26) }, + new DataSetQueryLocationProviderUkprn { Ukprn = new string('x', 9) }, + new DataSetQueryLocationSchoolUrn { Urn = new string('x', 7) }, + }, + }; + + public class EqTests : DataSetQueryCriteriaLocationsValidatorTests + { + [Theory] + [MemberData(nameof(ValidLocationsSingle))] + public void Success(DataSetQueryLocation location) + { + var query = new DataSetQueryCriteriaLocations { Eq = location }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidLocationsSingle))] + public void Failure(DataSetQueryLocation location) + { + var query = new DataSetQueryCriteriaLocations { Eq = location }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("Eq", error.PropertyName)); + } + } + + public class NotEqTests : DataSetQueryCriteriaLocationsValidatorTests + { + [Theory] + [MemberData(nameof(ValidLocationsSingle))] + public void Success(DataSetQueryLocation location) + { + var query = new DataSetQueryCriteriaLocations { NotEq = location }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidLocationsSingle))] + public void Failure(DataSetQueryLocation location) + { + var query = new DataSetQueryCriteriaLocations { NotEq = location }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("NotEq", error.PropertyName)); + } + } + + public class InTests : DataSetQueryCriteriaLocationsValidatorTests + { + [Theory] + [MemberData(nameof(ValidLocationsMultiple))] + public void Success(params DataSetQueryLocation[] locations) + { + var query = new DataSetQueryCriteriaLocations { In = locations }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidLocationsMultiple))] + public void Failure(DataSetQueryLocation[] locations) + { + var query = new DataSetQueryCriteriaLocations { In = locations }; + + var result = _validator.TestValidate(query); + + Assert.True( + result.Errors.Count >= locations.Length, + "Must have at least as many errors as locations"); + + Assert.All(result.Errors, error => + Assert.StartsWith("In", error.PropertyName)); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryCriteriaLocations { In = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } + + public class NotInTests : DataSetQueryCriteriaLocationsValidatorTests + { + [Theory] + [MemberData(nameof(ValidLocationsMultiple))] + public void Success(params DataSetQueryLocation[] locations) + { + var query = new DataSetQueryCriteriaLocations { NotIn = locations }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidLocationsMultiple))] + public void Failure(params DataSetQueryLocation[] locations) + { + var query = new DataSetQueryCriteriaLocations { NotIn = locations }; + + var result = _validator.TestValidate(query); + + Assert.True( + result.Errors.Count >= locations.Length, + "Must have at least as many errors as locations"); + + Assert.All(result.Errors, error => + Assert.StartsWith("NotIn", error.PropertyName)); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryCriteriaLocations { NotIn = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } + + public class EmptyTests : DataSetQueryCriteriaLocationsValidatorTests + { + [Fact] + public void AllNull_Success() + { + var query = new DataSetQueryCriteriaLocations(); + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void SomeEmpty_Failure() + { + var query = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "NAT", Id = "12345" }, + NotEq = null, + In = [new DataSetQueryLocationCode { Level = "REG", Code = "12345" }], + NotIn = [] + }; + + var result = _validator.TestValidate(query); + + Assert.Single(result.Errors); + + result.ShouldNotHaveValidationErrorFor(q => q.Eq); + result.ShouldNotHaveValidationErrorFor(q => q.In); + result.ShouldNotHaveValidationErrorFor(q => q.NotEq); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaTimePeriodsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaTimePeriodsValidatorTests.cs new file mode 100644 index 00000000000..ef60a1e5024 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaTimePeriodsValidatorTests.cs @@ -0,0 +1,357 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryCriteriaTimePeriodsValidatorTests +{ + private readonly DataSetQueryCriteriaTimePeriods.Validator _validator = new(); + + public static readonly TheoryData ValidTimePeriodsSingle = new() + { + new DataSetQueryTimePeriod { Period = "2020", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2020", Code = "FY" }, + new DataSetQueryTimePeriod { Period = "2021", Code = "M1" }, + new DataSetQueryTimePeriod { Period = "2021", Code = "W40" }, + new DataSetQueryTimePeriod { Period = "2021", Code = "T3" }, + new DataSetQueryTimePeriod { Period = "2021/2022", Code = "T3" }, + new DataSetQueryTimePeriod { Period = "2019", Code = "FYQ4" }, + new DataSetQueryTimePeriod { Period = "2019/2020", Code = "FYQ4" }, + }; + + public static readonly TheoryData ValidTimePeriodsMultiple = new() + { + new DataSetQueryTimePeriod[] + { + new() { Period = "2020", Code = "AY" }, + new() { Period = "2020/2021", Code = "AY" }, + new() { Period = "2020", Code = "FY" }, + }, + new DataSetQueryTimePeriod[] + { + new() { Period = "2021", Code = "M1" }, + new() { Period = "2021", Code = "W40" }, + new() { Period = "2021", Code = "T3" }, + }, + new DataSetQueryTimePeriod[] + { + new() { Period = "2021/2022", Code = "T3" }, + new() { Period = "2019", Code = "FYQ4" }, + new() { Period = "2019/2020", Code = "FYQ4" }, + }, + }; + + public static readonly TheoryData InvalidTimePeriodsSingle = new() + { + new DataSetQueryTimePeriod { Period = "", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "Invalid", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2020/2022", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022/2020", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2000/1999", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022", Code = "" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "" }, + new DataSetQueryTimePeriod { Period = "2022", Code = "ay" }, + new DataSetQueryTimePeriod { Period = "2022", Code = "Invalid" }, + new DataSetQueryTimePeriod { Period = "2022", Code = "1" }, + new DataSetQueryTimePeriod { Period = "2022", Code = "WEEK12" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "JANUARY" }, + }; + + public static readonly TheoryData InvalidTimePeriodsMultiple = new() + { + new DataSetQueryTimePeriod[] + { + new() { Period = "", Code = "AY" }, + new() { Period = "Invalid", Code = "AY" }, + new() { Period = "2020/2022", Code = "AY" }, + new() { Period = "2022/2020", Code = "AY" }, + new() { Period = "2000/1999", Code = "AY" }, + }, + new DataSetQueryTimePeriod[] + { + new() { Period = "2022", Code = "" }, + new() { Period = "2022/2023", Code = "" }, + new() { Period = "2022", Code = "ay" }, + new() { Period = "2022", Code = "Invalid" }, + new() { Period = "2022", Code = "1" }, + new() { Period = "2022", Code = "WEEK12" }, + new() { Period = "2022/2023", Code = "JANUARY" }, + }, + }; + + public class EqTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Eq = timePeriod }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsSingle))] + public void Failure(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Eq = timePeriod }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("Eq", error.PropertyName)); + } + } + + public class NotEqTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { NotEq = timePeriod }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsSingle))] + public void Failure(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { NotEq = timePeriod }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("NotEq", error.PropertyName)); + } + } + + public class InTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsMultiple))] + public void Success(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetQueryCriteriaTimePeriods { In = timePeriods }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsMultiple))] + public void Failure(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetQueryCriteriaTimePeriods { In = timePeriods }; + + var result = _validator.TestValidate(query); + + Assert.Equal(timePeriods.Length, result.Errors.Count); + + Assert.All(result.Errors, error => + Assert.StartsWith("In", error.PropertyName)); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryCriteriaTimePeriods { In = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.In) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } + + public class NotInTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsMultiple))] + public void Success(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetQueryCriteriaTimePeriods { NotIn = timePeriods }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsMultiple))] + public void Failure(DataSetQueryTimePeriod[] timePeriods) + { + var query = new DataSetQueryCriteriaTimePeriods { NotIn = timePeriods }; + + var result = _validator.TestValidate(query); + + Assert.Equal(timePeriods.Length, result.Errors.Count); + + Assert.All(result.Errors, error => + Assert.StartsWith("NotIn", error.PropertyName)); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryCriteriaTimePeriods { NotIn = [] }; + + var result = _validator.TestValidate(query); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } + + public class GtTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Gt = timePeriod }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsSingle))] + public void Failure(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Gt = timePeriod }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("Gt", error.PropertyName)); + } + } + + public class GteTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Gte = timePeriod }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsSingle))] + public void Failure(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Gte = timePeriod }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("Gte", error.PropertyName)); + } + } + + public class LtTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Lt = timePeriod }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsSingle))] + public void Failure(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Lt = timePeriod }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("Lt", error.PropertyName)); + } + } + + public class LteTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Theory] + [MemberData(nameof(ValidTimePeriodsSingle))] + public void Success(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Lte = timePeriod }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [MemberData(nameof(InvalidTimePeriodsSingle))] + public void Failure(DataSetQueryTimePeriod timePeriod) + { + var query = new DataSetQueryCriteriaTimePeriods { Lte = timePeriod }; + + var result = _validator.TestValidate(query); + + Assert.NotEmpty(result.Errors); + + Assert.All(result.Errors, error => + Assert.StartsWith("Lte", error.PropertyName)); + } + } + + public class EmptyTests : DataSetQueryCriteriaTimePeriodsValidatorTests + { + [Fact] + public void AllNull_Success() + { + var query = new DataSetQueryCriteriaTimePeriods(); + + var result = _validator.TestValidate(query); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void SomeEmpty_Failure() + { + var query = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2020", Code = "AY" }, + NotEq = null, + In = [new DataSetQueryTimePeriod { Period = "2021/2022", Code = "T3" }], + NotIn = [], + Gt = new DataSetQueryTimePeriod { Period = "2019", Code = "M4" }, + Gte = null, + Lt = null, + Lte = new DataSetQueryTimePeriod { Period = "2030", Code = "M7" } + }; + + var result = _validator.TestValidate(query); + + Assert.Single(result.Errors); + + result.ShouldNotHaveValidationErrorFor(q => q.Eq); + result.ShouldNotHaveValidationErrorFor(q => q.NotEq); + result.ShouldNotHaveValidationErrorFor(q => q.In); + result.ShouldNotHaveValidationErrorFor(q => q.Gt); + result.ShouldNotHaveValidationErrorFor(q => q.Gte); + result.ShouldNotHaveValidationErrorFor(q => q.Lt); + result.ShouldNotHaveValidationErrorFor(q => q.Lte); + + result.ShouldHaveValidationErrorFor(q => q.NotIn) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryFilters.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryFilters.cs index 04486834aab..dea7e934e40 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryFilters.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryFilters.cs @@ -1,32 +1,21 @@ -using FluentValidation; using GovUk.Education.ExploreEducationStatistics.Common.ModelBinding; using Microsoft.AspNetCore.Mvc; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; -public record DataSetGetQueryFilters +public record DataSetGetQueryFilters : DataSetQueryCriteriaFilters { - /// - /// Filter the results to have a filter option matching this ID. - /// - public string? Eq { get; init; } - - /// - /// Filter the results to not have a filter option matching this ID. - /// - public string? NotEq { get; init; } - /// /// Filter the results to have a filter option matching at least one of these IDs. /// [FromQuery, QuerySeparator] - public IReadOnlyList? In { get; init; } + public override IReadOnlyList? In { get; init; } /// /// Filter the results to not have a filter option matching any of these IDs. /// [FromQuery, QuerySeparator] - public IReadOnlyList? NotIn { get; init; } + public override IReadOnlyList? NotIn { get; init; } public DataSetQueryCriteriaFilters ToCriteria() { @@ -39,39 +28,5 @@ public DataSetQueryCriteriaFilters ToCriteria() }; } - public class Validator : AbstractValidator - { - public Validator() - { - RuleFor(q => q.Eq) - .NotEmpty() - .MaximumLength(10) - .When(q => q.Eq is not null); - - RuleFor(q => q.NotEq) - .NotEmpty() - .MaximumLength(10) - .When(q => q.NotEq is not null); - - When(q => q.In is not null, () => - { - RuleFor(q => q.In) - .NotEmpty(); - - RuleForEach(q => q.In) - .NotEmpty() - .MaximumLength(10); - }); - - When(q => q.NotIn is not null, () => - { - RuleFor(q => q.NotIn) - .NotEmpty(); - - RuleForEach(q => q.NotIn) - .NotEmpty() - .MaximumLength(10); - }); - } - } + public new class Validator : DataSetQueryCriteriaFilters.Validator; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryGeographicLevels.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryGeographicLevels.cs index 28073c7901f..90078828cfe 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryGeographicLevels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetGetQueryGeographicLevels.cs @@ -1,34 +1,19 @@ -using FluentValidation; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.ModelBinding; -using GovUk.Education.ExploreEducationStatistics.Common.Utils; -using GovUk.Education.ExploreEducationStatistics.Common.Validators; using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Swagger; using Microsoft.AspNetCore.Mvc; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; -public record DataSetGetQueryGeographicLevels +public record DataSetGetQueryGeographicLevels : DataSetQueryCriteriaGeographicLevels { - /// - /// Filter the results to be in this geographic level. - /// - [SwaggerEnum(typeof(GeographicLevel))] - public string? Eq { get; init; } - - /// - /// Filter the results to not be in this geographic level. - /// - [SwaggerEnum(typeof(GeographicLevel))] - public string? NotEq { get; init; } - /// /// Filter the results to be in one of these geographic levels. /// [FromQuery] [QuerySeparator] [SwaggerEnum(typeof(GeographicLevel))] - public IReadOnlyList? In { get; init; } + public override IReadOnlyList? In { get; init; } /// /// Filter the results to not be in one of these geographic levels. @@ -36,7 +21,7 @@ public record DataSetGetQueryGeographicLevels [FromQuery] [QuerySeparator] [SwaggerEnum(typeof(GeographicLevel))] - public IReadOnlyList? NotIn { get; init; } + public override IReadOnlyList? NotIn { get; init; } public DataSetQueryCriteriaGeographicLevels ToCriteria() { @@ -49,33 +34,5 @@ public DataSetQueryCriteriaGeographicLevels ToCriteria() }; } - public class Validator : AbstractValidator - { - public Validator() - { - RuleFor(q => q.Eq) - .AllowedValue(GeographicLevelUtils.OrderedCodes) - .When(q => q.Eq is not null); - - RuleFor(q => q.NotEq) - .AllowedValue(GeographicLevelUtils.OrderedCodes) - .When(q => q.NotEq is not null); - - When(q => q.In is not null, () => - { - RuleFor(q => q.In) - .NotEmpty(); - RuleForEach(q => q.In) - .AllowedValue(GeographicLevelUtils.OrderedCodes); - }); - - When(q => q.NotIn is not null, () => - { - RuleFor(q => q.NotIn) - .NotEmpty(); - RuleForEach(q => q.NotIn) - .AllowedValue(GeographicLevelUtils.OrderedCodes); - }); - } - } + public new class Validator : DataSetQueryCriteriaGeographicLevels.Validator; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs index 019c8a89a10..dfece491c70 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs @@ -1,3 +1,5 @@ +using FluentValidation; + namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; /// @@ -18,12 +20,12 @@ public record DataSetQueryCriteriaFilters /// /// Filter the results to have a filter option matching at least one of these IDs. /// - public IReadOnlyList? In { get; init; } + public virtual IReadOnlyList? In { get; init; } /// /// Filter the results to not have a filter option matching any of these IDs. /// - public IReadOnlyList? NotIn { get; init; } + public virtual IReadOnlyList? NotIn { get; init; } public HashSet GetOptions() { @@ -39,4 +41,40 @@ public HashSet GetOptions() .OfType() .ToHashSet(); } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.Eq) + .NotEmpty() + .MaximumLength(10) + .When(q => q.Eq is not null); + + RuleFor(q => q.NotEq) + .NotEmpty() + .MaximumLength(10) + .When(q => q.NotEq is not null); + + When(q => q.In is not null, () => + { + RuleFor(q => q.In) + .NotEmpty(); + + RuleForEach(q => q.In) + .NotEmpty() + .MaximumLength(10); + }); + + When(q => q.NotIn is not null, () => + { + RuleFor(q => q.NotIn) + .NotEmpty(); + + RuleForEach(q => q.NotIn) + .NotEmpty() + .MaximumLength(10); + }); + } + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs index b9a6fc60c27..9f29d24ebc4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs @@ -1,6 +1,8 @@ -using System.Text.Json.Serialization; +using FluentValidation; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Utils; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Swagger; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; @@ -12,51 +14,81 @@ public record DataSetQueryCriteriaGeographicLevels /// /// Filter the results to be in this geographic level. /// + [SwaggerEnum(typeof(GeographicLevel))] public string? Eq { get; init; } /// /// Filter the results to not be in this geographic level. /// + [SwaggerEnum(typeof(GeographicLevel))] public string? NotEq { get; init; } /// /// Filter the results to be in one of these geographic levels. /// - public IReadOnlyList? In { get; init; } + [SwaggerEnum(typeof(GeographicLevel))] + public virtual IReadOnlyList? In { get; init; } /// /// Filter the results to not be in one of these geographic levels. /// - public IReadOnlyList? NotIn { get; init; } + [SwaggerEnum(typeof(GeographicLevel))] + public virtual IReadOnlyList? NotIn { get; init; } - [JsonIgnore] - public GeographicLevel? ParsedEq + public GeographicLevel? ParsedEq() => Eq is not null ? EnumUtil.GetFromEnumValue(Eq) : null; - [JsonIgnore] - public GeographicLevel? ParsedNotEq + public GeographicLevel? ParsedNotEq() => NotEq is not null ? EnumUtil.GetFromEnumValue(NotEq) : null; - [JsonIgnore] - public IReadOnlyList? ParsedIn + public IReadOnlyList? ParsedIn() => In?.Select(EnumUtil.GetFromEnumValue).ToList(); - [JsonIgnore] - public IReadOnlyList? ParsedNotIn + public IReadOnlyList? ParsedNotIn() => NotIn?.Select(EnumUtil.GetFromEnumValue).ToList(); public HashSet GetOptions() { List options = [ - ParsedEq, - ParsedNotEq, - ..ParsedIn ?? [], - ..ParsedNotIn ?? [] + ParsedEq(), + ParsedNotEq(), + ..ParsedIn() ?? [], + ..ParsedNotIn() ?? [] ]; return options .OfType() .ToHashSet(); } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.Eq) + .AllowedValue(GeographicLevelUtils.OrderedCodes) + .When(q => q.Eq is not null); + + RuleFor(q => q.NotEq) + .AllowedValue(GeographicLevelUtils.OrderedCodes) + .When(q => q.NotEq is not null); + + When(q => q.In is not null, () => + { + RuleFor(q => q.In) + .NotEmpty(); + RuleForEach(q => q.In) + .AllowedValue(GeographicLevelUtils.OrderedCodes); + }); + + When(q => q.NotIn is not null, () => + { + RuleFor(q => q.NotIn) + .NotEmpty(); + RuleForEach(q => q.NotIn) + .AllowedValue(GeographicLevelUtils.OrderedCodes); + }); + } + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs index caee2994256..3797c3ae508 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs @@ -1,3 +1,6 @@ +using FluentValidation; +using FluentValidation.Validators; + namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; /// @@ -49,4 +52,46 @@ public HashSet GetOptions() .OfType() .ToHashSet(); } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(request => request.Eq) + .SetInheritanceValidator(InheritanceValidator!) + .When(request => request.Eq is not null); + + RuleFor(request => request.NotEq) + .SetInheritanceValidator(InheritanceValidator!) + .When(request => request.NotEq is not null); + + When(q => q.In is not null, () => + { + RuleFor(request => request.In) + .NotEmpty(); + RuleForEach(request => request.In) + .SetInheritanceValidator(InheritanceValidator); + }); + + When(q => q.NotIn is not null, () => + { + RuleFor(request => request.NotIn) + .NotEmpty(); + RuleForEach(request => request.NotIn) + .SetInheritanceValidator(InheritanceValidator); + }); + } + + private static void InheritanceValidator( + PolymorphicValidator validator) + { + validator.Add(new DataSetQueryLocationId.Validator()); + validator.Add(new DataSetQueryLocationCode.Validator()); + validator.Add(new DataSetQueryLocationLocalAuthorityCode.Validator()); + validator.Add(new DataSetQueryLocationLocalAuthorityOldCode.Validator()); + validator.Add(new DataSetQueryLocationProviderUkprn.Validator()); + validator.Add(new DataSetQueryLocationSchoolLaEstab.Validator()); + validator.Add(new DataSetQueryLocationSchoolUrn.Validator()); + } + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs index 73e70566cea..99a0c353cb9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs @@ -1,3 +1,5 @@ +using FluentValidation; + namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; /// @@ -67,4 +69,50 @@ public HashSet GetOptions() .OfType() .ToHashSet(); } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(request => request.Eq) + .SetValidator(new DataSetQueryTimePeriod.Validator()!) + .When(request => request.Eq is not null); + + RuleFor(request => request.NotEq)! + .SetValidator(new DataSetQueryTimePeriod.Validator()!) + .When(request => request.NotEq is not null); + + When(q => q.In is not null, () => + { + RuleFor(request => request.In) + .NotEmpty(); + RuleForEach(request => request.In) + .SetValidator(new DataSetQueryTimePeriod.Validator()); + }); + + When(q => q.NotIn is not null, () => + { + RuleFor(request => request.NotIn) + .NotEmpty(); + RuleForEach(request => request.NotIn) + .SetValidator(new DataSetQueryTimePeriod.Validator()); + }); + + RuleFor(request => request.Gt) + .SetValidator(new DataSetQueryTimePeriod.Validator()!) + .When(request => request.Gt is not null); + + RuleFor(request => request.Gte) + .SetValidator(new DataSetQueryTimePeriod.Validator()!) + .When(request => request.Gte is not null); + + RuleFor(request => request.Lt) + .SetValidator(new DataSetQueryTimePeriod.Validator()!) + .When(request => request.Lt is not null); + + RuleFor(request => request.Lte) + .SetValidator(new DataSetQueryTimePeriod.Validator()!) + .When(request => request.Lte is not null); + } + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Query/GeographicLevelFacetsParser.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Query/GeographicLevelFacetsParser.cs index 68ee45f4605..f56d59a6b67 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Query/GeographicLevelFacetsParser.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Query/GeographicLevelFacetsParser.cs @@ -20,7 +20,7 @@ public IInterpolatedSql Parse(DataSetQueryCriteriaFacets facets, string path) { var fragments = new List(); - var parsedEq = facets.GeographicLevels?.ParsedEq; + var parsedEq = facets.GeographicLevels?.ParsedEq(); if (parsedEq is not null) { @@ -32,7 +32,7 @@ public IInterpolatedSql Parse(DataSetQueryCriteriaFacets facets, string path) ); } - var parsedNotEq = facets.GeographicLevels?.ParsedNotEq; + var parsedNotEq = facets.GeographicLevels?.ParsedNotEq(); if (parsedNotEq is not null) { @@ -45,7 +45,7 @@ public IInterpolatedSql Parse(DataSetQueryCriteriaFacets facets, string path) ); } - var parsedIn = facets.GeographicLevels?.ParsedIn; + var parsedIn = facets.GeographicLevels?.ParsedIn(); if (parsedIn is not null && parsedIn.Count != 0) { @@ -57,7 +57,7 @@ public IInterpolatedSql Parse(DataSetQueryCriteriaFacets facets, string path) ); } - var parsedNotIn = facets.GeographicLevels?.ParsedNotIn; + var parsedNotIn = facets.GeographicLevels?.ParsedNotIn(); if (parsedNotIn is not null && parsedNotIn.Count != 0) { From 7e0389925d3cd47d4480824153c971396cb3037c Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Tue, 30 Apr 2024 02:01:46 +0100 Subject: [PATCH 24/66] EES-4722 Add data set query criteria condition models and validators --- .../DataSetQueryCriteriaAndValidatorTests.cs | 390 ++++++++++++++++ ...ataSetQueryCriteriaFacetsValidatorTests.cs | 210 +++++++++ .../DataSetQueryCriteriaNotValidatorTests.cs | 286 ++++++++++++ .../DataSetQueryCriteriaOrValidatorTests.cs | 392 ++++++++++++++++ .../DataSetQueryRequestValidatorTests.cs | 439 ++++++++++++++++++ .../DataSetQueryCriteriaJsonConverter.cs | 83 ++++ .../Requests/DataSetQueryCriteria.cs | 18 +- .../Requests/DataSetQueryCriteriaAnd.cs | 30 ++ .../Requests/DataSetQueryCriteriaFacets.cs | 26 +- .../Requests/DataSetQueryCriteriaNot.cs | 27 ++ .../Requests/DataSetQueryCriteriaOr.cs | 30 ++ .../Requests/DataSetQueryRequest.cs | 36 ++ 12 files changed, 1965 insertions(+), 2 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaAndValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFacetsValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaNotValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaOrValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryRequestValidatorTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryCriteriaJsonConverter.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaAnd.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaNot.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaOr.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaAndValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaAndValidatorTests.cs new file mode 100644 index 00000000000..38b60614775 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaAndValidatorTests.cs @@ -0,0 +1,390 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryCriteriaAndValidatorTests +{ + private readonly DataSetQueryCriteriaAnd.Validator _validator = new(); + + [Fact] + public void Empty_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = [], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.And) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + + [Fact] + public void Nulls_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + null!, + new DataSetQueryCriteriaFacets() + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("And[0]") + .WithErrorCode(FluentValidationKeys.NotNullValidator) + .Only(); + } + + [Fact] + public void SingleFacets_Success() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "" + }, + }, + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("And[0].Filters.Eq"); + } + + [Fact] + public void MultipleFacets_Success() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + } + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + NotEq = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void MultipleFacets_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "" + }, + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "Invalid" } + }, + }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("And[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("And[1].TimePeriods.Gte.Code"); + } + + [Fact] + public void SingleCondition_Empty_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaOr + { + Or = [], + }, + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("And[0].Or") + .Only(); + } + + [Fact] + public void SingleCondition_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void SingleCondition_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("And[0].Or[0].Filters.Eq") + .Only(); + } + + [Fact] + public void MultipleConditions_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }, + new DataSetQueryCriteriaNot + { + Not =new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + In = [new DataSetQueryLocationId { Level = "NAT", Id = "12345" }], + }, + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void MultipleConditions_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "", Code = "AY" } + }, + }, + ], + }, + new DataSetQueryCriteriaNot + { + Not =new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + In = [new DataSetQueryLocationId { Level = "nat", Id = "12345" }], + }, + }, + }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(3, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("And[0].And[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("And[1].Or[0].TimePeriods.Gte.Period"); + result.ShouldHaveValidationErrorFor("And[2].Not.Locations.In[0].Level"); + } + + [Fact] + public void MixedCriteria_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void MixedCriteria_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "", Code = "AY" } + }, + }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("And[0].And[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("And[1].TimePeriods.Gte.Period"); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFacetsValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFacetsValidatorTests.cs new file mode 100644 index 00000000000..8424653e51a --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaFacetsValidatorTests.cs @@ -0,0 +1,210 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; +using ValidationMessages = GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Validators.ValidationMessages; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public abstract class DataSetQueryCriteriaFacetsValidatorTests +{ + private readonly DataSetQueryCriteriaFacets.Validator _validator = new(); + + public class FiltersTests : DataSetQueryCriteriaFacetsValidatorTests + { + [Fact] + public void Success() + { + var facets = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "abc" + }, + }; + + _validator.TestValidate(facets).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure() + { + var facets = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "" + }, + }; + + _validator.TestValidate(facets) + .ShouldHaveValidationErrorFor(f => f.Filters!.Eq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } + + public class GeographicLevelsTests : DataSetQueryCriteriaFacetsValidatorTests + { + [Fact] + public void Success() + { + var facets = new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetGetQueryGeographicLevels + { + Eq = "NAT" + }, + }; + + _validator.TestValidate(facets).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure() + { + var facets = new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetGetQueryGeographicLevels + { + Eq = "Invalid" + }, + }; + + _validator.TestValidate(facets) + .ShouldHaveValidationErrorFor(f => f.GeographicLevels!.Eq) + .WithErrorCode(Common.Validators.ValidationMessages.AllowedValue.Code); + } + } + + public class LocationsTests : DataSetQueryCriteriaFacetsValidatorTests + { + [Fact] + public void Success() + { + var facets = new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "NAT", Id = "12345" }, + }, + }; + + _validator.TestValidate(facets).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure() + { + var facets = new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "Invalid", Id = "12345" }, + }, + }; + + _validator.TestValidate(facets) + .ShouldHaveValidationErrorFor(f => f.Locations!.Eq!.Level) + .WithErrorCode(Common.Validators.ValidationMessages.AllowedValue.Code); + } + } + + public class TimePeriodsTests : DataSetQueryCriteriaFacetsValidatorTests + { + [Fact] + public void Success() + { + var facets = new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" }, + }, + }; + + _validator.TestValidate(facets).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure() + { + var facets = new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "Invalid" } + }, + }; + + _validator.TestValidate(facets) + .ShouldHaveValidationErrorFor(f => f.TimePeriods!.Eq!.Code) + .WithErrorCode(ValidationMessages.TimePeriodAllowedCode.Code); + } + } + + public class MixtureTests : DataSetQueryCriteriaFacetsValidatorTests + { + [Fact] + public void Success() + { + var facets = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "abc" + }, + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "NAT", Id = "12345" }, + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + Eq = "NAT" + }, + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" }, + }, + }; + + _validator.TestValidate(facets).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure() + { + var facets = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "" + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + Eq = "Invalid" + }, + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "REG", Id = new string('x', 11) }, + }, + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2020/2018", Code = "AY" }, + }, + }; + + var result = _validator.TestValidate(facets); + + result.ShouldHaveValidationErrorFor(f => f.Filters!.Eq) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + + result.ShouldHaveValidationErrorFor(f => f.GeographicLevels!.Eq) + .WithErrorCode(Common.Validators.ValidationMessages.AllowedValue.Code); + + result.ShouldHaveValidationErrorFor("Locations.Eq.Id") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + + result.ShouldHaveValidationErrorFor(f => f.TimePeriods!.Eq!.Period) + .WithErrorCode(ValidationMessages.TimePeriodInvalidYearRange.Code); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaNotValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaNotValidatorTests.cs new file mode 100644 index 00000000000..378fc43be2a --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaNotValidatorTests.cs @@ -0,0 +1,286 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryCriteriaNotValidatorTests +{ + private readonly DataSetQueryCriteriaNot.Validator _validator = new(); + + [Fact] + public void Null_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = null! + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Not") + .WithErrorCode(FluentValidationKeys.NotNullValidator) + .Only(); + } + + [Fact] + public void Facets_Success() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Facets_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "" + }, + }, + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Not.Filters.Eq") + .Only(); + } + + [Fact] + public void Condition_Empty_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaOr + { + Or = [], + }, + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Not.Or") + .Only(); + } + + [Fact] + public void Condition_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Condition_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Not.Or[0].Filters.Eq") + .Only(); + } + + [Fact] + public void Condition_MultipleFacets_Success() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Condition_MultipleFacets_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "", Code = "AY" } + }, + }, + ], + }, + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Not.And[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("Not.And[1].TimePeriods.Gte.Period"); + } + + [Fact] + public void Not_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + }, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + + [Fact] + public void Not_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + }, + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Not.Not.Filters.Eq") + .Only(); + } + + [Fact] + public void Condition_Not_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + }, + ], + } + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Condition_Not_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + }, + ], + } + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Not.And[0].Not.Filters.Eq") + .Only(); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaOrValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaOrValidatorTests.cs new file mode 100644 index 00000000000..28197302420 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryCriteriaOrValidatorTests.cs @@ -0,0 +1,392 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryCriteriaOrValidatorTests +{ + private readonly DataSetQueryCriteriaOr.Validator _validator = new(); + + [Fact] + public void Empty_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = [], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Or) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + + [Fact] + public void Nulls_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + null!, + new DataSetQueryCriteriaFacets() + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Or[0]") + .WithErrorCode(FluentValidationKeys.NotNullValidator) + .Only(); + } + + [Fact] + public void SingleFacets_Success() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "" + }, + }, + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Or[0].Filters.Eq") + .Only(); + } + + [Fact] + public void MultipleFacets_Success() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + } + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + NotEq = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + + [Fact] + public void MultipleFacets_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "" + }, + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "Invalid" } + }, + }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Or[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("Or[1].TimePeriods.Gte.Code"); + } + + [Fact] + public void SingleCondition_Empty_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaOr + { + Or = [], + }, + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Or[0].Or") + .Only(); + } + + [Fact] + public void SingleCondition_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void SingleCondition_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + ], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Or[0].Or[0].Filters.Eq") + .Only(); + } + + [Fact] + public void MultipleConditions_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + In = [new DataSetQueryLocationId { Level = "NAT", Id = "12345" }], + }, + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void MultipleConditions_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "", Code = "AY" } + }, + }, + ], + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + In = [new DataSetQueryLocationId { Level = "nat", Id = "12345" }], + }, + }, + }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(3, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Or[0].And[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("Or[1].Or[0].TimePeriods.Gte.Period"); + result.ShouldHaveValidationErrorFor("Or[2].Not.Locations.In[0].Level"); + } + + [Fact] + public void MixedCriteria_SingleFacets_Success() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "12345" + }, + }, + ], + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" } + }, + }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void MixedCriteria_SingleFacets_Failure() + { + var query = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('x', 11) + }, + }, + ], + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gte = new DataSetQueryTimePeriod { Period = "", Code = "AY" } + }, + }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(2, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Or[0].And[0].Filters.Eq"); + result.ShouldHaveValidationErrorFor("Or[1].TimePeriods.Gte.Period"); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryRequestValidatorTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryRequestValidatorTests.cs new file mode 100644 index 00000000000..ffb2fa6e0f2 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Requests/DataSetQueryRequestValidatorTests.cs @@ -0,0 +1,439 @@ +using FluentValidation.TestHelper; +using GovUk.Education.ExploreEducationStatistics.Common.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Requests; + +public class DataSetQueryRequestValidatorTests +{ + private readonly DataSetQueryRequest.Validator _validator = new(); + + public class CriteriaTests : DataSetQueryRequestValidatorTests + { + [Fact] + public void Facets_Success() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "abc" + }, + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Facets_Failure() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "" + }, + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Criteria.Filters.Eq") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + + [Fact] + public void AndCondition_Success() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "abc" + }, + } + ] + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void AndCondition_Failure() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "" + }, + } + ] + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Criteria.And[0].Filters.Eq") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + + [Fact] + public void OrCondition_Success() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "abc" + }, + } + ] + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void OrCondition_Failure() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "" + }, + } + ] + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Criteria.Or[0].Filters.Eq") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + + [Fact] + public void NotCondition_Success() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "abc" + }, + } + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void NotCondition_Failure() + { + var query = new DataSetQueryRequest + { + Criteria = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetGetQueryFilters + { + Eq = "" + }, + } + }, + Indicators = ["indicator1", "indicator2"], + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor("Criteria.Not.Filters.Eq") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + } + + public class IndicatorsTests : DataSetQueryRequestValidatorTests + { + [Theory] + [InlineData("indicator1")] + [InlineData("indicator1111111111111111111111111111111")] + [InlineData("indicator1", "indicator2")] + public void Success(params string[] indicators) + { + var query = new DataSetQueryRequest + { + Indicators = indicators, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryRequest + { + Indicators = [] + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Indicators) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_EmptyValues() + { + var query = new DataSetQueryRequest + { + Indicators = ["", " ", " ", null!] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.Indicators.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Indicators[0]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + + result.ShouldHaveValidationErrorFor("Indicators[1]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + + result.ShouldHaveValidationErrorFor("Indicators[2]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + + result.ShouldHaveValidationErrorFor("Indicators[3]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + + [Fact] + public void Failure_MaxLength() + { + var query = new DataSetQueryRequest + { + Indicators = [new string('x', 41), new string('x', 42)] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.Indicators.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Indicators[0]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + + result.ShouldHaveValidationErrorFor("Indicators[1]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + } + + [Fact] + public void Failure_Mixture() + { + var query = new DataSetQueryRequest + { + Indicators = [new string('x', 101), ""] + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.Indicators.Count, result.Errors.Count); + + result + .ShouldHaveValidationErrorFor("Indicators[0]") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + + result + .ShouldHaveValidationErrorFor("Indicators[1]") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + } + } + + public class SortsTests : DataSetQueryRequestValidatorTests + { + [Fact] + public void Success() + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + Sorts = + [ + new DataSetQuerySort { Field = "TimePeriod", Direction = "Asc" }, + new DataSetQuerySort { Field = "GeographicLevel", Direction = "Asc" }, + ], + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Failure_Empty() + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + Sorts = [] + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Sorts) + .WithErrorCode(FluentValidationKeys.NotEmptyValidator) + .Only(); + } + + [Fact] + public void Failure_InvalidSorts() + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + Sorts = + [ + null!, + new DataSetQuerySort { Field = "", Direction = "Asc" }, + new DataSetQuerySort { Field = "timePeriod", Direction = "" }, + new DataSetQuerySort { Field = "timePeriod", Direction = "asc" }, + new DataSetQuerySort { Field = new string('x', 41), Direction = "Asc" }, + ], + }; + + var result = _validator.TestValidate(query); + + Assert.Equal(query.Sorts.Count, result.Errors.Count); + + result.ShouldHaveValidationErrorFor("Sorts[0]") + .WithErrorCode(FluentValidationKeys.NotNullValidator); + + result.ShouldHaveValidationErrorFor("Sorts[1].Field") + .WithErrorCode(FluentValidationKeys.NotEmptyValidator); + + result.ShouldHaveValidationErrorFor("Sorts[2].Direction") + .WithErrorCode(ValidationMessages.AllowedValue.Code); + + result.ShouldHaveValidationErrorFor("Sorts[3].Direction") + .WithErrorCode(ValidationMessages.AllowedValue.Code); + + result.ShouldHaveValidationErrorFor("Sorts[4].Field") + .WithErrorCode(FluentValidationKeys.MaximumLengthValidator); + } + } + + public class PageTests : DataSetQueryRequestValidatorTests + { + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(10)] + [InlineData(100)] + public void Success(int page) + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + Page = page, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-2)] + [InlineData(-10)] + public void Failure_LessThanOne(int page) + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + Page = page, + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.Page) + .WithErrorCode(FluentValidationKeys.GreaterThanOrEqualValidator) + .Only(); + } + } + + public class PageSizeTests : DataSetQueryRequestValidatorTests + { + [Theory] + [InlineData(1)] + [InlineData(500)] + [InlineData(1000)] + [InlineData(10000)] + public void Success(int pageSize) + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + PageSize = pageSize, + }; + + _validator.TestValidate(query).ShouldNotHaveAnyValidationErrors(); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(10001)] + public void Failure_OutOfBounds(int pageSize) + { + var query = new DataSetQueryRequest + { + Indicators = ["indicator1", "indicator2"], + PageSize = pageSize, + }; + + _validator.TestValidate(query) + .ShouldHaveValidationErrorFor(q => q.PageSize) + .WithErrorCode(FluentValidationKeys.InclusiveBetweenValidator) + .Only(); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryCriteriaJsonConverter.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryCriteriaJsonConverter.cs new file mode 100644 index 00000000000..afde2f926a5 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryCriteriaJsonConverter.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests.Converters; + +public class DataSetQueryCriteriaJsonConverter : JsonConverter +{ + public override bool CanConvert(Type type) + { + return type.IsAssignableFrom(typeof(DataSetQueryCriteria)); + } + + public override DataSetQueryCriteria? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + { + throw new JsonException(); + } + + try + { + var rootElement = doc.RootElement.GetRawText(); + + var propertyNames = doc.RootElement + .EnumerateObject() + .Select(p => p.Name.ToUpperFirst()) + .ToHashSet(); + + if (propertyNames.Contains(nameof(DataSetQueryCriteriaAnd.And))) + { + return JsonSerializer.Deserialize(rootElement, options); + } + + if (propertyNames.Contains(nameof(DataSetQueryCriteriaOr.Or))) + { + return JsonSerializer.Deserialize(rootElement, options); + } + + if (propertyNames.Contains(nameof(DataSetQueryCriteriaNot.Not))) + { + return JsonSerializer.Deserialize(rootElement, options); + } + + return JsonSerializer.Deserialize(rootElement, options); + } + catch (JsonException exception) + { + throw new JsonException(message: null, exception); + } + } + + public override void Write(Utf8JsonWriter writer, DataSetQueryCriteria value, JsonSerializerOptions options) + { + try + { + switch (value) + { + case DataSetQueryCriteriaAnd: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryCriteriaAnd), options); + return; + case DataSetQueryCriteriaOr: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryCriteriaOr), options); + return; + case DataSetQueryCriteriaNot: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryCriteriaNot), options); + return; + case DataSetQueryCriteriaFacets: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryCriteriaFacets), options); + return; + } + } + catch (JsonException exception) + { + throw new JsonException(message: null, exception); + } + + throw new JsonException(); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteria.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteria.cs index 3b24976dca1..e16893fa9c5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteria.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteria.cs @@ -1,3 +1,19 @@ +using System.Text.Json.Serialization; +using FluentValidation.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests.Converters; + namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; -public abstract record DataSetQueryCriteria; +[JsonConverter(typeof(DataSetQueryCriteriaJsonConverter))] +public abstract record DataSetQueryCriteria +{ + protected static void InheritanceValidator( + PolymorphicValidator validator) + where TCriteria : DataSetQueryCriteria + { + validator.Add(_ => new DataSetQueryCriteriaAnd.Validator()); + validator.Add(_ => new DataSetQueryCriteriaOr.Validator()); + validator.Add(_ => new DataSetQueryCriteriaNot.Validator()); + validator.Add(_ => new DataSetQueryCriteriaFacets.Validator()); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaAnd.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaAnd.cs new file mode 100644 index 00000000000..c767cd0200d --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaAnd.cs @@ -0,0 +1,30 @@ +using FluentValidation; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +/// +/// A condition criteria where one or more sub-criteria must all resolve +/// to true for the overall query to match any results. +/// +/// This is equivalent to the `AND` operator in SQL. +/// +public record DataSetQueryCriteriaAnd : DataSetQueryCriteria +{ + /// + /// The sub-criteria which all must resolve to true. + /// + public required IReadOnlyList And { get; init; } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.And) + .NotEmpty(); + + RuleForEach(q => q.And) + .NotNull() + .SetInheritanceValidator(InheritanceValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFacets.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFacets.cs index 60cf7443835..62939f64996 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFacets.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFacets.cs @@ -1,7 +1,9 @@ +using FluentValidation; + namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; /// -/// Criteria outlining the facets of the data to filter results by in the data set query. +/// A set of criteria specifying which facets the query should match results with. /// /// All parts of the criteria must resolve to true to match a result. /// @@ -26,4 +28,26 @@ public record DataSetQueryCriteriaFacets : DataSetQueryCriteria /// Query criteria relating to time periods. /// public DataSetQueryCriteriaTimePeriods? TimePeriods { get; init; } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.Filters) + .SetValidator(new DataSetQueryCriteriaFilters.Validator()!) + .When(q => q.Filters is not null); + + RuleFor(q => q.GeographicLevels) + .SetValidator(new DataSetQueryCriteriaGeographicLevels.Validator()!) + .When(q => q.GeographicLevels is not null); + + RuleFor(q => q.Locations) + .SetValidator(new DataSetQueryCriteriaLocations.Validator()!) + .When(q => q.Locations is not null); + + RuleFor(q => q.TimePeriods) + .SetValidator(new DataSetQueryCriteriaTimePeriods.Validator()!) + .When(q => q.TimePeriods is not null); + } + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaNot.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaNot.cs new file mode 100644 index 00000000000..0427cf565de --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaNot.cs @@ -0,0 +1,27 @@ +using FluentValidation; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +/// +/// A condition criteria where its sub-criteria must resolve +/// to *false* for the overall query to match any results. +/// +/// This is equivalent to the `NOT` operator in SQL. +/// +public record DataSetQueryCriteriaNot : DataSetQueryCriteria +{ + /// + /// The sub-criteria which must resolve to false. + /// + public required DataSetQueryCriteria Not { get; init; } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.Not) + .NotNull() + .SetInheritanceValidator(InheritanceValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaOr.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaOr.cs new file mode 100644 index 00000000000..24c5f7d219a --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaOr.cs @@ -0,0 +1,30 @@ +using FluentValidation; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; + +/// +/// A condition criteria where at least one sub-criteria must resolve +/// to true for the overall query to match any results. +/// +/// This is equivalent to the `OR` operator in SQL. +/// +public record DataSetQueryCriteriaOr : DataSetQueryCriteria +{ + /// + /// The sub-criteria where one must resolve to true. + /// + public required IReadOnlyList Or { get; init; } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.Or) + .NotEmpty(); + + RuleForEach(q => q.Or) + .NotNull() + .SetInheritanceValidator(InheritanceValidator); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs index 53e57c4ad3e..2248bc74c38 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using FluentValidation; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; @@ -44,4 +45,39 @@ public record DataSetQueryRequest /// [DefaultValue(1000)] public int PageSize { get; init; } = 1000; + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(q => q.Indicators) + .NotEmpty(); + RuleForEach(q => q.Indicators) + .NotEmpty() + .MaximumLength(40); + + RuleFor(q => q.Criteria) + .SetInheritanceValidator(v => + { + v.Add(new DataSetQueryCriteriaAnd.Validator()); + v.Add(new DataSetQueryCriteriaOr.Validator()); + v.Add(new DataSetQueryCriteriaNot.Validator()); + v.Add(new DataSetQueryCriteriaFacets.Validator()); + }); + + When(q => q.Sorts is not null, () => + { + RuleFor(q => q.Sorts) + .NotEmpty(); + RuleForEach(q => q.Sorts) + .NotNull() + .SetValidator(new DataSetQuerySort.Validator()); + }); + + RuleFor(request => request.Page) + .GreaterThanOrEqualTo(1); + RuleFor(request => request.PageSize) + .InclusiveBetween(1, 10000); + } + } } From 939adf49cf3baa897c567fe8d32d9e170819146e Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Tue, 30 Apr 2024 02:02:40 +0100 Subject: [PATCH 25/66] EES-4722 Add `DataSetQueryLocationJsonConverter` --- .../DataSetQueryLocationJsonConverter.cs | 120 ++++++++++++++++++ .../Requests/DataSetQueryLocation.cs | 2 + 2 files changed, 122 insertions(+) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryLocationJsonConverter.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryLocationJsonConverter.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryLocationJsonConverter.cs new file mode 100644 index 00000000000..35d27f68fe0 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/Converters/DataSetQueryLocationJsonConverter.cs @@ -0,0 +1,120 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; +using GovUk.Education.ExploreEducationStatistics.Common.Utils; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests.Converters; + +public class DataSetQueryLocationJsonConverter : JsonConverter +{ + public override bool CanConvert(Type type) + { + return type.IsAssignableFrom(typeof(DataSetQueryLocation)); + } + + public override DataSetQueryLocation? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + { + throw new JsonException(); + } + + var rootElement = doc.RootElement.GetRawText(); + + var propertyNames = doc.RootElement + .EnumerateObject() + .Select(p => p.Name.ToUpperFirst()) + .ToHashSet(); + + if (!doc.RootElement.TryGetProperty(nameof(DataSetQueryLocation.Level).ToLowerFirst(), out var levelProperty)) + { + throw new JsonException(); + } + + try + { + var levelString = levelProperty.GetString() ?? string.Empty; + + if (EnumUtil.TryGetFromEnumValue(levelString, out var level)) + { + switch (level) + { + case GeographicLevel.LocalAuthority + when propertyNames.Contains(nameof(DataSetQueryLocationLocalAuthorityCode.Code)): + return JsonSerializer.Deserialize(rootElement, options); + + case GeographicLevel.LocalAuthority + when propertyNames.Contains(nameof(DataSetQueryLocationLocalAuthorityOldCode.OldCode)): + return JsonSerializer.Deserialize( + rootElement, + options + ); + + case GeographicLevel.School + when propertyNames.Contains(nameof(DataSetQueryLocationSchoolLaEstab.LaEstab)): + return JsonSerializer.Deserialize(rootElement, options); + + case GeographicLevel.School + when propertyNames.Contains(nameof(DataSetQueryLocationSchoolUrn.Urn)): + return JsonSerializer.Deserialize(rootElement, options); + + case GeographicLevel.Provider + when propertyNames.Contains(nameof(DataSetQueryLocationProviderUkprn.Ukprn)): + return JsonSerializer.Deserialize(rootElement, options); + } + } + + if (propertyNames.Contains(nameof(DataSetQueryLocationCode.Code))) + { + return JsonSerializer.Deserialize(rootElement, options); + } + + return JsonSerializer.Deserialize(rootElement, options); + } + catch (JsonException exception) + { + throw new JsonException(message: null, exception); + } + } + + public override void Write(Utf8JsonWriter writer, DataSetQueryLocation value, JsonSerializerOptions options) + { + try + { + switch (value) + { + case DataSetQueryLocationLocalAuthorityCode: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationLocalAuthorityCode), options); + return; + case DataSetQueryLocationLocalAuthorityOldCode: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationLocalAuthorityOldCode), options); + return; + case DataSetQueryLocationProviderUkprn: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationProviderUkprn), options); + return; + case DataSetQueryLocationSchoolLaEstab: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationSchoolLaEstab), options); + return; + case DataSetQueryLocationSchoolUrn: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationSchoolUrn), options); + return; + case DataSetQueryLocationCode: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationCode), options); + return; + case DataSetQueryLocationId: + JsonSerializer.Serialize(writer, value, typeof(DataSetQueryLocationId), options); + return; + } + } + catch (JsonException exception) + { + throw new JsonException(message: null, exception); + } + + throw new JsonException(); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryLocation.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryLocation.cs index e15d3810b47..1a0d2c73b67 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryLocation.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryLocation.cs @@ -4,6 +4,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Validators; using GovUk.Education.ExploreEducationStatistics.Common.Utils; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests.Converters; namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; @@ -17,6 +18,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; /// When using codes, you may get more results than expected so it's recommended /// to use IDs where possible to ensure only a single location is matched. /// +[JsonConverter(typeof(DataSetQueryLocationJsonConverter))] public abstract record DataSetQueryLocation { /// From 29eebdab7ac6d244b1e62ef465bea871fdb3e8d4 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Wed, 1 May 2024 01:55:05 +0100 Subject: [PATCH 26/66] EES-4722 Add data set query POST endpoint --- .../Services/DataSetQueryParserTests.cs | 1269 +++++++++++++++-- .../Controllers/DataSetsController.cs | 30 + .../Requests/DataSetQueryRequest.cs | 2 +- .../Services/DataSetQueryParser.cs | 118 +- .../Services/DataSetQueryService.cs | 28 +- .../Interfaces/IDataSetQueryParser.cs | 1 + .../Interfaces/IDataSetQueryService.cs | 6 + 7 files changed, 1299 insertions(+), 155 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs index 5f72107ea7f..2b9859a1285 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs @@ -139,6 +139,7 @@ public async Task AllComparators_SingleOptionNotFound(string comparator, string Assert.Empty(queryState.Errors); var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"filters.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.FiltersNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.FiltersNotFound.Code, warning.Code); @@ -287,6 +288,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"filters.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.FiltersNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.FiltersNotFound.Code, warning.Code); @@ -323,6 +325,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"filters.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.FiltersNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.FiltersNotFound.Code, warning.Code); @@ -332,11 +335,11 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri } } - public class ParseCriteriaGeographicLevels : DataSetQueryParserTests + public class ParseCriteriaGeographicLevelsTests : DataSetQueryParserTests { private readonly DataSetVersion _dataSetVersion; - public ParseCriteriaGeographicLevels() + public ParseCriteriaGeographicLevelsTests() { _dataSetVersion = _dataFixture .DefaultDataSetVersion() @@ -434,6 +437,7 @@ public async Task AllComparators_SingleOptionNotFound(string comparator, string Assert.Empty(queryState.Errors); var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"geographicLevels.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.GeographicLevelsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.GeographicLevelsNotFound.Code, warning.Code); @@ -510,6 +514,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"geographicLevels.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.GeographicLevelsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.GeographicLevelsNotFound.Code, warning.Code); @@ -548,6 +553,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"geographicLevels.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.GeographicLevelsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.GeographicLevelsNotFound.Code, warning.Code); @@ -633,7 +639,7 @@ public async Task AllComparators_SingleOptionExists( .GenerateList(1); var queryLocation = locationOptions - .Select(o => MapOptionToQueryLocation(o, level)) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -694,6 +700,7 @@ public async Task AllComparators_SingleOptionNotFound(string comparator, string Assert.Empty(queryState.Errors); var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"locations.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.LocationsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.LocationsNotFound.Code, warning.Code); @@ -723,7 +730,7 @@ public async Task InComparators_MultipleOptionsForSingleLevel( .GenerateList(3); var queryLocations = locationOptions - .Select(o => MapOptionToQueryLocation(o, level)) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -777,7 +784,7 @@ public async Task InComparators_MultipleOptionsForMultipleLevels(string comparat .GenerateList(); var queryLocations = locationOptions - .Select(o => MapOptionToQueryLocation(o, EnumUtil.GetFromEnumValue(o.Level))) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -830,7 +837,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s .GenerateList(); var queryLocations = locationOptions - .Select(o => MapOptionToQueryLocation(o, EnumUtil.GetFromEnumValue(o.Level))) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -856,6 +863,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"locations.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.LocationsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.LocationsNotFound.Code, warning.Code); @@ -877,7 +885,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri .GenerateList(); var queryLocations = locationOptions - .Select(o => MapOptionToQueryLocation(o, EnumUtil.GetFromEnumValue(o.Level))) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -901,6 +909,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"locations.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.LocationsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.LocationsNotFound.Code, warning.Code); @@ -973,11 +982,7 @@ public async Task AllComparators_SingleTimePeriodExists(string comparator, strin .GenerateList(1); var queryTimePeriods = timePeriods - .Select(o => new DataSetQueryTimePeriod - { - Code = EnumUtil.GetFromEnumLabel(o.Identifier).GetEnumValue(), - Period = TimePeriodFormatter.FormatFromCsv(o.Period) - }) + .Select(MapQueryTimePeriod) .ToList(); _timePeriodRepository @@ -1038,6 +1043,7 @@ public async Task AllComparators_SingleTimePeriodNotFound(string comparator, str Assert.Empty(queryState.Errors); var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"timePeriods.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.TimePeriodsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.TimePeriodsNotFound.Code, warning.Code); @@ -1060,11 +1066,7 @@ public async Task InComparators_MultipleTimePeriods(string comparator, string ex .GenerateList(); var queryTimePeriods = timePeriods - .Select(o => new DataSetQueryTimePeriod - { - Code = EnumUtil.GetFromEnumLabel(o.Identifier).GetEnumValue(), - Period = TimePeriodFormatter.FormatFromCsv(o.Period) - }) + .Select(MapQueryTimePeriod) .ToList(); _timePeriodRepository @@ -1106,11 +1108,7 @@ public async Task InComparators_MultipleTimePeriodsSomeNotFound(string comparato .GenerateList(); var queryTimePeriods = timePeriods - .Select(o => new DataSetQueryTimePeriod - { - Code = EnumUtil.GetFromEnumLabel(o.Identifier).GetEnumValue(), - Period = TimePeriodFormatter.FormatFromCsv(o.Period) - }) + .Select(MapQueryTimePeriod) .ToList(); _timePeriodRepository @@ -1136,6 +1134,7 @@ public async Task InComparators_MultipleTimePeriodsSomeNotFound(string comparato var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"timePeriods.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.TimePeriodsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.TimePeriodsNotFound.Code, warning.Code); @@ -1184,6 +1183,7 @@ public async Task InComparators_MultipleTimePeriodsNoneFound(string comparator, var warning = Assert.Single(queryState.Warnings); + Assert.Equal($"timePeriods.{comparator.ToLowerFirst()}", warning.Path); Assert.Equal(ValidationMessages.TimePeriodsNotFound.Message, warning.Message); Assert.Equal(ValidationMessages.TimePeriodsNotFound.Code, warning.Code); @@ -1193,11 +1193,11 @@ public async Task InComparators_MultipleTimePeriodsNoneFound(string comparator, } } - public class ParseCriteriaMixedTest : DataSetQueryParserTests + public class ParseCriteriaMixedFacetsTests : DataSetQueryParserTests { private readonly DataSetVersion _dataSetVersion; - public ParseCriteriaMixedTest() + public ParseCriteriaMixedFacetsTests() { _dataSetVersion = DefaultDataSetVersion(); } @@ -1297,7 +1297,7 @@ public async Task AllFacetsWithMixedComparatorsAndSingleOption() .GenerateList(1); var queryLocations = locationOptions - .Select(o => MapOptionToQueryLocation(o, GeographicLevel.Region)) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -1313,11 +1313,7 @@ public async Task AllFacetsWithMixedComparatorsAndSingleOption() .GenerateList(1); var queryTimePeriods = timePeriods - .Select(o => new DataSetQueryTimePeriod - { - Code = EnumUtil.GetFromEnumLabel(o.Identifier).GetEnumValue(), - Period = TimePeriodFormatter.FormatFromCsv(o.Period) - }) + .Select(MapQueryTimePeriod) .ToList(); _timePeriodRepository @@ -1390,7 +1386,7 @@ public async Task AllFacetsWithMixedComparatorsAndMultipleOptions() .GenerateList(); var queryLocations = locationOptions - .Select(o => MapOptionToQueryLocation(o, EnumUtil.GetFromEnumValue(o.Level))) + .Select(MapOptionToQueryLocation) .ToList(); _locationRepository @@ -1407,11 +1403,7 @@ public async Task AllFacetsWithMixedComparatorsAndMultipleOptions() .GenerateList(); var queryTimePeriods = timePeriods - .Select(o => new DataSetQueryTimePeriod - { - Code = EnumUtil.GetFromEnumLabel(o.Identifier).GetEnumValue(), - Period = TimePeriodFormatter.FormatFromCsv(o.Period) - }) + .Select(MapQueryTimePeriod) .ToList(); _timePeriodRepository @@ -1457,123 +1449,1125 @@ AND data.time_period_id IN (?, ?) } } - private DataSetVersion DefaultDataSetVersion() => _dataFixture - .DefaultDataSetVersion() - .WithMetaSummary( - _dataFixture.DefaultDataSetVersionMetaSummary() - .WithGeographicLevels([GeographicLevel.Country, GeographicLevel.Region]) - ); - - private static DataSetQueryCriteriaFilters CreateCriteriaFilters( - string comparator, - IReadOnlyList filterOptionIds) + public class ParseCriteriaAndTests : DataSetQueryParserTests { - return comparator switch + private readonly DataSetVersion _dataSetVersion; + + public ParseCriteriaAndTests() { - nameof(DataSetQueryCriteriaFilters.Eq) => - new DataSetQueryCriteriaFilters { Eq = filterOptionIds.Count > 0 ? filterOptionIds[0] : null }, - nameof(DataSetQueryCriteriaFilters.NotEq) => - new DataSetQueryCriteriaFilters { NotEq = filterOptionIds.Count > 0 ? filterOptionIds[0] : null }, - nameof(DataSetQueryCriteriaFilters.In) => - new DataSetQueryCriteriaFilters { In = filterOptionIds }, - nameof(DataSetQueryCriteriaFilters.NotIn) => - new DataSetQueryCriteriaFilters { NotIn = filterOptionIds }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } + _dataSetVersion = DefaultDataSetVersion(); - private static DataSetQueryCriteriaGeographicLevels CreateCriteriaGeographicLevels( - string comparator, - IReadOnlyList geographicLevels) - { - return comparator switch + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + + _locationRepository + .Setup(r => r.ListOptions(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + + _timePeriodRepository + .Setup(r => r.List(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + } + + [Fact] + public async Task Empty() { - nameof(DataSetQueryCriteriaGeographicLevels.Eq) => new DataSetQueryCriteriaGeographicLevels - { - Eq = geographicLevels.Count > 0 ? geographicLevels[0].GetEnumValue() : null - }, - nameof(DataSetQueryCriteriaGeographicLevels.NotEq) => new DataSetQueryCriteriaGeographicLevels + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaAnd { - NotEq = geographicLevels.Count > 0 ? geographicLevels[0].GetEnumValue() : null - }, - nameof(DataSetQueryCriteriaGeographicLevels.In) => new DataSetQueryCriteriaGeographicLevels + And = [] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + Assert.Empty(parsed.Sql); + Assert.Empty(parsed.SqlParameters); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + + [Fact] + public async Task SingleFacets() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .WithFilterId("field_a") + .GenerateList(1); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaAnd { - In = geographicLevels.Select(l => l.GetEnumValue()).ToList() - }, - nameof(DataSetQueryCriteriaGeographicLevels.NotIn) => new DataSetQueryCriteriaGeographicLevels + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[0] } + } + ] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + data."field_a" = ? + """; + + Assert.Equal(expectedSql, parsed.Sql); + + var parameter = Assert.Single(parsed.SqlParameters); + Assert.Equal(filterOptions[0].Id, parameter.Argument); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + + [Fact] + public async Task MultipleFacets() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .ForIndex(0, s => s.SetFilterId("field_a")) + .ForIndex(1, s => s.SetFilterId("field_b")) + .GenerateList(); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var locationOptions = _dataFixture + .DefaultParquetLocationOption() + .ForIndex(0, s => s.SetDefaults(GeographicLevel.Region)) + .ForIndex(1, s => s.SetDefaults(GeographicLevel.LocalAuthority)) + .GenerateList(); + + var queryLocations = locationOptions + .Select(MapOptionToQueryLocation) + .ToList(); + + _locationRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryLocations.ToHashSet(), default)) + .ReturnsAsync(locationOptions); + + var timePeriods = _dataFixture + .DefaultParquetTimePeriod() + .WithPeriod("202324") + .WithIdentifier(TimeIdentifier.AcademicYear.GetEnumLabel()) + .GenerateList(1); + + var queryTimePeriods = timePeriods + .Select(MapQueryTimePeriod) + .ToList(); + + _timePeriodRepository + .Setup(r => r.List(_dataSetVersion, queryTimePeriods, default)) + .ReturnsAsync(timePeriods); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaAnd { - NotIn = geographicLevels.Select(l => l.GetEnumValue()).ToList() - }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { In = queryFilterOptionIds } + }, + new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetQueryCriteriaGeographicLevels + { + Eq = GeographicLevel.Region.GetEnumValue() + } + }, + new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations { NotIn = queryLocations } + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods { Gt = queryTimePeriods[0] } + }, + ] + }; - private static DataSetQueryCriteriaLocations CreateCriteriaLocations( - string comparator, - IReadOnlyList locations) - { - return comparator switch + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + ((data."field_a" IN (?) + OR data."field_b" IN (?))) + AND (data.geographic_level = ?) + AND ((data.locations_reg_id NOT IN (?) + AND data.locations_la_id NOT IN (?))) + AND (data.time_period_id > ?) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Equal(6, parsed.SqlParameters.Count); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + Assert.Equal(filterOptions[1].Id, parsed.SqlParameters[1].Argument); + Assert.Equal(GeographicLevel.Region.GetEnumLabel(), parsed.SqlParameters[2].Argument); + Assert.Equal(locationOptions[0].Id, parsed.SqlParameters[3].Argument); + Assert.Equal(locationOptions[1].Id, parsed.SqlParameters[4].Argument); + Assert.Equal(timePeriods[0].Id, parsed.SqlParameters[5].Argument); + } + + [Fact] + public async Task NestedConditions() { - nameof(DataSetQueryCriteriaLocations.Eq) => - new DataSetQueryCriteriaLocations { Eq = locations.Count > 0 ? locations[0] : null }, - nameof(DataSetQueryCriteriaLocations.NotEq) => - new DataSetQueryCriteriaLocations { NotEq = locations.Count > 0 ? locations[0] : null }, - nameof(DataSetQueryCriteriaLocations.In) => - new DataSetQueryCriteriaLocations { In = locations }, - nameof(DataSetQueryCriteriaLocations.NotIn) => - new DataSetQueryCriteriaLocations { NotIn = locations }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .ForRange(..2, s => s.SetFilterId("field_a")) + .ForIndex(2, s => s.SetFilterId("field_b")) + .ForRange(3..5, s => s.SetFilterId("field_c")) + .GenerateList(); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[0] } + }, + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[1] } + } + ] + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { NotEq = queryFilterOptionIds[2] } + } + ] + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { In = queryFilterOptionIds[3..] } + } + }, + ] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + ((data."field_a" = ?) + OR (data."field_a" = ?)) + AND (data."field_b" != ?) + AND (NOT (data."field_c" IN (?, ?))) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Equal(5, parsed.SqlParameters.Count); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + Assert.Equal(filterOptions[1].Id, parsed.SqlParameters[1].Argument); + Assert.Equal(filterOptions[2].Id, parsed.SqlParameters[2].Argument); + Assert.Equal(filterOptions[3].Id, parsed.SqlParameters[3].Argument); + Assert.Equal(filterOptions[4].Id, parsed.SqlParameters[4].Argument); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } } - private static DataSetQueryCriteriaTimePeriods CreateCriteriaTimePeriods( - string comparator, - IReadOnlyList timePeriods) + public class ParseCriteriaOrTests : DataSetQueryParserTests { - return comparator switch - { - nameof(DataSetQueryCriteriaTimePeriods.Eq) => - new DataSetQueryCriteriaTimePeriods { Eq = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.NotEq) => - new DataSetQueryCriteriaTimePeriods { NotEq = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.In) => - new DataSetQueryCriteriaTimePeriods { In = timePeriods }, - nameof(DataSetQueryCriteriaTimePeriods.NotIn) => - new DataSetQueryCriteriaTimePeriods { NotIn = timePeriods }, - nameof(DataSetQueryCriteriaTimePeriods.Gt) => - new DataSetQueryCriteriaTimePeriods { Gt = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.Gte) => - new DataSetQueryCriteriaTimePeriods { Gte = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.Lt) => - new DataSetQueryCriteriaTimePeriods { Lt = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.Lte) => - new DataSetQueryCriteriaTimePeriods { Lte = timePeriods.Count > 0 ? timePeriods[0] : null }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } + private readonly DataSetVersion _dataSetVersion; - private static DataSetQueryLocation MapOptionToQueryLocation(ParquetLocationOption option, GeographicLevel level) - => level switch + public ParseCriteriaOrTests() { - GeographicLevel.LocalAuthority => option switch - { - { Code: not null } => - new DataSetQueryLocationLocalAuthorityCode { Level = option.Level, Code = option.Code }, - { OldCode: not null } => - new DataSetQueryLocationLocalAuthorityOldCode { Level = option.Level, OldCode = option.OldCode }, - _ => throw new NullReferenceException( - $"{nameof(option.Code)} and {nameof(option.OldCode)} cannot both be null") - }, - GeographicLevel.Provider => option switch - { - { Ukprn: not null } => + _dataSetVersion = DefaultDataSetVersion(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + + _locationRepository + .Setup(r => r.ListOptions(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + + _timePeriodRepository + .Setup(r => r.List(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + } + + [Fact] + public async Task Empty() + { + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaOr + { + Or = [] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + Assert.Empty(parsed.Sql); + Assert.Empty(parsed.SqlParameters); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + + [Fact] + public async Task SingleFacets() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .WithFilterId("field_a") + .GenerateList(1); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[0] } + } + ] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + data."field_a" = ? + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Single(parsed.SqlParameters); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + + [Fact] + public async Task MultipleFacets() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .ForIndex(0, s => s.SetFilterId("field_a")) + .ForIndex(1, s => s.SetFilterId("field_b")) + .GenerateList(); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var locationOptions = _dataFixture + .DefaultParquetLocationOption() + .ForIndex(0, s => s.SetDefaults(GeographicLevel.Region)) + .ForIndex(1, s => s.SetDefaults(GeographicLevel.LocalAuthority)) + .GenerateList(); + + var queryLocations = locationOptions + .Select(MapOptionToQueryLocation) + .ToList(); + + _locationRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryLocations.ToHashSet(), default)) + .ReturnsAsync(locationOptions); + + var timePeriods = _dataFixture + .DefaultParquetTimePeriod() + .WithPeriod("202324") + .WithIdentifier(TimeIdentifier.AcademicYear.GetEnumLabel()) + .GenerateList(1); + + var queryTimePeriods = timePeriods + .Select(MapQueryTimePeriod) + .ToList(); + + _timePeriodRepository + .Setup(r => r.List(_dataSetVersion, queryTimePeriods, default)) + .ReturnsAsync(timePeriods); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { In = queryFilterOptionIds } + }, + new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetQueryCriteriaGeographicLevels + { + Eq = GeographicLevel.Region.GetEnumValue() + } + }, + new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations { NotIn = queryLocations } + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods { Gt = queryTimePeriods[0] } + }, + ] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + ((data."field_a" IN (?) + OR data."field_b" IN (?))) + OR (data.geographic_level = ?) + OR ((data.locations_reg_id NOT IN (?) + AND data.locations_la_id NOT IN (?))) + OR (data.time_period_id > ?) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Equal(6, parsed.SqlParameters.Count); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + Assert.Equal(filterOptions[1].Id, parsed.SqlParameters[1].Argument); + Assert.Equal(GeographicLevel.Region.GetEnumLabel(), parsed.SqlParameters[2].Argument); + Assert.Equal(locationOptions[0].Id, parsed.SqlParameters[3].Argument); + Assert.Equal(locationOptions[1].Id, parsed.SqlParameters[4].Argument); + Assert.Equal(timePeriods[0].Id, parsed.SqlParameters[5].Argument); + } + + [Fact] + public async Task NestedConditions() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .ForIndex(0, s => s.SetFilterId("field_a")) + .ForIndex(1, s => s.SetFilterId("field_b")) + .ForRange(2..5, s => s.SetFilterId("field_c")) + .GenerateList(); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[0] } + }, + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { NotEq = queryFilterOptionIds[1] } + } + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[2] } + } + ] + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { In = queryFilterOptionIds[3..] } + } + }, + ] + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + ((data."field_a" = ?) + AND (data."field_b" != ?)) + OR (data."field_c" = ?) + OR (NOT (data."field_c" IN (?, ?))) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Equal(5, parsed.SqlParameters.Count); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + Assert.Equal(filterOptions[1].Id, parsed.SqlParameters[1].Argument); + Assert.Equal(filterOptions[2].Id, parsed.SqlParameters[2].Argument); + Assert.Equal(filterOptions[3].Id, parsed.SqlParameters[3].Argument); + Assert.Equal(filterOptions[4].Id, parsed.SqlParameters[4].Argument); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + } + + public class ParseCriteriaNotTests : DataSetQueryParserTests + { + private readonly DataSetVersion _dataSetVersion; + + public ParseCriteriaNotTests() + { + _dataSetVersion = DefaultDataSetVersion(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + + _locationRepository + .Setup(r => r.ListOptions(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + + _timePeriodRepository + .Setup(r => r.List(_dataSetVersion, Array.Empty(), default)) + .ReturnsAsync([]); + } + + [Fact] + public async Task SingleFacets() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .WithFilterId("field_a") + .GenerateList(1); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[0] } + } + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + NOT (data."field_a" = ?) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Single(parsed.SqlParameters); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + + [Fact] + public async Task MultipleFacets() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .ForIndex(0, s => s.SetFilterId("field_a")) + .ForIndex(1, s => s.SetFilterId("field_b")) + .GenerateList(); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var locationOptions = _dataFixture + .DefaultParquetLocationOption() + .ForIndex(0, s => s.SetDefaults(GeographicLevel.Region)) + .ForIndex(1, s => s.SetDefaults(GeographicLevel.LocalAuthority)) + .GenerateList(); + + var queryLocations = locationOptions + .Select(MapOptionToQueryLocation) + .ToList(); + + _locationRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryLocations.ToHashSet(), default)) + .ReturnsAsync(locationOptions); + + var timePeriods = _dataFixture + .DefaultParquetTimePeriod() + .WithPeriod("202324") + .WithIdentifier(TimeIdentifier.AcademicYear.GetEnumLabel()) + .GenerateList(1); + + var queryTimePeriods = timePeriods + .Select(MapQueryTimePeriod) + .ToList(); + + _timePeriodRepository + .Setup(r => r.List(_dataSetVersion, queryTimePeriods, default)) + .ReturnsAsync(timePeriods); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { In = queryFilterOptionIds }, + GeographicLevels = new DataSetQueryCriteriaGeographicLevels + { + Eq = GeographicLevel.Region.GetEnumValue() + }, + Locations = new DataSetQueryCriteriaLocations { NotIn = queryLocations }, + TimePeriods = new DataSetQueryCriteriaTimePeriods { Gt = queryTimePeriods[0] } + } + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + NOT ((data."field_a" IN (?) + OR data."field_b" IN (?)) + OR data.geographic_level = ? + OR (data.locations_reg_id NOT IN (?) + AND data.locations_la_id NOT IN (?)) + OR data.time_period_id > ?) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Equal(6, parsed.SqlParameters.Count); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + Assert.Equal(filterOptions[1].Id, parsed.SqlParameters[1].Argument); + Assert.Equal(GeographicLevel.Region.GetEnumLabel(), parsed.SqlParameters[2].Argument); + Assert.Equal(locationOptions[0].Id, parsed.SqlParameters[3].Argument); + Assert.Equal(locationOptions[1].Id, parsed.SqlParameters[4].Argument); + Assert.Equal(timePeriods[0].Id, parsed.SqlParameters[5].Argument); + } + + [Fact] + public async Task NestedConditions() + { + var filterOptions = _dataFixture + .DefaultParquetFilterOption() + .ForIndex(0, s => s.SetFilterId("field_a")) + .ForIndex(1, s => s.SetFilterId("field_b")) + .ForIndex(2, s => s.SetFilterId("field_c")) + .ForRange(3..5, s => s.SetFilterId("field_d")) + .GenerateList(); + + var queryFilterOptionIds = filterOptions + .Select(o => o.PublicId) + .ToList(); + + _filterRepository + .Setup(r => r.ListOptions(_dataSetVersion, queryFilterOptionIds.ToHashSet(), default)) + .ReturnsAsync(filterOptions); + + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { NotEq = queryFilterOptionIds[0] } + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[1] } + }, + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { Eq = queryFilterOptionIds[2] } + } + ] + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters { In = queryFilterOptionIds[3..] } + } + }, + ] + } + }; + + var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); + + const string expectedSql = """ + NOT ((data."field_a" != ?) + AND ((data."field_b" = ?) + OR (data."field_c" = ?)) + AND (NOT (data."field_d" IN (?, ?)))) + """; + + Assert.Equal(expectedSql, parsed.Sql); + + Assert.Equal(5, parsed.SqlParameters.Count); + Assert.Equal(filterOptions[0].Id, parsed.SqlParameters[0].Argument); + Assert.Equal(filterOptions[1].Id, parsed.SqlParameters[1].Argument); + Assert.Equal(filterOptions[2].Id, parsed.SqlParameters[2].Argument); + Assert.Equal(filterOptions[3].Id, parsed.SqlParameters[3].Argument); + Assert.Equal(filterOptions[4].Id, parsed.SqlParameters[4].Argument); + + Assert.Empty(queryState.Errors); + Assert.Empty(queryState.Warnings); + } + } + + public class ParseCriteriaPathTests : DataSetQueryParserTests + { + private readonly DataSetVersion _dataSetVersion; + + public ParseCriteriaPathTests() + { + _dataSetVersion = DefaultDataSetVersion(); + + _filterRepository + .Setup(r => + r.ListOptions(_dataSetVersion, It.IsAny>(), default)) + .ReturnsAsync([]); + + _locationRepository + .Setup(r => + r.ListOptions(_dataSetVersion, It.IsAny>(), default)) + .ReturnsAsync([]); + + _timePeriodRepository + .Setup(r => + r.List(_dataSetVersion, It.IsAny>(), default)) + .ReturnsAsync([]); + } + + [Theory] + [InlineData("criteria")] + [InlineData("test.path")] + public async Task FacetsOnly_BasePathChanged(string basePath) + { + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaFacets + { + Filters = CreateCriteriaFilters("In", ["invalidFilter"]), + Locations = CreateCriteriaLocations( + "In", + [ + new DataSetQueryLocationId { Id = "12345", Level = "NAT" } + ] + ), + TimePeriods = CreateCriteriaTimePeriods( + "In", + [ + new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } + ] + ), + }; + + await service.ParseCriteria(criteria, _dataSetVersion, queryState, basePath); + + Assert.Equal(3, queryState.Warnings.Count); + + Assert.Equal($"{basePath}.filters.in", queryState.Warnings[0].Path); + Assert.Equal($"{basePath}.locations.in", queryState.Warnings[1].Path); + Assert.Equal($"{basePath}.timePeriods.in", queryState.Warnings[2].Path); + } + + [Theory] + [InlineData("criteria")] + [InlineData("test.path")] + public async Task AndCondition_BasePathChanged(string basePath) + { + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = CreateCriteriaFilters("In", ["invalidFilter"]), + Locations = CreateCriteriaLocations( + "In", + [ + new DataSetQueryLocationId { Id = "12345", Level = "NAT" } + ] + ), + TimePeriods = CreateCriteriaTimePeriods( + "In", + [ + new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } + ] + ), + } + ] + }; + + await service.ParseCriteria(criteria, _dataSetVersion, queryState, basePath); + + Assert.Equal(3, queryState.Warnings.Count); + + Assert.Equal($"{basePath}.and[0].filters.in", queryState.Warnings[0].Path); + Assert.Equal($"{basePath}.and[0].locations.in", queryState.Warnings[1].Path); + Assert.Equal($"{basePath}.and[0].timePeriods.in", queryState.Warnings[2].Path); + } + + [Theory] + [InlineData("criteria")] + [InlineData("test.path")] + public async Task OrCondition_BasePathChanged(string basePath) + { + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = CreateCriteriaFilters("In", ["invalidFilter"]), + Locations = CreateCriteriaLocations( + "In", + [ + new DataSetQueryLocationId { Id = "12345", Level = "NAT" } + ] + ), + TimePeriods = CreateCriteriaTimePeriods( + "In", + [ + new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } + ] + ), + } + ] + }; + + await service.ParseCriteria(criteria, _dataSetVersion, queryState, basePath); + + Assert.Equal(3, queryState.Warnings.Count); + + Assert.Equal($"{basePath}.or[0].filters.in", queryState.Warnings[0].Path); + Assert.Equal($"{basePath}.or[0].locations.in", queryState.Warnings[1].Path); + Assert.Equal($"{basePath}.or[0].timePeriods.in", queryState.Warnings[2].Path); + } + + [Theory] + [InlineData("criteria")] + [InlineData("test.path")] + public async Task NotCondition_BasePathChanged(string basePath) + { + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = CreateCriteriaFilters("In", ["invalidFilter"]), + Locations = CreateCriteriaLocations( + "In", + [ + new DataSetQueryLocationId { Id = "12345", Level = "NAT" } + ] + ), + TimePeriods = CreateCriteriaTimePeriods( + "In", + [ + new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } + ] + ), + } + }; + + await service.ParseCriteria(criteria, _dataSetVersion, queryState, basePath); + + Assert.Equal(3, queryState.Warnings.Count); + + Assert.Equal($"{basePath}.not.filters.in", queryState.Warnings[0].Path); + Assert.Equal($"{basePath}.not.locations.in", queryState.Warnings[1].Path); + Assert.Equal($"{basePath}.not.timePeriods.in", queryState.Warnings[2].Path); + } + + [Theory] + [InlineData("criteria")] + [InlineData("test.path")] + public async Task NestedConditionMixture_BasePathChanged(string basePath) + { + var service = BuildService(); + + var queryState = new QueryState(); + var criteria = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + Filters = CreateCriteriaFilters("In", ["invalidFilter"]), + Locations = CreateCriteriaLocations( + "In", + [ + new DataSetQueryLocationId { Id = "12345", Level = "NAT" } + ] + ), + TimePeriods = CreateCriteriaTimePeriods( + "In", + [ + new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } + ] + ), + } + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = CreateCriteriaFilters("In", ["invalidFilter"]), + Locations = CreateCriteriaLocations( + "In", + [ + new DataSetQueryLocationId { Id = "12345", Level = "NAT" } + ] + ), + TimePeriods = CreateCriteriaTimePeriods( + "In", + [ + new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } + ] + ), + } + ], + } + ] + }; + + await service.ParseCriteria(criteria, _dataSetVersion, queryState, basePath); + + Assert.Equal(6, queryState.Warnings.Count); + + Assert.Equal($"{basePath}.or[0].not.filters.in", queryState.Warnings[0].Path); + Assert.Equal($"{basePath}.or[0].not.locations.in", queryState.Warnings[1].Path); + Assert.Equal($"{basePath}.or[0].not.timePeriods.in", queryState.Warnings[2].Path); + + Assert.Equal($"{basePath}.or[1].and[0].filters.in", queryState.Warnings[3].Path); + Assert.Equal($"{basePath}.or[1].and[0].locations.in", queryState.Warnings[4].Path); + Assert.Equal($"{basePath}.or[1].and[0].timePeriods.in", queryState.Warnings[5].Path); + } + } + + private DataSetVersion DefaultDataSetVersion() => _dataFixture + .DefaultDataSetVersion() + .WithMetaSummary( + _dataFixture.DefaultDataSetVersionMetaSummary() + .WithGeographicLevels([GeographicLevel.Country, GeographicLevel.Region]) + ); + + private static DataSetQueryCriteriaFilters CreateCriteriaFilters( + string comparator, + IReadOnlyList filterOptionIds) + { + return comparator switch + { + nameof(DataSetQueryCriteriaFilters.Eq) => + new DataSetQueryCriteriaFilters { Eq = filterOptionIds.Count > 0 ? filterOptionIds[0] : null }, + nameof(DataSetQueryCriteriaFilters.NotEq) => + new DataSetQueryCriteriaFilters { NotEq = filterOptionIds.Count > 0 ? filterOptionIds[0] : null }, + nameof(DataSetQueryCriteriaFilters.In) => + new DataSetQueryCriteriaFilters { In = filterOptionIds }, + nameof(DataSetQueryCriteriaFilters.NotIn) => + new DataSetQueryCriteriaFilters { NotIn = filterOptionIds }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + + private static DataSetQueryCriteriaGeographicLevels CreateCriteriaGeographicLevels( + string comparator, + IReadOnlyList geographicLevels) + { + return comparator switch + { + nameof(DataSetQueryCriteriaGeographicLevels.Eq) => new DataSetQueryCriteriaGeographicLevels + { + Eq = geographicLevels.Count > 0 ? geographicLevels[0].GetEnumValue() : null + }, + nameof(DataSetQueryCriteriaGeographicLevels.NotEq) => new DataSetQueryCriteriaGeographicLevels + { + NotEq = geographicLevels.Count > 0 ? geographicLevels[0].GetEnumValue() : null + }, + nameof(DataSetQueryCriteriaGeographicLevels.In) => new DataSetQueryCriteriaGeographicLevels + { + In = geographicLevels.Select(l => l.GetEnumValue()).ToList() + }, + nameof(DataSetQueryCriteriaGeographicLevels.NotIn) => new DataSetQueryCriteriaGeographicLevels + { + NotIn = geographicLevels.Select(l => l.GetEnumValue()).ToList() + }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + + private static DataSetQueryCriteriaLocations CreateCriteriaLocations( + string comparator, + IReadOnlyList locations) + { + return comparator switch + { + nameof(DataSetQueryCriteriaLocations.Eq) => + new DataSetQueryCriteriaLocations { Eq = locations.Count > 0 ? locations[0] : null }, + nameof(DataSetQueryCriteriaLocations.NotEq) => + new DataSetQueryCriteriaLocations { NotEq = locations.Count > 0 ? locations[0] : null }, + nameof(DataSetQueryCriteriaLocations.In) => + new DataSetQueryCriteriaLocations { In = locations }, + nameof(DataSetQueryCriteriaLocations.NotIn) => + new DataSetQueryCriteriaLocations { NotIn = locations }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + + private static DataSetQueryCriteriaTimePeriods CreateCriteriaTimePeriods( + string comparator, + IReadOnlyList timePeriods) + { + return comparator switch + { + nameof(DataSetQueryCriteriaTimePeriods.Eq) => + new DataSetQueryCriteriaTimePeriods { Eq = timePeriods.Count > 0 ? timePeriods[0] : null }, + nameof(DataSetQueryCriteriaTimePeriods.NotEq) => + new DataSetQueryCriteriaTimePeriods { NotEq = timePeriods.Count > 0 ? timePeriods[0] : null }, + nameof(DataSetQueryCriteriaTimePeriods.In) => + new DataSetQueryCriteriaTimePeriods { In = timePeriods }, + nameof(DataSetQueryCriteriaTimePeriods.NotIn) => + new DataSetQueryCriteriaTimePeriods { NotIn = timePeriods }, + nameof(DataSetQueryCriteriaTimePeriods.Gt) => + new DataSetQueryCriteriaTimePeriods { Gt = timePeriods.Count > 0 ? timePeriods[0] : null }, + nameof(DataSetQueryCriteriaTimePeriods.Gte) => + new DataSetQueryCriteriaTimePeriods { Gte = timePeriods.Count > 0 ? timePeriods[0] : null }, + nameof(DataSetQueryCriteriaTimePeriods.Lt) => + new DataSetQueryCriteriaTimePeriods { Lt = timePeriods.Count > 0 ? timePeriods[0] : null }, + nameof(DataSetQueryCriteriaTimePeriods.Lte) => + new DataSetQueryCriteriaTimePeriods { Lte = timePeriods.Count > 0 ? timePeriods[0] : null }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + + private static DataSetQueryLocation MapOptionToQueryLocation(ParquetLocationOption option) + { + var level = EnumUtil.GetFromEnumValue(option.Level); + + return level switch + { + GeographicLevel.LocalAuthority => option switch + { + { Code: not null } => + new DataSetQueryLocationLocalAuthorityCode { Level = option.Level, Code = option.Code }, + { OldCode: not null } => + new DataSetQueryLocationLocalAuthorityOldCode { Level = option.Level, OldCode = option.OldCode }, + _ => throw new NullReferenceException( + $"{nameof(option.Code)} and {nameof(option.OldCode)} cannot both be null") + }, + GeographicLevel.Provider => option switch + { + { Ukprn: not null } => new DataSetQueryLocationProviderUkprn { Level = option.Level, Ukprn = option.Ukprn! }, _ => throw new NullReferenceException($"{nameof(option.Ukprn)} cannot both be null") }, GeographicLevel.RscRegion => new DataSetQueryLocationId { - Level = option.Level, - Id = option.PublicId, + Level = option.Level, Id = option.PublicId, }, GeographicLevel.School => option switch { @@ -1591,6 +2585,13 @@ private static DataSetQueryLocation MapOptionToQueryLocation(ParquetLocationOpti _ => throw new NullReferenceException($"{nameof(option.Code)} cannot be null") } }; + } + + private static DataSetQueryTimePeriod MapQueryTimePeriod(ParquetTimePeriod option) => new() + { + Code = EnumUtil.GetFromEnumLabel(option.Identifier).GetEnumValue(), + Period = TimePeriodFormatter.FormatFromCsv(option.Period) + }; private DataSetQueryParser BuildService( IParquetFilterRepository? filterRepository = null, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Controllers/DataSetsController.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Controllers/DataSetsController.cs index dbef6096bac..14edabeb7d6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Controllers/DataSetsController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Controllers/DataSetsController.cs @@ -268,4 +268,34 @@ public async Task> QueryData .Query(dataSetId, request, dataSetVersion, cancellationToken) .HandleFailuresOrOk(); } + + /// + /// Query a data set (POST) + /// + /// + /// Query a data set using a `POST` request, returning the filtered results. + /// + /// Note that for simpler queries or exploratory testing, there is also GET variant of this endpoint + /// only handles a smaller subset of querying functionality. However, for most use-cases, + /// this endpoint is recommended as it provides the complete set of functionality. + /// + /// Unlike the `GET` endpoint, the `POST` endpoint allows condition criteria (`and`, `or`, `not`) + /// and consequently can express more complex queries. + /// + [HttpPost("{dataSetId:guid}/query")] + [Produces("application/json")] + [SwaggerResponse(200, "The paginated list of query results", type: typeof(DataSetQueryPaginatedResultsViewModel))] + [SwaggerResponse(400, type: typeof(ValidationProblemViewModel))] + [SwaggerResponse(403, type: typeof(ProblemDetailsViewModel))] + [SwaggerResponse(404, type: typeof(ProblemDetailsViewModel))] + public async Task> QueryDataSetPost( + [SwaggerParameter("The ID of the data set.")] Guid dataSetId, + [SwaggerParameter("The version of the data set to use e.g. 2.0, 1.1, etc.")][FromQuery] string? dataSetVersion, + [FromBody] DataSetQueryRequest request, + CancellationToken cancellationToken) + { + return await dataSetQueryService + .Query(dataSetId, request, dataSetVersion, cancellationToken) + .HandleFailuresOrOk(); + } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs index 2248bc74c38..30e889149df 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryRequest.cs @@ -16,7 +16,7 @@ public record DataSetQueryRequest /// /// The IDs of indicators in the data set to return values for. /// - public required IReadOnlyList Indicators { get; init; } + public IReadOnlyList Indicators { get; init; } = []; /// /// The sorts to sort the results by. Sorts at the start of the diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryParser.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryParser.cs index fcdb277bb44..398bc5aa5cc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryParser.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryParser.cs @@ -3,6 +3,7 @@ using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Services.Query; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Utils; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; using GovUk.Education.ExploreEducationStatistics.Public.Data.Model.DuckDb; using InterpolatedSql; @@ -20,6 +21,7 @@ public async Task ParseCriteria( DataSetQueryCriteria criteria, DataSetVersion dataSetVersion, QueryState queryState, + string basePath = "", CancellationToken cancellationToken = default) { using var _ = MiniProfiler.Current @@ -44,18 +46,32 @@ public async Task ParseCriteria( new TimePeriodFacetsParser(queryState, timePeriodMetas.Result), ]; - return ParseCriteriaFragment(criteria, parsers); + return ParseCriteriaFragment(criteria, parsers, basePath); } - private static Facets ExtractFacets(DataSetQueryCriteria criteria) + private static Facets ExtractFacets(DataSetQueryCriteria criteria, Facets? facets = null) { - var facets = new Facets(); + facets ??= new Facets(); - if (criteria is DataSetQueryCriteriaFacets criteriaFacets) + switch (criteria) { - facets.Filters.AddRange(criteriaFacets.Filters?.GetOptions() ?? []); - facets.Locations.AddRange(criteriaFacets.Locations?.GetOptions() ?? []); - facets.TimePeriods.AddRange(criteriaFacets.TimePeriods?.GetOptions() ?? []); + case DataSetQueryCriteriaFacets criteriaFacets: + facets.Filters.AddRange(criteriaFacets.Filters?.GetOptions() ?? []); + facets.Locations.AddRange(criteriaFacets.Locations?.GetOptions() ?? []); + facets.TimePeriods.AddRange(criteriaFacets.TimePeriods?.GetOptions() ?? []); + break; + + case DataSetQueryCriteriaAnd andCriteria: + andCriteria.And.ForEach(subCriteria => ExtractFacets(subCriteria, facets)); + break; + + case DataSetQueryCriteriaOr orCriteria: + orCriteria.Or.ForEach(subCriteria => ExtractFacets(subCriteria, facets)); + break; + + case DataSetQueryCriteriaNot notCriteria: + ExtractFacets(notCriteria.Not, facets); + break; } return facets; @@ -63,23 +79,95 @@ private static Facets ExtractFacets(DataSetQueryCriteria criteria) private static IInterpolatedSql ParseCriteriaFragment( DataSetQueryCriteria criteria, - IEnumerable facetParsers) + IEnumerable facetParsers, + string path, + string facetJoinCondition = "AND") { - var path = ""; var builder = new DuckDbSqlBuilder(); - if (criteria is DataSetQueryCriteriaFacets criteriaFacets) + switch (criteria) { - var fragments = facetParsers - .Select(parser => parser.Parse(criteriaFacets, path)) - .Where(fragment => !fragment.IsEmpty()); - - builder.AppendRange(fragments, joinString: "\nAND "); + case DataSetQueryCriteriaFacets facetCriteria: + var facetFragments = facetParsers + .Select(parser => parser.Parse(facetCriteria, path)) + .Where(fragment => !fragment.IsEmpty()); + + builder.AppendRange(facetFragments, joinString: $"\n{facetJoinCondition} "); + break; + + case DataSetQueryCriteriaAnd andCriteria: + var andFragments = andCriteria.And + .Select( + (fragment, index) => ParseCriteriaFragment( + fragment, + facetParsers, + path: QueryUtils.Path(path, $"and[{index}]") + ) + ) + .ToList(); + + if (andFragments.Count == 1) + { + builder.Append(andFragments[0]); + } + else + { + builder.AppendRange(andFragments.Select(WrapFragmentInParentheses), joinString: "\nAND "); + } + + break; + + case DataSetQueryCriteriaOr orCriteria: + var orFragments = orCriteria.Or + .Select( + (fragment, index) => ParseCriteriaFragment( + fragment, + facetParsers, + path: QueryUtils.Path(path, $"or[{index}]") + ) + ) + .ToList(); + + if (orFragments.Count == 1) + { + builder.Append(orFragments[0]); + } + else + { + builder.AppendRange(orFragments.Select(WrapFragmentInParentheses), joinString: "\nOR "); + } + + break; + + case DataSetQueryCriteriaNot notCriteria: + builder.AppendLiteral("NOT "); + builder.Append( + WrapFragmentInParentheses( + ParseCriteriaFragment( + notCriteria.Not, + facetParsers, + path: QueryUtils.Path(path, "not"), + facetJoinCondition: "OR" + ) + ) + ); + break; } return builder.Build(); } + private static IInterpolatedSql WrapFragmentInParentheses(IInterpolatedSql sql) + { + var builder = new DuckDbSqlBuilder(); + + builder.AppendLiteral("("); + builder.Append(sql); + builder.AppendLiteral(")"); + + return builder.Build(); + } + private class Facets { public HashSet Filters { get; init; } = []; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs index 1c4c802debf..db2e2d1fcda 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs @@ -97,6 +97,22 @@ public async Task> Q ); } + public async Task> Query( + Guid dataSetId, + DataSetQueryRequest request, + string? dataSetVersion, + CancellationToken cancellationToken = default) + { + return await FindDataSetVersion(dataSetId, dataSetVersion, cancellationToken) + .OnSuccessDo(userService.CheckCanQueryDataSetVersion) + .OnSuccess(dsv => RunQuery( + dataSetVersion: dsv, + query: request, + cancellationToken: cancellationToken, + baseCriteriaPath: "criteria" + )); + } + private async Task> FindDataSetVersion( Guid dataSetId, string? dataSetVersion, @@ -131,7 +147,8 @@ private async Task> FindDataSetVersion( private async Task> RunQuery( DataSetVersion dataSetVersion, DataSetQueryRequest query, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + string baseCriteriaPath = "") { using var _ = MiniProfiler.Current .Step($"{nameof(DataSetQueryService)}.{nameof(RunQuery)}"); @@ -143,10 +160,11 @@ private async Task> if (query.Criteria is not null) { whereBuilder += await dataSetQueryParser.ParseCriteria( - query.Criteria, - dataSetVersion, - queryState, - cancellationToken + criteria: query.Criteria, + dataSetVersion: dataSetVersion, + queryState: queryState, + basePath: baseCriteriaPath, + cancellationToken: cancellationToken ); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryParser.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryParser.cs index 3f271036d97..3e404439ebc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryParser.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryParser.cs @@ -11,5 +11,6 @@ Task ParseCriteria( DataSetQueryCriteria criteria, DataSetVersion dataSetVersion, QueryState queryState, + string basePath = "", CancellationToken cancellationToken = default); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryService.cs index f45f349fee1..5badc138e22 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/Interfaces/IDataSetQueryService.cs @@ -12,4 +12,10 @@ Task> Query( DataSetGetQueryRequest request, string? dataSetVersion = null, CancellationToken cancellationToken = default); + + Task> Query( + Guid dataSetId, + DataSetQueryRequest request, + string? dataSetVersion, + CancellationToken cancellationToken = default); } From d021e8c615c8e531722391f0a9d19df6975a9224 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Wed, 1 May 2024 02:39:22 +0100 Subject: [PATCH 27/66] EES-4722 Manually open/close DuckDB connections during data set queries This seems to provide a performance increase to data set queries by manually opening and closing the DuckDB connection. By doing this, we avoid Dapper automatically closing the connection after each DuckDB query, which chips away at the overall query time. --- .../Services/DataSetQueryService.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs index db2e2d1fcda..82e0379954c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Services/DataSetQueryService.cs @@ -30,6 +30,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Services; internal class DataSetQueryService( PublicDataDbContext publicDataDbContext, + IDuckDbConnection duckDbConnection, IUserService userService, IDataSetQueryParser dataSetQueryParser, IParquetDataRepository dataRepository, @@ -150,6 +151,8 @@ private async Task> CancellationToken cancellationToken, string baseCriteriaPath = "") { + duckDbConnection.Open(); + using var _ = MiniProfiler.Current .Step($"{nameof(DataSetQueryService)}.{nameof(RunQuery)}"); @@ -231,18 +234,22 @@ private async Task> }); } + var results = await MapQueryResults( + rows: rowsTask.Result, + dataSetVersion: dataSetVersion, + columnsByType: columnsByType, + debug: query.Debug, + cancellationToken: cancellationToken); + + duckDbConnection.Close(); + return new DataSetQueryPaginatedResultsViewModel { Paging = new PagingViewModel( page: query.Page, pageSize: query.PageSize, totalResults: (int)countTask.Result), - Results = await MapQueryResults( - rows: rowsTask.Result, - dataSetVersion: dataSetVersion, - columnsByType: columnsByType, - debug: query.Debug, - cancellationToken: cancellationToken), + Results = results, Warnings = queryState.Warnings }; } From e804c7329815e02b3054ec81fb19a74c14ba2325 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Wed, 8 May 2024 12:48:50 +0100 Subject: [PATCH 28/66] EES-4722 Refactor query criteria `Create` methods into request models --- .../Services/DataSetQueryParserTests.cs | 200 +++++------------- .../Requests/DataSetQueryCriteriaFilters.cs | 26 +++ .../DataSetQueryCriteriaGeographicLevels.cs | 32 +++ .../Requests/DataSetQueryCriteriaLocations.cs | 26 +++ .../DataSetQueryCriteriaTimePeriods.cs | 42 ++++ 5 files changed, 182 insertions(+), 144 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs index 2b9859a1285..985d042ccd0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Services/DataSetQueryParserTests.cs @@ -59,7 +59,7 @@ public async Task AllComparators_Empty(string comparator) var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, []) + Filters = DataSetQueryCriteriaFilters.Create(comparator, []) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -96,7 +96,7 @@ public async Task AllComparators_SingleOptionExists(string comparator, string ex var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, queryFilterOptionIds) + Filters = DataSetQueryCriteriaFilters.Create(comparator, queryFilterOptionIds) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -128,7 +128,7 @@ public async Task AllComparators_SingleOptionNotFound(string comparator, string var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, queryFilterOptionIds) + Filters = DataSetQueryCriteriaFilters.Create(comparator, queryFilterOptionIds) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -171,7 +171,7 @@ public async Task InComparators_MultipleOptionsForSingleFilter(string comparator var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, queryFilterOptionIds) + Filters = DataSetQueryCriteriaFilters.Create(comparator, queryFilterOptionIds) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -222,7 +222,7 @@ public async Task InComparators_MultipleOptionsForMultipleFilters(string compara var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, queryFilterOptionIds) + Filters = DataSetQueryCriteriaFilters.Create(comparator, queryFilterOptionIds) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -274,7 +274,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, queryFilterOptionIds) + Filters = DataSetQueryCriteriaFilters.Create(comparator, queryFilterOptionIds) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -313,7 +313,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters(comparator, queryFilterOptionIds) + Filters = DataSetQueryCriteriaFilters.Create(comparator, queryFilterOptionIds) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -371,7 +371,7 @@ public async Task AllComparators_Empty(string comparator) var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - GeographicLevels = CreateCriteriaGeographicLevels(comparator, []) + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, Array.Empty()) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -399,7 +399,7 @@ public async Task AllComparators_SingleOptionExists(string comparator, string ex var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - GeographicLevels = CreateCriteriaGeographicLevels(comparator, geographicLevels) + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, geographicLevels) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -426,7 +426,7 @@ public async Task AllComparators_SingleOptionNotFound(string comparator, string var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - GeographicLevels = CreateCriteriaGeographicLevels(comparator, geographicLevels) + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, geographicLevels) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -465,7 +465,7 @@ public async Task InComparators_MultipleOptions(string comparator, string expect var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - GeographicLevels = CreateCriteriaGeographicLevels(comparator, geographicLevels) + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, geographicLevels) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -500,7 +500,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - GeographicLevels = CreateCriteriaGeographicLevels(comparator, geographicLevels) + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, geographicLevels) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -541,7 +541,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - GeographicLevels = CreateCriteriaGeographicLevels(comparator, geographicLevels) + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, geographicLevels) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -596,7 +596,7 @@ public async Task AllComparators_Empty(string comparator) var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, []) + Locations = DataSetQueryCriteriaLocations.Create(comparator, []) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -651,7 +651,7 @@ public async Task AllComparators_SingleOptionExists( var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, queryLocation) + Locations = DataSetQueryCriteriaLocations.Create(comparator, queryLocation) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -689,7 +689,7 @@ public async Task AllComparators_SingleOptionNotFound(string comparator, string var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, queryLocation), + Locations = DataSetQueryCriteriaLocations.Create(comparator, queryLocation), }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -742,7 +742,7 @@ public async Task InComparators_MultipleOptionsForSingleLevel( var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, queryLocations) + Locations = DataSetQueryCriteriaLocations.Create(comparator, queryLocations) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -796,7 +796,7 @@ public async Task InComparators_MultipleOptionsForMultipleLevels(string comparat var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, queryLocations) + Locations = DataSetQueryCriteriaLocations.Create(comparator, queryLocations) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -849,7 +849,7 @@ public async Task InComparators_MultipleOptionsSomeNotFound(string comparator, s var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, queryLocations) + Locations = DataSetQueryCriteriaLocations.Create(comparator, queryLocations) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -897,7 +897,7 @@ public async Task InComparators_MultipleOptionsNoneFound(string comparator, stri var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Locations = CreateCriteriaLocations(comparator, queryLocations) + Locations = DataSetQueryCriteriaLocations.Create(comparator, queryLocations) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -956,7 +956,7 @@ public async Task AllComparators_Empty(string comparator) var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - TimePeriods = CreateCriteriaTimePeriods(comparator, []) + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, []) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -994,7 +994,7 @@ public async Task AllComparators_SingleTimePeriodExists(string comparator, strin var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - TimePeriods = CreateCriteriaTimePeriods(comparator, queryTimePeriods) + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, queryTimePeriods) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1032,7 +1032,7 @@ public async Task AllComparators_SingleTimePeriodNotFound(string comparator, str var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - TimePeriods = CreateCriteriaTimePeriods(comparator, queryTimePeriods) + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, queryTimePeriods) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1078,7 +1078,7 @@ public async Task InComparators_MultipleTimePeriods(string comparator, string ex var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - TimePeriods = CreateCriteriaTimePeriods(comparator, queryTimePeriods) + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, queryTimePeriods) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1120,7 +1120,7 @@ public async Task InComparators_MultipleTimePeriodsSomeNotFound(string comparato var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - TimePeriods = CreateCriteriaTimePeriods(comparator, queryTimePeriods) + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, queryTimePeriods) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1171,7 +1171,7 @@ public async Task InComparators_MultipleTimePeriodsNoneFound(string comparator, var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - TimePeriods = CreateCriteriaTimePeriods(comparator, queryTimePeriods) + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, queryTimePeriods) }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1251,10 +1251,10 @@ public async Task AllFacetsWithMixedComparatorsAndNoOptions() var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("Eq", []), - GeographicLevels = CreateCriteriaGeographicLevels("In", []), - Locations = CreateCriteriaLocations("NotEq", []), - TimePeriods = CreateCriteriaTimePeriods("Gt", []), + Filters = DataSetQueryCriteriaFilters.Create("Eq", []), + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create("In", Array.Empty()), + Locations = DataSetQueryCriteriaLocations.Create("NotEq", []), + TimePeriods = DataSetQueryCriteriaTimePeriods.Create("Gt", []), }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1325,10 +1325,10 @@ public async Task AllFacetsWithMixedComparatorsAndSingleOption() var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("Eq", queryFilterOptionIds), - GeographicLevels = CreateCriteriaGeographicLevels("In", geographicLevels), - Locations = CreateCriteriaLocations("NotEq", queryLocations), - TimePeriods = CreateCriteriaTimePeriods("Gt", queryTimePeriods), + Filters = DataSetQueryCriteriaFilters.Create("Eq", queryFilterOptionIds), + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create("In", geographicLevels), + Locations = DataSetQueryCriteriaLocations.Create("NotEq", queryLocations), + TimePeriods = DataSetQueryCriteriaTimePeriods.Create("Gt", queryTimePeriods), }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -1415,10 +1415,10 @@ public async Task AllFacetsWithMixedComparatorsAndMultipleOptions() var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("NotIn", queryFilterOptionIds), - GeographicLevels = CreateCriteriaGeographicLevels("NotIn", geographicLevels), - Locations = CreateCriteriaLocations("In", queryLocations), - TimePeriods = CreateCriteriaTimePeriods("In", queryTimePeriods), + Filters = DataSetQueryCriteriaFilters.Create("NotIn", queryFilterOptionIds), + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create("NotIn", geographicLevels), + Locations = DataSetQueryCriteriaLocations.Create("In", queryLocations), + TimePeriods = DataSetQueryCriteriaTimePeriods.Create("In", queryTimePeriods), }; var parsed = await service.ParseCriteria(criteria, _dataSetVersion, queryState); @@ -2238,14 +2238,14 @@ public async Task FacetsOnly_BasePathChanged(string basePath) var queryState = new QueryState(); var criteria = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("In", ["invalidFilter"]), - Locations = CreateCriteriaLocations( + Filters = DataSetQueryCriteriaFilters.Create("In", ["invalidFilter"]), + Locations = DataSetQueryCriteriaLocations.Create( "In", [ new DataSetQueryLocationId { Id = "12345", Level = "NAT" } ] ), - TimePeriods = CreateCriteriaTimePeriods( + TimePeriods = DataSetQueryCriteriaTimePeriods.Create( "In", [ new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } @@ -2276,14 +2276,14 @@ public async Task AndCondition_BasePathChanged(string basePath) [ new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("In", ["invalidFilter"]), - Locations = CreateCriteriaLocations( + Filters = DataSetQueryCriteriaFilters.Create("In", ["invalidFilter"]), + Locations = DataSetQueryCriteriaLocations.Create( "In", [ new DataSetQueryLocationId { Id = "12345", Level = "NAT" } ] ), - TimePeriods = CreateCriteriaTimePeriods( + TimePeriods = DataSetQueryCriteriaTimePeriods.Create( "In", [ new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } @@ -2316,14 +2316,14 @@ public async Task OrCondition_BasePathChanged(string basePath) [ new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("In", ["invalidFilter"]), - Locations = CreateCriteriaLocations( + Filters = DataSetQueryCriteriaFilters.Create("In", ["invalidFilter"]), + Locations = DataSetQueryCriteriaLocations.Create( "In", [ new DataSetQueryLocationId { Id = "12345", Level = "NAT" } ] ), - TimePeriods = CreateCriteriaTimePeriods( + TimePeriods = DataSetQueryCriteriaTimePeriods.Create( "In", [ new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } @@ -2354,14 +2354,14 @@ public async Task NotCondition_BasePathChanged(string basePath) { Not = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("In", ["invalidFilter"]), - Locations = CreateCriteriaLocations( + Filters = DataSetQueryCriteriaFilters.Create("In", ["invalidFilter"]), + Locations = DataSetQueryCriteriaLocations.Create( "In", [ new DataSetQueryLocationId { Id = "12345", Level = "NAT" } ] ), - TimePeriods = CreateCriteriaTimePeriods( + TimePeriods = DataSetQueryCriteriaTimePeriods.Create( "In", [ new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } @@ -2395,14 +2395,14 @@ public async Task NestedConditionMixture_BasePathChanged(string basePath) { Not = new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("In", ["invalidFilter"]), - Locations = CreateCriteriaLocations( + Filters = DataSetQueryCriteriaFilters.Create("In", ["invalidFilter"]), + Locations = DataSetQueryCriteriaLocations.Create( "In", [ new DataSetQueryLocationId { Id = "12345", Level = "NAT" } ] ), - TimePeriods = CreateCriteriaTimePeriods( + TimePeriods = DataSetQueryCriteriaTimePeriods.Create( "In", [ new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } @@ -2416,14 +2416,14 @@ public async Task NestedConditionMixture_BasePathChanged(string basePath) [ new DataSetQueryCriteriaFacets { - Filters = CreateCriteriaFilters("In", ["invalidFilter"]), - Locations = CreateCriteriaLocations( + Filters = DataSetQueryCriteriaFilters.Create("In", ["invalidFilter"]), + Locations = DataSetQueryCriteriaLocations.Create( "In", [ new DataSetQueryLocationId { Id = "12345", Level = "NAT" } ] ), - TimePeriods = CreateCriteriaTimePeriods( + TimePeriods = DataSetQueryCriteriaTimePeriods.Create( "In", [ new DataSetQueryTimePeriod { Period = "2022", Code = "AY" } @@ -2456,94 +2456,6 @@ private DataSetVersion DefaultDataSetVersion() => _dataFixture .WithGeographicLevels([GeographicLevel.Country, GeographicLevel.Region]) ); - private static DataSetQueryCriteriaFilters CreateCriteriaFilters( - string comparator, - IReadOnlyList filterOptionIds) - { - return comparator switch - { - nameof(DataSetQueryCriteriaFilters.Eq) => - new DataSetQueryCriteriaFilters { Eq = filterOptionIds.Count > 0 ? filterOptionIds[0] : null }, - nameof(DataSetQueryCriteriaFilters.NotEq) => - new DataSetQueryCriteriaFilters { NotEq = filterOptionIds.Count > 0 ? filterOptionIds[0] : null }, - nameof(DataSetQueryCriteriaFilters.In) => - new DataSetQueryCriteriaFilters { In = filterOptionIds }, - nameof(DataSetQueryCriteriaFilters.NotIn) => - new DataSetQueryCriteriaFilters { NotIn = filterOptionIds }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } - - private static DataSetQueryCriteriaGeographicLevels CreateCriteriaGeographicLevels( - string comparator, - IReadOnlyList geographicLevels) - { - return comparator switch - { - nameof(DataSetQueryCriteriaGeographicLevels.Eq) => new DataSetQueryCriteriaGeographicLevels - { - Eq = geographicLevels.Count > 0 ? geographicLevels[0].GetEnumValue() : null - }, - nameof(DataSetQueryCriteriaGeographicLevels.NotEq) => new DataSetQueryCriteriaGeographicLevels - { - NotEq = geographicLevels.Count > 0 ? geographicLevels[0].GetEnumValue() : null - }, - nameof(DataSetQueryCriteriaGeographicLevels.In) => new DataSetQueryCriteriaGeographicLevels - { - In = geographicLevels.Select(l => l.GetEnumValue()).ToList() - }, - nameof(DataSetQueryCriteriaGeographicLevels.NotIn) => new DataSetQueryCriteriaGeographicLevels - { - NotIn = geographicLevels.Select(l => l.GetEnumValue()).ToList() - }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } - - private static DataSetQueryCriteriaLocations CreateCriteriaLocations( - string comparator, - IReadOnlyList locations) - { - return comparator switch - { - nameof(DataSetQueryCriteriaLocations.Eq) => - new DataSetQueryCriteriaLocations { Eq = locations.Count > 0 ? locations[0] : null }, - nameof(DataSetQueryCriteriaLocations.NotEq) => - new DataSetQueryCriteriaLocations { NotEq = locations.Count > 0 ? locations[0] : null }, - nameof(DataSetQueryCriteriaLocations.In) => - new DataSetQueryCriteriaLocations { In = locations }, - nameof(DataSetQueryCriteriaLocations.NotIn) => - new DataSetQueryCriteriaLocations { NotIn = locations }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } - - private static DataSetQueryCriteriaTimePeriods CreateCriteriaTimePeriods( - string comparator, - IReadOnlyList timePeriods) - { - return comparator switch - { - nameof(DataSetQueryCriteriaTimePeriods.Eq) => - new DataSetQueryCriteriaTimePeriods { Eq = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.NotEq) => - new DataSetQueryCriteriaTimePeriods { NotEq = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.In) => - new DataSetQueryCriteriaTimePeriods { In = timePeriods }, - nameof(DataSetQueryCriteriaTimePeriods.NotIn) => - new DataSetQueryCriteriaTimePeriods { NotIn = timePeriods }, - nameof(DataSetQueryCriteriaTimePeriods.Gt) => - new DataSetQueryCriteriaTimePeriods { Gt = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.Gte) => - new DataSetQueryCriteriaTimePeriods { Gte = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.Lt) => - new DataSetQueryCriteriaTimePeriods { Lt = timePeriods.Count > 0 ? timePeriods[0] : null }, - nameof(DataSetQueryCriteriaTimePeriods.Lte) => - new DataSetQueryCriteriaTimePeriods { Lte = timePeriods.Count > 0 ? timePeriods[0] : null }, - _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) - }; - } - private static DataSetQueryLocation MapOptionToQueryLocation(ParquetLocationOption option) { var level = EnumUtil.GetFromEnumValue(option.Level); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs index dfece491c70..7537e599ce1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaFilters.cs @@ -42,6 +42,32 @@ public HashSet GetOptions() .ToHashSet(); } + public static DataSetQueryCriteriaFilters Create( + string comparator, + IList optionIds) + { + return comparator switch + { + nameof(Eq) => new DataSetQueryCriteriaFilters + { + Eq = optionIds.Count > 0 ? optionIds[0] : null + }, + nameof(NotEq) => new DataSetQueryCriteriaFilters + { + NotEq = optionIds.Count > 0 ? optionIds[0] : null + }, + nameof(In) => new DataSetQueryCriteriaFilters + { + In = optionIds.ToList() + }, + nameof(NotIn) => new DataSetQueryCriteriaFilters + { + NotIn = optionIds.ToList() + }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + public class Validator : AbstractValidator { public Validator() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs index 9f29d24ebc4..a214f32d8a5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaGeographicLevels.cs @@ -1,4 +1,5 @@ using FluentValidation; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Validators; @@ -62,6 +63,37 @@ public HashSet GetOptions() .ToHashSet(); } + public static DataSetQueryCriteriaGeographicLevels Create( + string comparator, + IList geographicLevels) + => Create(comparator, geographicLevels.Select(l => l.GetEnumValue()).ToList()); + + public static DataSetQueryCriteriaGeographicLevels Create( + string comparator, + IList geographicLevels) + { + return comparator switch + { + nameof(Eq) => new DataSetQueryCriteriaGeographicLevels + { + Eq = geographicLevels.Count > 0 ? geographicLevels[0] : null + }, + nameof(NotEq) => new DataSetQueryCriteriaGeographicLevels + { + NotEq = geographicLevels.Count > 0 ? geographicLevels[0] : null + }, + nameof(In) => new DataSetQueryCriteriaGeographicLevels + { + In = geographicLevels.ToList() + }, + nameof(NotIn) => new DataSetQueryCriteriaGeographicLevels + { + NotIn = geographicLevels.ToList() + }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + public class Validator : AbstractValidator { public Validator() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs index 3797c3ae508..d48a0eda0e6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaLocations.cs @@ -53,6 +53,32 @@ public HashSet GetOptions() .ToHashSet(); } + public static DataSetQueryCriteriaLocations Create( + string comparator, + IList locations) + { + return comparator switch + { + nameof(Eq) => new DataSetQueryCriteriaLocations + { + Eq = locations.Count > 0 ? locations[0] : null + }, + nameof(NotEq) => new DataSetQueryCriteriaLocations + { + NotEq = locations.Count > 0 ? locations[0] : null + }, + nameof(In) => new DataSetQueryCriteriaLocations + { + In = locations.ToList() + }, + nameof(NotIn) => new DataSetQueryCriteriaLocations + { + NotIn = locations.ToList() + }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + public class Validator : AbstractValidator { public Validator() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs index 99a0c353cb9..67c7668b0bb 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api/Requests/DataSetQueryCriteriaTimePeriods.cs @@ -70,6 +70,48 @@ public HashSet GetOptions() .ToHashSet(); } + public static DataSetQueryCriteriaTimePeriods Create( + string comparator, + IList timePeriods) + { + return comparator switch + { + nameof(Eq) => new DataSetQueryCriteriaTimePeriods + { + Eq = timePeriods.Count > 0 ? timePeriods[0] : null + }, + nameof(NotEq) => new DataSetQueryCriteriaTimePeriods + { + NotEq = timePeriods.Count > 0 ? timePeriods[0] : null + }, + nameof(In) => new DataSetQueryCriteriaTimePeriods + { + In = timePeriods.ToList() + }, + nameof(NotIn) => new DataSetQueryCriteriaTimePeriods + { + NotIn = timePeriods.ToList() + }, + nameof(Gt) => new DataSetQueryCriteriaTimePeriods + { + Gt = timePeriods.Count > 0 ? timePeriods[0] : null + }, + nameof(Gte) => new DataSetQueryCriteriaTimePeriods + { + Gte = timePeriods.Count > 0 ? timePeriods[0] : null + }, + nameof(Lt) => new DataSetQueryCriteriaTimePeriods + { + Lt = timePeriods.Count > 0 ? timePeriods[0] : null + }, + nameof(Lte) => new DataSetQueryCriteriaTimePeriods + { + Lte = timePeriods.Count > 0 ? timePeriods[0] : null + }, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; + } + public class Validator : AbstractValidator { public Validator() From 4ad6eb4399c27c8acf69be406d75572b6872b506 Mon Sep 17 00:00:00 2001 From: Nicholas Tsim Date: Thu, 9 May 2024 23:45:48 +0100 Subject: [PATCH 29/66] EES-4722 Add integration tests for data set query POST endpoint --- ...alidationProblemViewModelTestExtensions.cs | 17 + .../DataSetsControllerGetQueryTests.cs | 19 +- .../DataSetsControllerPostQueryTests.cs | 2878 +++++++++++++++++ 3 files changed, 2907 insertions(+), 7 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerPostQueryTests.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ValidationProblemViewModelTestExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ValidationProblemViewModelTestExtensions.cs index e1ba57c6890..70e8ba3897d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ValidationProblemViewModelTestExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ValidationProblemViewModelTestExtensions.cs @@ -302,6 +302,23 @@ public static ErrorViewModel AssertHasEnumError( expectedKey: FluentValidationKeys.EnumValidator ); + public static ErrorViewModel AssertHasAllowedValueError( + this ValidationProblemViewModel validationProblem, + string expectedPath, + TValue? value) + { + var error = validationProblem.AssertHasError( + expectedPath: expectedPath, + expectedCode: ValidationMessages.AllowedValue.Code + ); + + var errorDetail = error.GetDetail>(); + + Assert.Equal(value, errorDetail.Value); + + return error; + } + public static ErrorViewModel AssertHasAllowedValueError( this ValidationProblemViewModel validationProblem, string expectedPath, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs index ab8bd925952..d52989e4360 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerGetQueryTests.cs @@ -1021,13 +1021,17 @@ public async Task PageSizeOutOfBounds_Returns400(int pageSize) } [Theory] - [InlineData(1, 50, 5)] - [InlineData(2, 50, 5)] - [InlineData(3, 50, 5)] - [InlineData(1, 150, 2)] - [InlineData(2, 150, 2)] - [InlineData(1, 216, 1)] - public async Task MultiplePages_Returns200_PaginatedCorrectly(int page, int pageSize, int totalPages) + [InlineData(1, 50, 5, 50)] + [InlineData(2, 50, 5, 50)] + [InlineData(3, 50, 5, 50)] + [InlineData(1, 150, 2, 150)] + [InlineData(2, 150, 2, 66)] + [InlineData(1, 216, 1, 216)] + public async Task MultiplePages_Returns200_PaginatedCorrectly( + int page, + int pageSize, + int totalPages, + int pageResults) { var dataSetVersion = await SetupDefaultDataSetVersion(); @@ -1040,6 +1044,7 @@ public async Task MultiplePages_Returns200_PaginatedCorrectly(int page, int page var viewModel = response.AssertOk(useSystemJson: true); + Assert.Equal(pageResults, viewModel.Results.Count); Assert.Equal(page, viewModel.Paging.Page); Assert.Equal(pageSize, viewModel.Paging.PageSize); Assert.Equal(totalPages, viewModel.Paging.TotalPages); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerPostQueryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerPostQueryTests.cs new file mode 100644 index 00000000000..ca6e89be4a6 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests/Controllers/DataSetsControllerPostQueryTests.cs @@ -0,0 +1,2878 @@ +using System.Net.Http.Json; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Utils; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Fixture; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Requests; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Validators; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Api.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model.Tests.Fixtures; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Services.Interfaces; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; + +namespace GovUk.Education.ExploreEducationStatistics.Public.Data.Api.Tests.Controllers; + +public abstract class DataSetsControllerPostQueryTests(TestApplicationFactory testApp) : IntegrationTestFixture(testApp) +{ + private const string BaseUrl = "api/v1/data-sets"; + + private readonly TestParquetPathResolver _parquetPathResolver = new() + { + Directory = "AbsenceSchool" + }; + + public class AccessTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData(DataSetVersionStatus.Processing)] + [InlineData(DataSetVersionStatus.Failed)] + [InlineData(DataSetVersionStatus.Draft)] + [InlineData(DataSetVersionStatus.Mapping)] + [InlineData(DataSetVersionStatus.Withdrawn)] + [InlineData(DataSetVersionStatus.Cancelled)] + public async Task VersionNotAvailable_Returns403(DataSetVersionStatus versionStatus) + { + var dataSetVersion = await SetupDefaultDataSetVersion(versionStatus); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"] + } + ); + + response.AssertForbidden(); + } + + [Theory] + [InlineData(DataSetVersionStatus.Published)] + [InlineData(DataSetVersionStatus.Deprecated)] + public async Task VersionAvailable_Returns200(DataSetVersionStatus versionStatus) + { + var dataSetVersion = await SetupDefaultDataSetVersion(versionStatus); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"] + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(1, viewModel.Paging.Page); + Assert.Equal(1, viewModel.Paging.TotalPages); + Assert.Equal(216, viewModel.Paging.TotalResults); + + Assert.Empty(viewModel.Warnings); + + Assert.Equal(216, viewModel.Results.Count); + } + + [Fact] + public async Task DataSetDoesNotExist_Returns404() + { + var response = await QueryDataSet( + dataSetId: Guid.NewGuid(), + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"] + } + ); + + response.AssertNotFound(); + } + + [Fact] + public async Task VersionDoesNotExist_Returns404() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + dataSetVersion: "2.0", + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"] + } + ); + + response.AssertNotFound(); + } + } + + public class IndicatorValidationTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = [] + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError("indicators"); + } + + [Fact] + public async Task Blank_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["", " ", " "] + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(3, validationProblem.Errors.Count); + + validationProblem.AssertHasNotEmptyError("indicators[0]"); + validationProblem.AssertHasNotEmptyError("indicators[1]"); + validationProblem.AssertHasNotEmptyError("indicators[2]"); + } + + [Fact] + public async Task TooLong_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = [new string('a', 41), new string('a', 42)] + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(2, validationProblem.Errors.Count); + + validationProblem.AssertHasMaximumLengthError("indicators[0]", maxLength: 40); + validationProblem.AssertHasMaximumLengthError("indicators[1]", maxLength: 40); + } + + [Fact] + public async Task MissingParam_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var client = BuildApp().CreateClient(); + + var response = await client.PostAsJsonAsync( + $"{BaseUrl}/{dataSetVersion.DataSetId}/query", + new {} + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError("indicators"); + } + + [Fact] + public async Task NotFound_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] notFoundIndicators = ["invalid1", "invalid2", "invalid3"]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = notFoundIndicators + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasIndicatorsNotFoundError("indicators", notFoundIndicators); + } + } + + public class FiltersValidationTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task Empty_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = DataSetQueryCriteriaFilters.Create(comparator, []) + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError($"criteria.filters.{comparator.ToLowerFirst()}"); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task InvalidMix_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] invalidFilters = + [ + "", + " ", + " ", + new string('a', 11), + new string('a', 12), + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = DataSetQueryCriteriaFilters.Create(comparator, invalidFilters) + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(5, validationProblem.Errors.Count); + + var basePath = $"criteria.filters.{comparator.ToLowerFirst()}"; + + validationProblem.AssertHasNotEmptyError($"{basePath}[0]"); + validationProblem.AssertHasNotEmptyError($"{basePath}[1]"); + validationProblem.AssertHasNotEmptyError($"{basePath}[2]"); + validationProblem.AssertHasMaximumLengthError($"{basePath}[3]", maxLength: 10); + validationProblem.AssertHasMaximumLengthError($"{basePath}[4]", maxLength: 10); + } + + [Fact] + public async Task AllComparatorsInvalid_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] invalidFilters = + [ + new string('a', 11), + "" + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = new string('a', 11), + NotEq = new string('a', 12), + In = [], + NotIn = invalidFilters + } + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(5, validationProblem.Errors.Count); + + validationProblem.AssertHasMaximumLengthError("criteria.filters.eq", maxLength: 10); + validationProblem.AssertHasMaximumLengthError("criteria.filters.notEq", maxLength: 10); + validationProblem.AssertHasNotEmptyError("criteria.filters.in"); + validationProblem.AssertHasMaximumLengthError("criteria.filters.notIn[0]", maxLength: 10); + validationProblem.AssertHasNotEmptyError("criteria.filters.notIn[1]"); + } + + [Fact] + public async Task NotFound_Returns200_HasWarning() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] notFoundFilters = + [ + "invalid", + "9999999" + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = notFoundFilters[0], + NotEq = notFoundFilters[1], + In = ["IzBzg", ..notFoundFilters], + NotIn = ["IzBzg", ..notFoundFilters] + } + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(5, viewModel.Warnings.Count); + + viewModel.AssertHasFiltersNotFoundWarning("criteria.filters.eq", [notFoundFilters[0]]); + viewModel.AssertHasFiltersNotFoundWarning("criteria.filters.notEq", [notFoundFilters[1]]); + viewModel.AssertHasFiltersNotFoundWarning("criteria.filters.in", notFoundFilters); + viewModel.AssertHasFiltersNotFoundWarning("criteria.filters.notIn", notFoundFilters); + } + } + + public class GeographicLevelsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerPostQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetGetQueryGeographicLevels + { + In = [], + NotIn = [] + } + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(2, validationProblem.Errors.Count); + + validationProblem.AssertHasNotEmptyError("criteria.geographicLevels.in"); + validationProblem.AssertHasNotEmptyError("criteria.geographicLevels.in"); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task InvalidMix_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] invalidLevels = + [ + "", + " ", + "LADD", + "NATT", + "National", + "Local authority", + "LocalAuthority" + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, invalidLevels) + } + } + ); + + var allowed = GeographicLevelUtils.OrderedCodes; + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(7, validationProblem.Errors.Count); + + var path = $"criteria.geographicLevels.{comparator.ToLowerFirst()}"; + + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[0]", value: invalidLevels[0], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[1]", value: invalidLevels[1], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[2]", value: invalidLevels[2], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[3]", value: invalidLevels[3], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[4]", value: invalidLevels[4], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[5]", value: invalidLevels[5], allowed); + validationProblem.AssertHasAllowedValueError(expectedPath: $"{path}[6]", value: invalidLevels[6], allowed); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task NotFound_Returns200_HasWarning(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + GeographicLevel[] notFoundGeographicLevels = + [ + GeographicLevel.Ward, + GeographicLevel.OpportunityArea, + GeographicLevel.PlanningArea, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, [ + GeographicLevel.LocalAuthority, + ..notFoundGeographicLevels + ]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Single(viewModel.Warnings); + + viewModel.AssertHasGeographicLevelsNotFoundWarning( + $"criteria.geographicLevels.{comparator.ToLowerFirst()}", + notFoundGeographicLevels.Select(l => l.GetEnumValue())); + } + + [Fact] + public async Task AllComparatorsInvalid_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + string[] invalidLevels = + [ + " ", + "National", + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetQueryCriteriaGeographicLevels + { + Eq = "NATT", + NotEq = "LADD", + In = invalidLevels, + NotIn = [] + } + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + var allowed = GeographicLevelUtils.OrderedCodes; + + Assert.Equal(5, validationProblem.Errors.Count); + + validationProblem.AssertHasAllowedValueError( + expectedPath: "criteria.geographicLevels.eq", + value: "NATT", + allowed: allowed + ); + validationProblem.AssertHasAllowedValueError( + expectedPath: "criteria.geographicLevels.notEq", + value: "LADD", + allowed: allowed + ); + validationProblem.AssertHasAllowedValueError( + expectedPath: "criteria.geographicLevels.in[0]", + value: invalidLevels[0], + allowed: allowed + ); + validationProblem.AssertHasAllowedValueError( + expectedPath: "criteria.geographicLevels.in[1]", + value: invalidLevels[1], + allowed: allowed + ); + validationProblem.AssertHasNotEmptyError("criteria.geographicLevels.notIn"); + } + } + + public class LocationsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task Empty_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = DataSetQueryCriteriaLocations.Create(comparator, []) + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError($"criteria.locations.{comparator.ToLowerFirst()}"); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task InvalidMix_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = DataSetQueryCriteriaLocations.Create(comparator, [ + new DataSetQueryLocationCode { Level = "LADD", Code = "12345" }, + new DataSetQueryLocationCode { Level = "NATT", Code = "12345" }, + new DataSetQueryLocationId { Level = "NAT", Id = "" }, + new DataSetQueryLocationId { Level = "NAT", Id = " " }, + new DataSetQueryLocationCode { Level = "REG", Code = "" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "" }, + new DataSetQueryLocationId { Level = "NAT", Id = new string('a', 11) }, + new DataSetQueryLocationLocalAuthorityCode { Code = new string('a', 26) }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = new string('a', 11) }, + new DataSetQueryLocationProviderUkprn { Ukprn = new string('a', 9) }, + new DataSetQueryLocationSchoolUrn { Urn = new string('a', 7) }, + new DataSetQueryLocationSchoolLaEstab { LaEstab = new string('a', 8) } + ]) + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(12, validationProblem.Errors.Count); + + var path = $"criteria.locations.{comparator.ToLowerFirst()}"; + + validationProblem.AssertHasAllowedValueError( + expectedPath: $"{path}[0].level", + value: "LADD", + allowed: GeographicLevelUtils.OrderedCodes); + validationProblem.AssertHasAllowedValueError( + expectedPath: $"{path}[1].level", + value: "NATT", + allowed: GeographicLevelUtils.OrderedCodes); + + validationProblem.AssertHasNotEmptyError($"{path}[2].id"); + validationProblem.AssertHasNotEmptyError($"{path}[3].id"); + validationProblem.AssertHasNotEmptyError($"{path}[4].code"); + validationProblem.AssertHasNotEmptyError($"{path}[5].code"); + + validationProblem.AssertHasMaximumLengthError($"{path}[6].id", maxLength: 10); + validationProblem.AssertHasMaximumLengthError($"{path}[7].code", maxLength: 25); + validationProblem.AssertHasMaximumLengthError($"{path}[8].oldCode", maxLength: 10); + validationProblem.AssertHasMaximumLengthError($"{path}[9].ukprn", maxLength: 8); + validationProblem.AssertHasMaximumLengthError($"{path}[10].urn", maxLength: 6); + validationProblem.AssertHasMaximumLengthError($"{path}[11].laEstab", maxLength: 7); + } + + [Fact] + public async Task AllComparatorsInvalid_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQueryLocation[] invalidLocations = + [ + new DataSetQueryLocationId { Level = "", Id = "" }, + new DataSetQueryLocationId { Level = "NAT", Id = " " }, + new DataSetQueryLocationId { Level = "NAT", Id = new string('a', 11) }, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationCode { Level = "LADD", Code = "12345" }, + NotEq = new DataSetQueryLocationId { Level = "NAT", Id = " " }, + In = invalidLocations, + NotIn = [] + } + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(8, validationProblem.Errors.Count); + + validationProblem.AssertHasAllowedValueError( + expectedPath: "criteria.locations.eq.level", + value: "LADD", + allowed: GeographicLevelUtils.OrderedCodes + ); + validationProblem.AssertHasNotEmptyError(expectedPath: "criteria.locations.notEq.id"); + validationProblem.AssertHasNotEmptyError(expectedPath: "criteria.locations.in[0].id"); + validationProblem.AssertHasNotEmptyError(expectedPath: "criteria.locations.in[0].level"); + validationProblem.AssertHasNotEmptyError(expectedPath: "criteria.locations.in[1].id"); + validationProblem.AssertHasMaximumLengthError(expectedPath: "criteria.locations.in[2].id", maxLength: 10); + validationProblem.AssertHasNotEmptyError("criteria.locations.notIn"); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task NotFound_Returns200_HasWarning(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQueryLocation[] notFoundLocations = + [ + new DataSetQueryLocationId { Level = "NAT", Id = "11111111" }, + new DataSetQueryLocationCode { Level = "NAT", Code = "11111111" }, + new DataSetQueryLocationId { Level = "REG", Id = "22222222" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "4444444" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode= "333" }, + new DataSetQueryLocationProviderUkprn { Ukprn = "88888888" }, + new DataSetQueryLocationSchoolUrn { Urn = "666666" }, + new DataSetQueryLocationSchoolLaEstab { LaEstab = "7777777" }, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = DataSetQueryCriteriaLocations.Create(comparator, + [ + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000016" }, + ..notFoundLocations + ]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Single(viewModel.Warnings); + + viewModel.AssertHasLocationsNotFoundWarning( + $"criteria.locations.{comparator.ToLowerFirst()}", + notFoundLocations); + } + } + + public class TimePeriodsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task Empty_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, []) + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError($"criteria.timePeriods.{comparator.ToLowerFirst()}"); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task InvalidMix_Returns400(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQueryTimePeriod[] invalidTimePeriods = + [ + new DataSetQueryTimePeriod { Code = "", Period = "" }, + new DataSetQueryTimePeriod { Code = "AY", Period = "2020/2019" }, + new DataSetQueryTimePeriod { Code = "AY", Period = "2020/2022" }, + new DataSetQueryTimePeriod { Code = "INVALID", Period = "2020" }, + new DataSetQueryTimePeriod { Code = "CY", Period = "2020/2021" }, + new DataSetQueryTimePeriod { Code = "CYQ2", Period = "2020/2021" }, + new DataSetQueryTimePeriod { Code = "RY", Period = "2020/2021" }, + new DataSetQueryTimePeriod { Code = "W10", Period = "2020/2021" }, + new DataSetQueryTimePeriod { Code = "M5", Period = "2020/2021" } + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, invalidTimePeriods) + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(9, validationProblem.Errors.Count); + + var path = $"criteria.timePeriods.{comparator.ToLowerFirst()}"; + + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[0].code", code: ""); + + validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{path}[1].period", period: "2020/2019"); + validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{path}[2].period", period: "2020/2022"); + + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[3].code", code: "INVALID"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[4].code", code: "CY"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[5].code", code: "CYQ2"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[6].code", code: "RY"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[7].code", code: "W10"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{path}[8].code", code: "M5"); + } + + [Fact] + public async Task AllComparatorsInvalid_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQueryTimePeriod[] invalidTimePeriods = + [ + new DataSetQueryTimePeriod { Code = "INVALID", Period = "2020" }, + new DataSetQueryTimePeriod { Code = "CY", Period = "" }, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Code = "AY", Period = "2020/2019" }, + NotEq = new DataSetQueryTimePeriod { Code = "W10", Period = "2020/2021" }, + In = invalidTimePeriods, + NotIn = [], + Gt = new DataSetQueryTimePeriod { Code = "CY", Period = "2020/2021" }, + Gte = new DataSetQueryTimePeriod { Code = "AY", Period = "2020/" }, + Lt = new DataSetQueryTimePeriod { Code = "", Period = "2020" }, + Lte = new DataSetQueryTimePeriod { Code = "", Period = "2020/2021" } + } + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(9, validationProblem.Errors.Count); + + const string basePath = "criteria.timePeriods"; + + validationProblem.AssertHasTimePeriodYearRangeError($"{basePath}.eq.period", period: "2020/2019"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{basePath}.notEq.code", code: "W10"); + validationProblem.AssertHasTimePeriodAllowedCodeError( + expectedPath: $"{basePath}.in[0].code", + code: "INVALID" + ); + validationProblem.AssertHasTimePeriodInvalidYearError( + expectedPath: $"{basePath}.in[1].period", + period: "" + ); + validationProblem.AssertHasNotEmptyError(expectedPath: $"{basePath}.notIn"); + + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{basePath}.gt.code", code: "CY"); + validationProblem.AssertHasTimePeriodYearRangeError(expectedPath: $"{basePath}.gte.period", period: "2020/"); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{basePath}.lt.code", code: ""); + validationProblem.AssertHasTimePeriodAllowedCodeError(expectedPath: $"{basePath}.lte.code", code: ""); + } + + [Theory] + [InlineData("In")] + [InlineData("NotIn")] + public async Task NotFound_Returns200_HasWarning(string comparator) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQueryTimePeriod[] notFoundTimePeriods = + [ + new DataSetQueryTimePeriod { Code = "CY", Period = "2021" }, + new DataSetQueryTimePeriod { Code = "CY", Period = "2022" }, + new DataSetQueryTimePeriod { Code = "CY", Period = "2030" }, + new DataSetQueryTimePeriod { Code = "AY", Period = "2023/2024" }, + new DataSetQueryTimePeriod { Code = "AY", Period = "2018/2019" }, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, [ + new DataSetQueryTimePeriod + { + Code = "AY", + Period = "2020/2021" + }, + ..notFoundTimePeriods + ]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Single(viewModel.Warnings); + + viewModel.AssertHasTimePeriodsNotFoundWarning( + $"criteria.timePeriods.{comparator.ToLowerFirst()}", + notFoundTimePeriods); + } + } + + public class SortsValidationTests(TestApplicationFactory testApp) + : DataSetsControllerPostQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Sorts = [] + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError("sorts"); + } + + [Fact] + public async Task InvalidMix_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Sorts = + [ + new DataSetQuerySort { Field = "", Direction = "asc" }, + new DataSetQuerySort { Field = "test", Direction = "" }, + new DataSetQuerySort { Field = "test", Direction = "invalid" }, + new DataSetQuerySort { Field = "test", Direction = "asc" }, + new DataSetQuerySort { Field = "test", Direction = "desc" }, + new DataSetQuerySort { Field = $"{new string('a', 41)}", Direction = "Asc" }, + new DataSetQuerySort { Field = $"{new string('b', 41)}", Direction = "Desc" }, + ] + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Equal(8, validationProblem.Errors.Count); + + validationProblem.AssertHasNotEmptyError(expectedPath: "sorts[0].field"); + validationProblem.AssertHasAllowedValueError(expectedPath: "sorts[0].direction", value: "asc"); + + validationProblem.AssertHasAllowedValueError(expectedPath: "sorts[1].direction", value: ""); + validationProblem.AssertHasAllowedValueError(expectedPath: "sorts[2].direction", value: "invalid"); + validationProblem.AssertHasAllowedValueError(expectedPath: "sorts[3].direction", value: "asc"); + validationProblem.AssertHasAllowedValueError(expectedPath: "sorts[4].direction", value: "desc"); + + validationProblem.AssertHasMaximumLengthError( + expectedPath: "sorts[5].field", + maxLength: 40 + ); + validationProblem.AssertHasMaximumLengthError( + expectedPath: "sorts[6].field", + maxLength: 40 + ); + } + + [Fact] + public async Task FieldsNotFound_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQuerySort[] notFoundSorts = + [ + new DataSetQuerySort { Field = "invalid1", Direction = "Asc" }, + new DataSetQuerySort { Field = "invalid2", Direction = "Desc" }, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Sorts = + [ + new() { Field = "timePeriod", Direction = "Asc" }, + ..notFoundSorts, + ] + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasSortFieldsNotFoundError("sorts", notFoundSorts); + } + } + + public class PaginationTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData(-1)] + [InlineData(0)] + public async Task PageTooSmall_Returns400(int page) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Page = page + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasGreaterThanOrEqualError("page", comparisonValue: 1); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(10001)] + public async Task PageSizeOutOfBounds_Returns400(int pageSize) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + PageSize = pageSize + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasInclusiveBetweenError("pageSize", from: 1, to: 10000); + } + + [Theory] + [InlineData(1, 50, 5, 50)] + [InlineData(2, 50, 5, 50)] + [InlineData(3, 50, 5, 50)] + [InlineData(1, 150, 2, 150)] + [InlineData(2, 150, 2, 66)] + [InlineData(1, 216, 1, 216)] + public async Task MultiplePages_Returns200_PaginatedCorrectly( + int page, + int pageSize, + int totalPages, + int pageResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Page = page, + PageSize = pageSize + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(pageResults, viewModel.Results.Count); + Assert.Equal(page, viewModel.Paging.Page); + Assert.Equal(pageSize, viewModel.Paging.PageSize); + Assert.Equal(totalPages, viewModel.Paging.TotalPages); + Assert.Equal(216, viewModel.Paging.TotalResults); + } + } + + public class FiltersQueryTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("Eq", 54)] + [InlineData("NotEq", 162)] + [InlineData("In", 54)] + [InlineData("NotIn", 162)] + public async Task SingleOption_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Year 4 + const string filterOptionId = "IzBzg"; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = DataSetQueryCriteriaFilters.Create(comparator, [filterOptionId]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "Eq": + case "In": + Assert.Single(meta.Filters["ncyear"]); + Assert.Contains(filterOptionId, meta.Filters["ncyear"]); + break; + case "NotEq": + case "NotIn": + Assert.Equal(3, meta.Filters["ncyear"].Count); + Assert.DoesNotContain(filterOptionId, meta.Filters["ncyear"]); + break; + } + } + + [Theory] + [InlineData("In", 108)] + [InlineData("NotIn", 108)] + public async Task MultipleOptionsInSameFilter_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Year 4 and 8 + string[] filterOptionIds = ["IzBzg", "7zXob"]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = DataSetQueryCriteriaFilters.Create(comparator, filterOptionIds) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + Assert.Equal(2, meta.Indicators.Count); + Assert.Contains("enrolments", meta.Indicators); + Assert.Contains("sess_authorised", meta.Indicators); + + switch (comparator) + { + case "In": + Assert.Equal(2, meta.Filters["ncyear"].Count); + Assert.Contains(filterOptionIds[0], meta.Filters["ncyear"]); + Assert.Contains(filterOptionIds[1], meta.Filters["ncyear"]); + break; + case "NotIn": + Assert.Equal(2, meta.Filters["ncyear"].Count); + Assert.DoesNotContain(filterOptionIds[0], meta.Filters["ncyear"]); + Assert.DoesNotContain(filterOptionIds[1], meta.Filters["ncyear"]); + break; + } + } + + [Theory] + [InlineData("In", 150)] + [InlineData("NotIn", 66)] + public async Task MultipleOptionsInDifferentFilters_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Total and secondary school type + // Secondary free school and secondary sponsor led academy types + string[] filterOptionIds = ["0kT5D", "6jrfe", "9U4vZ", "O7CLF"]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Filters = DataSetQueryCriteriaFilters.Create(comparator, filterOptionIds) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "In": + Assert.Equal(2, meta.Filters["school_type"].Count); + Assert.Contains(filterOptionIds[0], meta.Filters["school_type"]); + Assert.Contains(filterOptionIds[1], meta.Filters["school_type"]); + + Assert.Equal(2, meta.Filters["academy_type"].Count); + Assert.Contains(filterOptionIds[2], meta.Filters["academy_type"]); + Assert.Contains(filterOptionIds[3], meta.Filters["academy_type"]); + break; + case "NotIn": + Assert.Single(meta.Filters["school_type"]); + Assert.DoesNotContain(filterOptionIds[0], meta.Filters["school_type"]); + Assert.DoesNotContain(filterOptionIds[1], meta.Filters["school_type"]); + + Assert.Single(meta.Filters["academy_type"]); + Assert.DoesNotContain(filterOptionIds[2], meta.Filters["academy_type"]); + Assert.DoesNotContain(filterOptionIds[3], meta.Filters["academy_type"]); + break; + } + } + } + + public class GeographicLevelsQueryTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("Eq", 132)] + [InlineData("NotEq", 84)] + [InlineData("In", 132)] + [InlineData("NotIn", 84)] + public async Task SingleOption_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + const GeographicLevel geographicLevel = GeographicLevel.LocalAuthority; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, [geographicLevel]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "Eq": + case "In": + Assert.Single(meta.GeographicLevels); + Assert.Contains(geographicLevel, meta.GeographicLevels); + break; + case "NotEq": + case "NotIn": + Assert.Equal(3, meta.GeographicLevels.Count); + Assert.DoesNotContain(geographicLevel, meta.GeographicLevels); + break; + } + } + + [Theory] + [InlineData("In", 180)] + [InlineData("NotIn", 36)] + public async Task MultipleOptions_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + GeographicLevel[] geographicLevels = [GeographicLevel.Region, GeographicLevel.LocalAuthority]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + GeographicLevels = DataSetQueryCriteriaGeographicLevels.Create(comparator, geographicLevels) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "In": + Assert.Equal(2, meta.GeographicLevels.Count); + Assert.Contains(geographicLevels[0], meta.GeographicLevels); + Assert.Contains(geographicLevels[1], meta.GeographicLevels); + break; + case "NotIn": + Assert.Equal(2, meta.GeographicLevels.Count); + Assert.DoesNotContain(geographicLevels[0], meta.GeographicLevels); + Assert.DoesNotContain(geographicLevels[1], meta.GeographicLevels); + break; + } + } + } + + public class LocationsQueryTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("Eq", 36)] + [InlineData("NotEq", 180)] + [InlineData("In", 36)] + [InlineData("NotIn", 180)] + public async Task SingleOption_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Sheffield + var location = new DataSetQueryLocationLocalAuthorityCode { Code = "E08000019" }; + const string locationId = "7zXob"; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = DataSetQueryCriteriaLocations.Create(comparator, [location]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "Eq": + case "In": + Assert.Single(meta.Locations["LA"]); + Assert.Contains(locationId, meta.Locations["LA"]); + break; + case "NotEq": + case "NotIn": + Assert.Equal(3, meta.Locations["LA"].Count); + Assert.DoesNotContain(locationId, meta.Locations["LA"]); + break; + } + } + + [Theory] + [InlineData("In", 72)] + [InlineData("NotIn", 144)] + public async Task MultipleOptionsInSameLevel_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Sheffield and Barnsley + DataSetQueryLocation[] locations = + [ + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000019" }, + new DataSetQueryLocationId { Level = "LA", Id = "O7CLF" } + ]; + string[] locationIds = ["7zXob", "O7CLF"]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = DataSetQueryCriteriaLocations.Create(comparator, locations) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "In": + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.Contains(locationIds[0], meta.Locations["LA"]); + Assert.Contains(locationIds[1], meta.Locations["LA"]); + break; + case "NotIn": + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.DoesNotContain(locationIds[0], meta.Locations["LA"]); + Assert.DoesNotContain(locationIds[1], meta.Locations["LA"]); + break; + } + } + + [Theory] + [InlineData("In", 84)] + [InlineData("NotIn", 132)] + public async Task MultipleOptionsInDifferentLevels_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + // Sheffield and Barnsley + // THe Kingston Academy and King Athelstan Primary School + DataSetQueryLocation[] locations = + [ + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000019" }, + new DataSetQueryLocationId { Level = "LA", Id = "O7CLF" }, + new DataSetQueryLocationSchoolLaEstab { LaEstab = "3144001" }, + new DataSetQueryLocationSchoolUrn { Urn = "102579" } + ]; + + string[] locationIds = ["7zXob", "O7CLF", "0kT5D", "arLPb"]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = DataSetQueryCriteriaLocations.Create(comparator, locations) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + switch (comparator) + { + case "In": + Assert.Equal(3, meta.Locations["LA"].Count); + Assert.Contains(locationIds[0], meta.Locations["LA"]); + Assert.Contains(locationIds[1], meta.Locations["LA"]); + + Assert.Equal(6, meta.Locations["SCH"].Count); + Assert.Contains(locationIds[2], meta.Locations["SCH"]); + Assert.Contains(locationIds[3], meta.Locations["SCH"]); + break; + case "NotIn": + Assert.Equal(2, meta.Locations["LA"].Count); + Assert.DoesNotContain(locationIds[0], meta.Locations["LA"]); + Assert.DoesNotContain(locationIds[1], meta.Locations["LA"]); + + Assert.Equal(2, meta.Locations["SCH"].Count); + Assert.DoesNotContain(locationIds[2], meta.Locations["SCH"]); + Assert.DoesNotContain(locationIds[3], meta.Locations["SCH"]); + break; + } + } + } + + public class TimePeriodsQueryTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Theory] + [InlineData("Eq", 72)] + [InlineData("NotEq", 144)] + [InlineData("In", 72)] + [InlineData("NotIn", 144)] + [InlineData("Gt", 72)] + [InlineData("Gte", 144)] + [InlineData("Lt", 72)] + [InlineData("Lte", 144)] + public async Task SingleOption_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var queryTimePeriod = new DataSetQueryTimePeriod { Code = "AY", Period = "2021/2022" }; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, [queryTimePeriod]) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + var timePeriod = new TimePeriodViewModel + { + Code = TimeIdentifier.AcademicYear, + Period = "2021/2022" + }; + + switch (comparator) + { + case "Eq": + case "In": + Assert.Single(meta.TimePeriods); + Assert.Contains(timePeriod, meta.TimePeriods); + break; + case "NotEq": + case "NotIn": + Assert.Equal(2, meta.TimePeriods.Count); + Assert.DoesNotContain(timePeriod, meta.TimePeriods); + break; + case "Lt": + case "Gt": + Assert.Single(meta.TimePeriods); + Assert.DoesNotContain(timePeriod, meta.TimePeriods); + break; + case "Lte": + case "Gte": + Assert.Equal(2, meta.TimePeriods.Count); + Assert.Contains(timePeriod, meta.TimePeriods); + break; + } + } + + [Theory] + [InlineData("In", 144)] + [InlineData("NotIn", 72)] + public async Task MultipleOptions_Returns200(string comparator, int expectedResults) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + DataSetQueryTimePeriod[] queryTimePeriods = + [ + new DataSetQueryTimePeriod { Code = "AY", Period = "2021" }, + new DataSetQueryTimePeriod { Code = "AY", Period = "2022/2023" }, + ]; + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + TimePeriods = DataSetQueryCriteriaTimePeriods.Create(comparator, queryTimePeriods) + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(expectedResults, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + TimePeriodViewModel[] timePeriods = + [ + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" } + ]; + + switch (comparator) + { + case "In": + Assert.Equal(2, meta.TimePeriods.Count); + Assert.Contains(timePeriods[0], meta.TimePeriods); + Assert.Contains(timePeriods[1], meta.TimePeriods); + break; + case "NotIn": + Assert.Single(meta.TimePeriods); + Assert.DoesNotContain(timePeriods[0], meta.TimePeriods); + Assert.DoesNotContain(timePeriods[1], meta.TimePeriods); + break; + } + } + } + + public class FacetsOnlyResultsTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Fact] + public async Task NoResults_Returns200_HasWarning() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Criteria = new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "LA", Id = "9U4vZ" } + }, + GeographicLevels = new DataSetQueryCriteriaGeographicLevels + { + Eq = "NAT" + } + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(1, viewModel.Paging.Page); + Assert.Equal(1000, viewModel.Paging.PageSize); + Assert.Equal(1, viewModel.Paging.TotalPages); + Assert.Equal(0, viewModel.Paging.TotalResults); + + var warning = Assert.Single(viewModel.Warnings); + + Assert.Equal(ValidationMessages.QueryNoResults.Code, warning.Code); + Assert.Equal(ValidationMessages.QueryNoResults.Message, warning.Message); + } + + [Fact] + public async Task DebugEnabled_Returns200_HasWarning() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"], + Debug = true + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + var warning = Assert.Single(viewModel.Warnings); + + Assert.Equal(ValidationMessages.DebugEnabled.Code, warning.Code); + Assert.Equal(ValidationMessages.DebugEnabled.Message, warning.Message); + } + + [Fact] + public async Task SingleIndicator_Returns200_CorrectViewModel() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["sess_authorised"] + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(1, viewModel.Paging.Page); + Assert.Equal(1, viewModel.Paging.TotalPages); + Assert.Equal(216, viewModel.Paging.TotalResults); + + Assert.Empty(viewModel.Warnings); + + Assert.Equal(216, viewModel.Results.Count); + + var result = viewModel.Results[0]; + + Assert.Equal(2, result.Filters.Count); + Assert.Equal("pTSoj", result.Filters["ncyear"]); + Assert.Equal("0kT5D", result.Filters["school_type"]); + + Assert.Equal(GeographicLevel.LocalAuthority, result.GeographicLevel); + + Assert.Equal(3, result.Locations.Count); + Assert.Equal("dP0Zw", result.Locations["LA"]); + Assert.Equal("pTSoj", result.Locations["NAT"]); + Assert.Equal("it6Xr", result.Locations["REG"]); + + Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); + Assert.Equal("2022/2023", result.TimePeriod.Period); + + Assert.Single(result.Values); + Assert.Equal("4064499", result.Values["sess_authorised"]); + } + + [Fact] + public async Task AllIndicators_Returns200_ResultValuesInAllowedRanges() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = + [ + "enrolments", + "sess_authorised", + "sess_possible", + "sess_unauthorised", + "sess_unauthorised_percent", + ] + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(216, viewModel.Results.Count); + + var values = viewModel.Results + .SelectMany(result => result.Values) + .GroupBy(kv => kv.Key, kv => kv.Value) + .ToDictionary(kv => kv.Key, kv => kv.ToList()); + + var enrolments = values["enrolments"].Select(int.Parse).ToList(); + + Assert.Equal(216, enrolments.Count); + Assert.Equal(999598, enrolments.Max()); + Assert.Equal(1072, enrolments.Min()); + + var sessAuthorised = values["sess_authorised"].Select(int.Parse).ToList(); + + Assert.Equal(216, sessAuthorised.Count); + Assert.Equal(4967515, sessAuthorised.Max()); + Assert.Equal(22441, sessAuthorised.Min()); + + var sessPossible = values["sess_possible"].Select(int.Parse).ToList(); + + Assert.Equal(216, sessPossible.Count); + Assert.Equal(9934276, sessPossible.Max()); + Assert.Equal(18306, sessPossible.Min()); + + var sessUnauthorised = values["sess_unauthorised"].Select(int.Parse).ToList(); + + Assert.Equal(216, sessUnauthorised.Count); + Assert.Equal(494993, sessUnauthorised.Max()); + Assert.Equal(2883, sessUnauthorised.Min()); + + var sessUnauthorisedPercent = values["sess_unauthorised_percent"].Select(float.Parse).ToList(); + + Assert.Equal(216, sessUnauthorisedPercent.Count); + Assert.Equal(14.8837004f, sessUnauthorisedPercent.Max()); + Assert.Equal(0.241600007f, sessUnauthorisedPercent.Min()); + } + + [Fact] + public async Task AllIndicators_Returns200_CorrectResultIds() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = + [ + "enrolments", + "sess_authorised", + "sess_possible", + "sess_unauthorised", + "sess_unauthorised_percent", + ] + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(216, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + Assert.Equal(3, meta.Filters.Count); + + Assert.Equal(3, meta.Filters["academy_type"].Count); + + Assert.Contains("dP0Zw", meta.Filters["academy_type"]); + Assert.Contains("9U4vZ", meta.Filters["academy_type"]); + Assert.Contains("O7CLF", meta.Filters["academy_type"]); + + Assert.Equal(4, meta.Filters["ncyear"].Count); + Assert.Contains("IzBzg", meta.Filters["ncyear"]); + Assert.Contains("it6Xr", meta.Filters["ncyear"]); + Assert.Contains("7zXob", meta.Filters["ncyear"]); + Assert.Contains("pTSoj", meta.Filters["ncyear"]); + + Assert.Equal(3, meta.Filters["school_type"].Count); + Assert.Contains("LxWjE", meta.Filters["school_type"]); + Assert.Contains("6jrfe", meta.Filters["school_type"]); + Assert.Contains("0kT5D", meta.Filters["school_type"]); + + Assert.Equal(4, meta.Locations.Count); + + Assert.Single(meta.Locations["NAT"]); + Assert.Contains("pTSoj", meta.Locations["NAT"]); + + Assert.Equal(2, meta.Locations["REG"].Count); + Assert.Contains("it6Xr", meta.Locations["REG"]); + Assert.Contains("IzBzg", meta.Locations["REG"]); + + Assert.Equal(4, meta.Locations["LA"].Count); + Assert.Contains("9U4vZ", meta.Locations["LA"]); + Assert.Contains("O7CLF", meta.Locations["LA"]); + Assert.Contains("dP0Zw", meta.Locations["LA"]); + Assert.Contains("7zXob", meta.Locations["LA"]); + + Assert.Equal(8, meta.Locations["SCH"].Count); + Assert.Contains("qFjG7", meta.Locations["SCH"]); + Assert.Contains("0kT5D", meta.Locations["SCH"]); + Assert.Contains("arLPb", meta.Locations["SCH"]); + Assert.Contains("6jrfe", meta.Locations["SCH"]); + Assert.Contains("HTzLj", meta.Locations["SCH"]); + Assert.Contains("LxWjE", meta.Locations["SCH"]); + Assert.Contains("CpId1", meta.Locations["SCH"]); + Assert.Contains("YPHKM", meta.Locations["SCH"]); + + Assert.Equal(4, meta.GeographicLevels.Count); + Assert.Contains(GeographicLevel.Country, meta.GeographicLevels); + Assert.Contains(GeographicLevel.Region, meta.GeographicLevels); + Assert.Contains(GeographicLevel.LocalAuthority, meta.GeographicLevels); + Assert.Contains(GeographicLevel.School, meta.GeographicLevels); + + Assert.Equal(3, meta.TimePeriods.Count); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2020/2021" }, + meta.TimePeriods + ); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + meta.TimePeriods + ); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" }, + meta.TimePeriods + ); + + Assert.Equal(5, meta.Indicators.Count); + Assert.Contains("enrolments", meta.Indicators); + Assert.Contains("sess_authorised", meta.Indicators); + Assert.Contains("sess_possible", meta.Indicators); + Assert.Contains("sess_unauthorised", meta.Indicators); + Assert.Contains("sess_unauthorised_percent", meta.Indicators); + } + + [Fact] + public async Task AllIndicators_Returns200_CorrectDebuggedResultLabels() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = + [ + "enrolments", + "sess_authorised", + "sess_possible", + "sess_unauthorised", + "sess_unauthorised_percent", + ], + Debug = true + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + Assert.Equal(216, viewModel.Results.Count); + + var meta = GatherQueryResultsMeta(viewModel); + + Assert.Equal(3, meta.Filters.Count); + + Assert.Equal(3, meta.Filters["academy_type"].Count); + + Assert.Contains("dP0Zw :: Primary sponsor led academy", meta.Filters["academy_type"]); + Assert.Contains("9U4vZ :: Secondary free school", meta.Filters["academy_type"]); + Assert.Contains("O7CLF :: Secondary sponsor led academy", meta.Filters["academy_type"]); + + Assert.Equal(4, meta.Filters["ncyear"].Count); + Assert.Contains("IzBzg :: Year 4", meta.Filters["ncyear"]); + Assert.Contains("it6Xr :: Year 6", meta.Filters["ncyear"]); + Assert.Contains("7zXob :: Year 8", meta.Filters["ncyear"]); + Assert.Contains("pTSoj :: Year 10", meta.Filters["ncyear"]); + + Assert.Equal(3, meta.Filters["school_type"].Count); + Assert.Contains("LxWjE :: State-funded primary", meta.Filters["school_type"]); + Assert.Contains("6jrfe :: State-funded secondary", meta.Filters["school_type"]); + Assert.Contains("0kT5D :: Total", meta.Filters["school_type"]); + + Assert.Equal(4, meta.Locations.Count); + + Assert.Single(meta.Locations["NAT"]); + Assert.Contains("pTSoj :: England (code = E92000001)", meta.Locations["NAT"]); + + Assert.Equal(2, meta.Locations["REG"].Count); + Assert.Contains("it6Xr :: Outer London (code = E13000002)", meta.Locations["REG"]); + Assert.Contains("IzBzg :: Yorkshire and The Humber (code = E12000003)", meta.Locations["REG"]); + + Assert.Equal(4, meta.Locations["LA"].Count); + Assert.Contains("9U4vZ :: Barnet (code = E09000003, oldCode = 302)", meta.Locations["LA"]); + Assert.Contains("O7CLF :: Barnsley (code = E08000016, oldCode = 370)", meta.Locations["LA"]); + Assert.Contains( + "dP0Zw :: Kingston upon Thames / Richmond upon Thames (code = E09000021 / E09000027, oldCode = 314)", + meta.Locations["LA"] + ); + Assert.Contains("7zXob :: Sheffield (code = E08000019, oldCode = 373)", meta.Locations["LA"]); + + Assert.Equal(8, meta.Locations["SCH"].Count); + Assert.Contains("qFjG7 :: Colindale Primary School (urn = 101269, laEstab = 3022014)", meta.Locations["SCH"]); + Assert.Contains("0kT5D :: Greenhill Primary School (urn = 145374, laEstab = 3732341)", meta.Locations["SCH"]); + Assert.Contains( + "arLPb :: Hoyland Springwood Primary School (urn = 141973, laEstab = 3702039)", + meta.Locations["SCH"] + ); + Assert.Contains( + "6jrfe :: King Athelstan Primary School (urn = 102579, laEstab = 3142032)", + meta.Locations["SCH"] + ); + Assert.Contains("HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", meta.Locations["SCH"]); + Assert.Contains("LxWjE :: Penistone Grammar School (urn = 106653, laEstab = 3704027)", meta.Locations["SCH"]); + Assert.Contains("CpId1 :: The Kingston Academy (urn = 141862, laEstab = 3144001)", meta.Locations["SCH"]); + Assert.Contains("YPHKM :: Wren Academy Finchley (urn = 135507, laEstab = 3026906)", meta.Locations["SCH"]); + + Assert.Equal(4, meta.GeographicLevels.Count); + Assert.Contains(GeographicLevel.Country, meta.GeographicLevels); + Assert.Contains(GeographicLevel.Region, meta.GeographicLevels); + Assert.Contains(GeographicLevel.LocalAuthority, meta.GeographicLevels); + Assert.Contains(GeographicLevel.School, meta.GeographicLevels); + + Assert.Equal(3, meta.TimePeriods.Count); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2020/2021" }, + meta.TimePeriods + ); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2021/2022" }, + meta.TimePeriods + ); + Assert.Contains( + new TimePeriodViewModel { Code = TimeIdentifier.AcademicYear, Period = "2022/2023" }, + meta.TimePeriods + ); + + Assert.Equal(5, meta.Indicators.Count); + Assert.Contains("enrolments", meta.Indicators); + Assert.Contains("sess_authorised", meta.Indicators); + Assert.Contains("sess_possible", meta.Indicators); + Assert.Contains("sess_unauthorised", meta.Indicators); + Assert.Contains("sess_unauthorised_percent", meta.Indicators); + } + + [Fact] + public async Task AllFacetsMixture_Returns200() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Debug = true, + Criteria = new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + NotEq = "7zXob", + In = ["9U4vZ", "O7CLF"] + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + NotEq = "NAT" + }, + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "NAT", Id = "pTSoj" }, + NotIn = + [ + new DataSetQueryLocationCode { Level = "REG", Code = "E13000002" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "370" }, + ] + }, + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Gt = new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" }, + Lt = new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" } + } + } + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + var result = Assert.Single(viewModel.Results); + + Assert.Equal(3, result.Filters.Count); + Assert.Equal("pTSoj :: Year 10", result.Filters["ncyear"]); + Assert.Equal("6jrfe :: State-funded secondary", result.Filters["school_type"]); + Assert.Equal("O7CLF :: Secondary sponsor led academy", result.Filters["academy_type"]); + + Assert.Equal(GeographicLevel.School, result.GeographicLevel); + + Assert.Equal(4, result.Locations.Count); + Assert.Equal("pTSoj :: England (code = E92000001)", result.Locations["NAT"]); + Assert.Equal("IzBzg :: Yorkshire and The Humber (code = E12000003)", result.Locations["REG"]); + Assert.Equal("7zXob :: Sheffield (code = E08000019, oldCode = 373)", result.Locations["LA"]); + Assert.Equal( + "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", + result.Locations["SCH"] + ); + + Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); + Assert.Equal("2021/2022", result.TimePeriod.Period); + + Assert.Equal(2, result.Values.Count); + Assert.Equal("752009", result.Values["enrolments"]); + Assert.Equal("262396", result.Values["sess_authorised"]); + } + } + + public class AndConditionTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Debug = true, + Criteria = new DataSetQueryCriteriaAnd + { + And = [] + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError("criteria.and"); + } + + private static readonly DataSetQueryCriteriaFacets BaseFacets = new() + { + Filters = new DataSetQueryCriteriaFilters + { + NotEq = "7zXob", + In = ["9U4vZ", "O7CLF"] + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + NotEq = "NAT" + }, + Locations = new DataSetQueryCriteriaLocations + { + NotIn = + [ + new DataSetQueryLocationCode { Level = "REG", Code = "E13000002" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "370" }, + ] + }, + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + } + }; + + public static readonly TheoryData EquivalentCriteria = new() + { + new DataSetQueryCriteriaAnd + { + And = [BaseFacets] + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = BaseFacets.Filters + }, + new DataSetQueryCriteriaFacets + { + GeographicLevels = BaseFacets.GeographicLevels + }, + new DataSetQueryCriteriaFacets + { + Locations = BaseFacets.Locations + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = BaseFacets.TimePeriods + }, + ] + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = [BaseFacets] + } + ] + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = BaseFacets.Filters + }, + new DataSetQueryCriteriaFacets + { + GeographicLevels = BaseFacets.GeographicLevels + }, + new DataSetQueryCriteriaFacets + { + Locations = BaseFacets.Locations + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = BaseFacets.TimePeriods + }, + ] + } + ] + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + NotEq = BaseFacets.Filters!.NotEq, + }, + }, + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + In = BaseFacets.Filters!.In + }, + Locations = BaseFacets.Locations + }, + ] + }, + new DataSetQueryCriteriaFacets + { + GeographicLevels = BaseFacets.GeographicLevels, + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = BaseFacets.TimePeriods + }, + ] + }, + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + NotEq = BaseFacets.Filters.NotEq, + }, + }, + new DataSetQueryCriteriaFacets + { + Locations = BaseFacets.Locations, + TimePeriods = BaseFacets.TimePeriods + }, + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "9U4vZ", + }, + }, + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + Eq = "O7CLF", + }, + }, + ] + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetGetQueryGeographicLevels + { + Eq = "NAT" + }, + } + } + ] + } + }; + + [Theory] + [MemberData(nameof(EquivalentCriteria))] + public async Task EquivalentCriteria_Returns200(DataSetQueryCriteria criteria) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Debug = true, + Criteria = criteria + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + var result = Assert.Single(viewModel.Results); + + Assert.Equal(3, result.Filters.Count); + Assert.Equal("pTSoj :: Year 10", result.Filters["ncyear"]); + Assert.Equal("6jrfe :: State-funded secondary", result.Filters["school_type"]); + Assert.Equal("O7CLF :: Secondary sponsor led academy", result.Filters["academy_type"]); + + Assert.Equal(GeographicLevel.School, result.GeographicLevel); + + Assert.Equal(4, result.Locations.Count); + Assert.Equal("pTSoj :: England (code = E92000001)", result.Locations["NAT"]); + Assert.Equal("IzBzg :: Yorkshire and The Humber (code = E12000003)", result.Locations["REG"]); + Assert.Equal("7zXob :: Sheffield (code = E08000019, oldCode = 373)", result.Locations["LA"]); + Assert.Equal( + "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", + result.Locations["SCH"] + ); + + Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); + Assert.Equal("2021/2022", result.TimePeriod.Period); + + Assert.Equal(2, result.Values.Count); + Assert.Equal("752009", result.Values["enrolments"]); + Assert.Equal("262396", result.Values["sess_authorised"]); + } + } + + public class OrConditionTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + [Fact] + public async Task Empty_Returns400() + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Debug = true, + Criteria = new DataSetQueryCriteriaOr + { + Or = [] + } + } + ); + + var validationProblem = response.AssertValidationProblem(); + + Assert.Single(validationProblem.Errors); + + validationProblem.AssertHasNotEmptyError("criteria.or"); + } + + private static readonly DataSetQueryCriteriaFacets BaseFacets = new() + { + Filters = new DataSetQueryCriteriaFilters + { + NotEq = "7zXob", + In = ["9U4vZ", "O7CLF"] + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + NotEq = "NAT" + }, + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "NAT", Id = "pTSoj" }, + NotIn = + [ + new DataSetQueryLocationCode { Level = "REG", Code = "E13000002" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "370" }, + ] + } + }; + + public static readonly TheoryData EquivalentCriteria = new() + { + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + In = + [ + new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" } + ], + } + }, + ], + }, + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + } + }, + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + } + } + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + } + }, + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + } + }, + ] + } + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + In = + [ + new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + ] + } + }, + ] + } + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + } + }, + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + } + }, + ] + } + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaAnd + { + And = + [ + new DataSetQueryCriteriaFacets { Filters = BaseFacets.Filters }, + new DataSetQueryCriteriaFacets { GeographicLevels = BaseFacets.GeographicLevels }, + new DataSetQueryCriteriaFacets { Locations = BaseFacets.Locations }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + In = + [ + new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + ] + } + }, + ] + }, + new DataSetQueryCriteriaOr + { + Or = + [ + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" }, + } + }, + BaseFacets with + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + } + }, + ] + } + ] + }, + }; + + [Theory] + [MemberData(nameof(EquivalentCriteria))] + public async Task EquivalentCriteria_Returns200(DataSetQueryCriteria criteria) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Debug = true, + Criteria = criteria + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + var results = viewModel.Results; + + Assert.Equal(2, results.Count); + + // Result 1 + + Assert.Equal(3, results[0].Filters.Count); + Assert.Equal("pTSoj :: Year 10", results[0].Filters["ncyear"]); + Assert.Equal("6jrfe :: State-funded secondary", results[0].Filters["school_type"]); + Assert.Equal("O7CLF :: Secondary sponsor led academy", results[0].Filters["academy_type"]); + + Assert.Equal(GeographicLevel.School, results[0].GeographicLevel); + + Assert.Equal(4, results[0].Locations.Count); + Assert.Equal("pTSoj :: England (code = E92000001)", results[0].Locations["NAT"]); + Assert.Equal("IzBzg :: Yorkshire and The Humber (code = E12000003)", results[0].Locations["REG"]); + Assert.Equal("7zXob :: Sheffield (code = E08000019, oldCode = 373)", results[0].Locations["LA"]); + Assert.Equal( + "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", + results[0].Locations["SCH"] + ); + + Assert.Equal(TimeIdentifier.AcademicYear, results[0].TimePeriod.Code); + Assert.Equal("2022/2023", results[0].TimePeriod.Period); + + Assert.Equal(2, results[0].Values.Count); + Assert.Equal("751028", results[0].Values["enrolments"]); + Assert.Equal("175843", results[0].Values["sess_authorised"]); + + // Result 2 + + Assert.Equal(3, results[1].Filters.Count); + Assert.Equal("pTSoj :: Year 10", results[1].Filters["ncyear"]); + Assert.Equal("6jrfe :: State-funded secondary", results[1].Filters["school_type"]); + Assert.Equal("O7CLF :: Secondary sponsor led academy", results[1].Filters["academy_type"]); + + Assert.Equal(GeographicLevel.School, results[1].GeographicLevel); + + Assert.Equal(4, results[1].Locations.Count); + Assert.Equal("pTSoj :: England (code = E92000001)", results[1].Locations["NAT"]); + Assert.Equal("IzBzg :: Yorkshire and The Humber (code = E12000003)", results[1].Locations["REG"]); + Assert.Equal("7zXob :: Sheffield (code = E08000019, oldCode = 373)", results[1].Locations["LA"]); + Assert.Equal( + "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", + results[1].Locations["SCH"] + ); + + Assert.Equal(TimeIdentifier.AcademicYear, results[1].TimePeriod.Code); + Assert.Equal("2021/2022", results[1].TimePeriod.Period); + + Assert.Equal(2, results[1].Values.Count); + Assert.Equal("752009", results[1].Values["enrolments"]); + Assert.Equal("262396", results[1].Values["sess_authorised"]); + } + } + + public class NotConditionTests(TestApplicationFactory testApp) : DataSetsControllerPostQueryTests(testApp) + { + private static readonly DataSetQueryCriteriaFacets BaseFacets = new() + { + Filters = new DataSetQueryCriteriaFilters + { + In = ["LxWjE", "7zXob"], + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + In = ["NAT", "REG", "LA"] + }, + Locations = new DataSetQueryCriteriaLocations + { + In = + [ + new DataSetQueryLocationLocalAuthorityCode { Code = "E09000003" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000016" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E09000021 / E09000027" }, + ] + }, + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + In = + [ + new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + ] + } + }; + + public static readonly TheoryData EquivalentCriteria = new() + { + new DataSetQueryCriteriaNot + { + Not = BaseFacets + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaNot + { + Not = BaseFacets + } + } + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaAnd + { + And = [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + NotEq = "7zXob", + In = ["9U4vZ", "O7CLF"] + }, + GeographicLevels = new DataSetGetQueryGeographicLevels + { + NotEq = "NAT" + }, + Locations = new DataSetQueryCriteriaLocations + { + Eq = new DataSetQueryLocationId { Level = "NAT", Id = "pTSoj" }, + NotIn = + [ + new DataSetQueryLocationCode { Level = "REG", Code = "E13000002" }, + new DataSetQueryLocationLocalAuthorityOldCode { OldCode = "370" }, + ] + }, + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + Eq = new DataSetQueryTimePeriod { Period = "2021/2022", Code = "AY" } + } + } + ] + } + } + }, + new DataSetQueryCriteriaNot + { + Not = new DataSetQueryCriteriaOr + { + Or = + [ + new DataSetQueryCriteriaFacets + { + Filters = new DataSetQueryCriteriaFilters + { + In = ["LxWjE", "7zXob"], + }, + }, + new DataSetQueryCriteriaFacets + { + GeographicLevels = new DataSetGetQueryGeographicLevels + { + In = ["NAT", "REG", "LA"] + }, + }, + new DataSetQueryCriteriaFacets + { + Locations = new DataSetQueryCriteriaLocations + { + In = + [ + new DataSetQueryLocationLocalAuthorityCode { Code = "E09000003" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E08000016" }, + new DataSetQueryLocationLocalAuthorityCode { Code = "E09000021 / E09000027" }, + ] + }, + }, + new DataSetQueryCriteriaFacets + { + TimePeriods = new DataSetQueryCriteriaTimePeriods + { + In = + [ + new DataSetQueryTimePeriod { Period = "2020/2021", Code = "AY" }, + new DataSetQueryTimePeriod { Period = "2022/2023", Code = "AY" }, + ] + }, + } + ] + } + }, + }; + + [Theory] + [MemberData(nameof(EquivalentCriteria))] + public async Task EquivalentCriteria_Returns200(DataSetQueryCriteria criteria) + { + var dataSetVersion = await SetupDefaultDataSetVersion(); + + var response = await QueryDataSet( + dataSetId: dataSetVersion.DataSetId, + request: new DataSetQueryRequest + { + Indicators = ["enrolments", "sess_authorised"], + Debug = true, + Criteria = criteria + } + ); + + var viewModel = response.AssertOk(useSystemJson: true); + + var result = Assert.Single(viewModel.Results); + + Assert.Equal(3, result.Filters.Count); + Assert.Equal("pTSoj :: Year 10", result.Filters["ncyear"]); + Assert.Equal("6jrfe :: State-funded secondary", result.Filters["school_type"]); + Assert.Equal("O7CLF :: Secondary sponsor led academy", result.Filters["academy_type"]); + + Assert.Equal(GeographicLevel.School, result.GeographicLevel); + + Assert.Equal(4, result.Locations.Count); + Assert.Equal("pTSoj :: England (code = E92000001)", result.Locations["NAT"]); + Assert.Equal("IzBzg :: Yorkshire and The Humber (code = E12000003)", result.Locations["REG"]); + Assert.Equal("7zXob :: Sheffield (code = E08000019, oldCode = 373)", result.Locations["LA"]); + Assert.Equal( + "HTzLj :: Newfield Secondary School (urn = 140821, laEstab = 3734008)", + result.Locations["SCH"] + ); + + Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriod.Code); + Assert.Equal("2021/2022", result.TimePeriod.Period); + + Assert.Equal(2, result.Values.Count); + Assert.Equal("752009", result.Values["enrolments"]); + Assert.Equal("262396", result.Values["sess_authorised"]); + } + } + + private async Task QueryDataSet( + Guid dataSetId, + DataSetQueryRequest request, + string? dataSetVersion = null) + { + var query = new Dictionary(); + + if (dataSetVersion is not null) + { + query["dataSetVersion"] = dataSetVersion; + } + + var client = BuildApp().CreateClient(); + + var uri = QueryHelpers.AddQueryString($"{BaseUrl}/{dataSetId}/query", query); + + return await client.PostAsJsonAsync(uri, request); + } + + private async Task SetupDefaultDataSetVersion( + DataSetVersionStatus versionStatus = DataSetVersionStatus.Published) + { + DataSet dataSet = DataFixture + .DefaultDataSet() + .WithStatusPublished(); + + await TestApp.AddTestData(context => context.DataSets.Add(dataSet)); + + DataSetVersion dataSetVersion = DataFixture + .DefaultDataSetVersion() + .WithDataSet(dataSet) + .WithMetaSummary( + DataFixture.DefaultDataSetVersionMetaSummary() + .WithGeographicLevels( + [ + GeographicLevel.Country, + GeographicLevel.LocalAuthority, + GeographicLevel.Region, + GeographicLevel.School + ] + ) + ) + .WithStatus(versionStatus); + + dataSet.LatestLiveVersion = dataSetVersion; + + await TestApp.AddTestData( + context => + { + context.DataSetVersions.Add(dataSetVersion); + context.DataSets.Update(dataSet); + } + ); + + return dataSetVersion; + } + + private WebApplicationFactory BuildApp() + { + return TestApp.ConfigureServices(services => + services.ReplaceService(_parquetPathResolver)); + } + + private static QueryResultsMeta GatherQueryResultsMeta(DataSetQueryPaginatedResultsViewModel viewModel) + { + var filters = new Dictionary>(); + var locations = new Dictionary>(); + var geographicLevels = new HashSet(); + var timePeriods = new HashSet(); + var indicators = new HashSet(); + + foreach (var result in viewModel.Results) + { + foreach (var filter in result.Filters) + { + if (!filters.ContainsKey(filter.Key)) + { + filters[filter.Key] = [filter.Value]; + } + else + { + filters[filter.Key].Add(filter.Value); + } + } + + foreach (var location in result.Locations) + { + if (!locations.ContainsKey(location.Key)) + { + locations[location.Key] = [location.Value]; + } + else + { + locations[location.Key].Add(location.Value); + } + } + + geographicLevels.Add(result.GeographicLevel); + timePeriods.Add(result.TimePeriod); + indicators.AddRange(result.Values.Keys); + } + + return new QueryResultsMeta + { + Filters = filters, + Indicators = indicators, + Locations = locations, + GeographicLevels = geographicLevels, + TimePeriods = timePeriods, + }; + } + + private record QueryResultsMeta + { + public required Dictionary> Filters { get; init; } = []; + public required Dictionary> Locations { get; init; } = []; + public required HashSet GeographicLevels { get; init; } = []; + public required HashSet TimePeriods { get; init; } = []; + public required HashSet Indicators { get; init; } = []; + } +} From d50af6bd23ff527d5f55e9da330eafd8fb3c1c56 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Thu, 16 May 2024 16:31:14 +0100 Subject: [PATCH 30/66] EES-4974 Update Data set details page table tool link --- .../Controllers/DataSetFilesControllerTests.cs | 5 +++++ .../DataSetFileService.cs | 1 + .../DataSetFileViewModel.cs | 2 ++ .../src/modules/data-catalogue/DataSetFilePage.tsx | 2 +- .../src/modules/data-catalogue/__data__/testDataSets.ts | 7 ++++++- .../data-catalogue/__tests__/DataSetFilePage.test.tsx | 5 ++++- .../src/services/dataSetFileService.ts | 2 +- 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs index e9fc1009197..bbda65ab07f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs @@ -1828,6 +1828,7 @@ public async Task FetchDataSetDetails_Success() .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta()) .WithPublicApiDataSetId(Guid.NewGuid()) .WithPublicApiDataSetVersion(major: 1, minor: 0) + .WithSubjectId(Guid.NewGuid()) ); var client = BuildApp() @@ -1850,6 +1851,7 @@ public async Task FetchDataSetDetails_Success() Assert.Equal(file.Id, viewModel.File.Id); Assert.Equal(file.Filename, viewModel.File.Name); Assert.Equal(file.DisplaySize(), viewModel.File.Size); + Assert.Equal(file.SubjectId, viewModel.File.SubjectId); Assert.Equal(releaseFile.ReleaseVersionId, viewModel.Release.Id); Assert.Equal(releaseFile.ReleaseVersion.Title, viewModel.Release.Title); @@ -1916,6 +1918,7 @@ public async Task FetchDataSetFiltersOrdered_Success() new FilterSequenceEntry(filter3Id, new List()), ]) .WithFile(_fixture.DefaultFile() + .WithSubjectId(Guid.NewGuid()) .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta() .WithFilters([ new FilterMeta { Id = filter3Id, Label = "Filter 3", }, @@ -1967,6 +1970,7 @@ public async Task FetchDataSetIndicatorsOrdered_Success() new List { indicator3Id, indicator4Id }) ]) .WithFile(_fixture.DefaultFile() + .WithSubjectId(Guid.NewGuid()) .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta() .WithIndicators([ new IndicatorMeta { Id = indicator3Id, Label = "Indicator 3", }, @@ -2061,6 +2065,7 @@ public async Task AmendmentNotPublished_ReturnsOk() .WithTheme(_fixture.DefaultTheme())); File file = _fixture.DefaultFile() + .WithSubjectId(Guid.NewGuid()) .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta()); ReleaseFile releaseFile0 = _fixture.DefaultReleaseFile() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs index 1f0168a5b37..75c7f2e7c2e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs @@ -187,6 +187,7 @@ public async Task> GetDataSetFile( Id = releaseFile.FileId, Name = releaseFile.File.Filename, Size = releaseFile.File.DisplaySize(), + SubjectId = releaseFile.File.SubjectId!.Value, }, Meta = BuildDataSetFileMetaViewModel( releaseFile.File.DataSetFileMeta, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs index 21ae5414df0..e5fca02b087 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileViewModel.cs @@ -57,4 +57,6 @@ public record DataSetFileFileViewModel public required string Name { get; init; } public required string Size { get; init; } + + public required Guid SubjectId { get; init; } } diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataSetFilePage.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataSetFilePage.tsx index 89560e2d7fe..7dcbd326569 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataSetFilePage.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataSetFilePage.tsx @@ -295,7 +295,7 @@ export default function DataSetFilePage({ dataSetFileId }: Props) { description="View tables that we have built for you, or create your own tables from open data using our table tool" link={ View or create your own tables diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts index 160bc110803..6e016d339d8 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__data__/testDataSets.ts @@ -111,7 +111,12 @@ export const testDataSetFileSummaries: DataSetFileSummary[] = [ export const testDataSet: DataSetFile = { id: 'datasetfile-id', - file: { id: 'file-id', name: 'file name', size: 'file size' }, + file: { + id: 'file-id', + name: 'file name', + size: 'file size', + subjectId: 'subject-id', + }, release: { id: 'release-id', isLatestPublishedRelease: true, diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataSetFilePage.test.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataSetFilePage.test.tsx index 56f41ac67e5..cfe0f730652 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataSetFilePage.test.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataSetFilePage.test.tsx @@ -81,7 +81,10 @@ describe('DataSetFilePage', () => { screen.getByRole('link', { name: 'View or create your own tables', }), - ).toHaveAttribute('href', '/data-tables/publication-slug/release-slug'); + ).toHaveAttribute( + 'href', + '/data-tables/publication-slug/release-slug?subjectId=subject-id', + ); const nav = within( screen.getByRole('navigation', { name: 'On this page' }), diff --git a/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts b/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts index cc75be15edf..701f8cb8658 100644 --- a/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts +++ b/src/explore-education-statistics-frontend/src/services/dataSetFileService.ts @@ -7,7 +7,7 @@ export interface DataSetFile { id: string; title: string; summary: string; - file: { id: string; name: string; size: string }; + file: { id: string; name: string; size: string; subjectId: string }; release: { id: string; isLatestPublishedRelease: boolean; From 4352c491fb8a244e678d1e034c133ea08e5a2083 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Tue, 14 May 2024 08:38:58 +0100 Subject: [PATCH 31/66] EES-4996 add api info to data set page --- .../status/MethodologyStatusPage.tsx | 11 +- .../PublicationUpdateConfirmModal.tsx | 4 +- .../src/pages/release/ReleaseStatusPage.tsx | 11 +- .../ReleasePreReleaseAccessPage.tsx | 27 +- .../src/components/UrlContainer.tsx | 36 +- .../src/services/api/index.ts | 6 + .../.env | 3 + .../next.config.js | 3 + .../data-catalogue/DataSetFilePage.tsx | 258 +++++++-------- .../data-catalogue/__data__/testDataSets.ts | 60 +++- .../__tests__/DataSetFilePage.test.tsx | 313 +++++++++++++----- .../components/DataSetFileDetails.tsx | 114 +++++++ .../components/DataSetFilePageNav.tsx | 13 +- .../components/DataSetFilePageSection.tsx | 13 +- .../components/DataSetFileQuickStart.tsx | 107 ++++++ .../components/DataSetFileUsingData.tsx | 54 +++ .../components/DataSetFileVersionHistory.tsx | 60 ++++ .../__tests__/DataSetFileDetails.test.tsx | 64 ++++ ...v.test.tsx => DataSetFilePageNav.test.tsx} | 44 ++- .../__tests__/DataSetFileQuickStart.test.tsx | 57 ++++ .../__tests__/DataSetFileUsingData.test.tsx | 96 ++++++ .../DataSetFileVersionHistory.test.tsx | 43 +++ .../table-tool/components/TableToolShare.tsx | 11 +- .../data-set/[dataSetFileId]/[version].tsx | 4 + .../src/queries/apiDataSetQueries.ts | 37 +++ .../src/services/apiDataSetService.ts | 84 +++++ 26 files changed, 1239 insertions(+), 294 deletions(-) create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileDetails.tsx create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileQuickStart.tsx create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileUsingData.tsx create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/DataSetFileVersionHistory.tsx create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetFileDetails.test.tsx rename src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/{DataSetPageNav.test.tsx => DataSetFilePageNav.test.tsx} (53%) create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetFileQuickStart.test.tsx create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetFileUsingData.test.tsx create mode 100644 src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetFileVersionHistory.test.tsx create mode 100644 src/explore-education-statistics-frontend/src/pages/data-catalogue/data-set/[dataSetFileId]/[version].tsx create mode 100644 src/explore-education-statistics-frontend/src/queries/apiDataSetQueries.ts create mode 100644 src/explore-education-statistics-frontend/src/services/apiDataSetService.ts diff --git a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/MethodologyStatusPage.tsx b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/MethodologyStatusPage.tsx index c023b0b2564..e1a244bc7f3 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/MethodologyStatusPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/edit-methodology/status/MethodologyStatusPage.tsx @@ -93,12 +93,11 @@ const MethodologyStatusPage = () => { The public methodology will be accessible at:

-

- -

+ diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationUpdateConfirmModal.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationUpdateConfirmModal.tsx index 3dbcbbb17ee..336d6a28a93 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/PublicationUpdateConfirmModal.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/PublicationUpdateConfirmModal.tsx @@ -48,12 +48,12 @@ export default function PublicationUpdateConfirmModal({ <>

The URL for this publication will change from

{' '} to{' '} diff --git a/src/explore-education-statistics-admin/src/pages/release/ReleaseStatusPage.tsx b/src/explore-education-statistics-admin/src/pages/release/ReleaseStatusPage.tsx index 7a1165b7db5..e18ef024864 100644 --- a/src/explore-education-statistics-admin/src/pages/release/ReleaseStatusPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/ReleaseStatusPage.tsx @@ -94,12 +94,11 @@ const ReleaseStatusPage = () => { The public release will be accessible at:

-

- -

+ diff --git a/src/explore-education-statistics-admin/src/pages/release/pre-release/ReleasePreReleaseAccessPage.tsx b/src/explore-education-statistics-admin/src/pages/release/pre-release/ReleasePreReleaseAccessPage.tsx index f9725920c13..cd49c2f1169 100644 --- a/src/explore-education-statistics-admin/src/pages/release/pre-release/ReleasePreReleaseAccessPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/pre-release/ReleasePreReleaseAccessPage.tsx @@ -73,20 +73,19 @@ const ReleasePreReleaseAccessPage = () => { The pre-release will be accessible at:

-

- ( - preReleaseContentRoute.path, - { - publicationId: release.publicationId, - releaseId: release.id, - }, - )}`} - /> -

+ ( + preReleaseContentRoute.path, + { + publicationId: release.publicationId, + releaseId: release.id, + }, + )}`} + /> )} diff --git a/src/explore-education-statistics-common/src/components/UrlContainer.tsx b/src/explore-education-statistics-common/src/components/UrlContainer.tsx index bd5d7768eb0..c75e6495adf 100644 --- a/src/explore-education-statistics-common/src/components/UrlContainer.tsx +++ b/src/explore-education-statistics-common/src/components/UrlContainer.tsx @@ -1,35 +1,49 @@ +import styles from '@common/components/UrlContainer.module.scss'; import classNames from 'classnames'; -import React from 'react'; -import styles from './UrlContainer.module.scss'; +import React, { ReactNode } from 'react'; interface Props { - 'data-testid'?: string; className?: string; + label?: string | ReactNode; + labelHidden?: boolean; + testId?: string; url: string; } const UrlContainer = ({ - 'data-testid': dataTestId = 'url', className, + label = 'Url', + labelHidden = true, + testId = 'url', url, }: Props) => { const handleFocus = (event: React.FocusEvent) => event.target.select(); return ( - <> -
diff --git a/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected3.tsx b/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected3.tsx index ac34359495b..c95714e324d 100644 --- a/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected3.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected3.tsx @@ -578,6 +578,7 @@ const PrototypeHomepage = () => {

Data set summary

@@ -588,11 +589,13 @@ const PrototypeHomepage = () => {
Using GET
Using POST
diff --git a/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected4.tsx b/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected4.tsx index 909cd26c915..d5f6f973345 100644 --- a/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected4.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected4.tsx @@ -668,6 +668,7 @@ const PrototypeHomepage = () => {
GET
@@ -679,6 +680,7 @@ const PrototypeHomepage = () => {
GET
@@ -691,6 +693,7 @@ const PrototypeHomepage = () => {
GET
@@ -702,6 +705,7 @@ const PrototypeHomepage = () => {
POST
diff --git a/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected5.tsx b/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected5.tsx index 0fe662f4a94..93bceaf0794 100644 --- a/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected5.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/PrototypeDataSelected5.tsx @@ -346,7 +346,7 @@ const PrototypeHomepage = () => { : 'Show all sections on page'} - + */} {sectionShowAll && ( <> @@ -636,7 +636,7 @@ const PrototypeHomepage = () => {
- {/* + {/* @@ -1102,6 +1103,7 @@ const PrototypeHomepage = () => { @@ -1114,6 +1116,7 @@ const PrototypeHomepage = () => { @@ -1125,6 +1128,7 @@ const PrototypeHomepage = () => { diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAPIDataSetPreview.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAPIDataSetPreview.tsx index 42385238046..12d4baa8f1d 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAPIDataSetPreview.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypeAPIDataSetPreview.tsx @@ -721,6 +721,7 @@ const PrototypeAPIDataSetPreview = ({
GET
@@ -732,6 +733,7 @@ const PrototypeAPIDataSetPreview = ({
GET
@@ -744,6 +746,7 @@ const PrototypeAPIDataSetPreview = ({
GET
@@ -755,6 +758,7 @@ const PrototypeAPIDataSetPreview = ({
POST
diff --git a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewExample.tsx b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewExample.tsx index b4fe3fd62a1..950ec77c568 100644 --- a/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewExample.tsx +++ b/src/explore-education-statistics-admin/src/prototypes/admin-api/components/PrototypePreviewExample.tsx @@ -115,6 +115,7 @@ const PrototypePreviewExample = () => { diff --git a/src/explore-education-statistics-common/src/components/CopyLinkButton.tsx b/src/explore-education-statistics-common/src/components/CopyLinkButton.tsx index 5860bcc508a..fd9c4df9e2b 100644 --- a/src/explore-education-statistics-common/src/components/CopyLinkButton.tsx +++ b/src/explore-education-statistics-common/src/components/CopyLinkButton.tsx @@ -30,7 +30,7 @@ const CopyLinkButton = ({ className, url }: Props) => {
- +