From 7372f24894f9ae3594d04ba22dca7f88f01e3738 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Thu, 19 Dec 2024 11:54:52 +0000 Subject: [PATCH 01/40] EES-5580 validate EditableContentForm on submit after reinitialisation --- .../src/components/editable/EditableContentForm.tsx | 1 + .../src/components/form/FormFieldEditor.tsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 658e0aaaf4f..329fa85c165 100644 --- a/src/explore-education-statistics-admin/src/components/editable/EditableContentForm.tsx +++ b/src/explore-education-statistics-admin/src/components/editable/EditableContentForm.tsx @@ -289,6 +289,7 @@ const EditableContentForm = ({ onChange={setElements} onCancelComment={toggleCommentAddForm.off} onClickAddComment={toggleCommentAddForm.on} + onElementsReady={setElements} onImageUpload={onImageUpload} onImageUploadCancel={onImageUploadCancel} /> 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 bc969779e6f..678339f261f 100644 --- a/src/explore-education-statistics-admin/src/components/form/FormFieldEditor.tsx +++ b/src/explore-education-statistics-admin/src/components/form/FormFieldEditor.tsx @@ -42,6 +42,7 @@ export default function FormFieldEditor({ testId, onBlur, onChange, + onElementsReady, ...props }: Props) { const { @@ -86,7 +87,10 @@ export default function FormFieldEditor({ } }} onElementsChange={handleElements} - onElementsReady={handleElements} + onElementsReady={els => { + handleElements(els); + onElementsReady?.(els); + }} onChange={nextValue => { setValue( name, From 898a54f80eff80805ac74cb797a07004288a3a18 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Thu, 19 Dec 2024 13:39:59 +0000 Subject: [PATCH 02/40] EES-5457 show extra create data block link when more than 5 --- .../datablocks/ReleaseDataBlocksPage.tsx | 2 +- .../__tests__/ReleaseDataBlocksPage.test.tsx | 242 +++++++++++------- 2 files changed, 146 insertions(+), 98 deletions(-) diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/ReleaseDataBlocksPage.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/ReleaseDataBlocksPage.tsx index 2c27275ff62..94545abcda5 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/ReleaseDataBlocksPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/ReleaseDataBlocksPage.tsx @@ -128,7 +128,7 @@ const ReleaseDataBlocksPage = ({

- {canUpdateRelease && filteredDataBlocks.length > 5 && ( + {canUpdateRelease && dataBlocks.length > 5 && ( Create data block )} diff --git a/src/explore-education-statistics-admin/src/pages/release/datablocks/__tests__/ReleaseDataBlocksPage.test.tsx b/src/explore-education-statistics-admin/src/pages/release/datablocks/__tests__/ReleaseDataBlocksPage.test.tsx index 11d757e419d..675d8b950c7 100644 --- a/src/explore-education-statistics-admin/src/pages/release/datablocks/__tests__/ReleaseDataBlocksPage.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/datablocks/__tests__/ReleaseDataBlocksPage.test.tsx @@ -14,7 +14,6 @@ import _permissionService from '@admin/services/permissionService'; import render from '@common-test/render'; import { waitFor } from '@testing-library/dom'; import { screen, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import React from 'react'; import { generatePath, MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; @@ -218,6 +217,10 @@ describe('ReleaseDataBlocksPage', () => { name: 'Delete block', }), ).toBeInTheDocument(); + + expect( + screen.getByRole('link', { name: 'Create data block' }), + ).toBeInTheDocument(); }); test('renders page correctly when release cannot be updated', async () => { @@ -334,9 +337,28 @@ describe('ReleaseDataBlocksPage', () => { ).toBeInTheDocument(); }); - test('clicking `Delete block` button shows modal', async () => { - dataBlockService.listDataBlocks.mockResolvedValue(testDataBlocks); - dataBlockService.getDeleteBlockPlan.mockResolvedValue(testBlock1DeletePlan); + test('renders an extra "Create data block" button when there are more than 5 blocks', async () => { + dataBlockService.listDataBlocks.mockResolvedValue([ + ...testDataBlocks, + { + id: 'block-5', + name: 'Block 5', + created: '2021-02-01T15:00:00.0000000', + heading: 'Block 5 heading', + source: 'Block 5 source', + inContent: false, + chartsCount: 0, + }, + { + id: 'block-6', + name: 'Block 6', + created: '2021-02-01T15:00:00.0000000', + heading: 'Block 6 heading', + source: 'Block 6 source', + inContent: false, + chartsCount: 0, + }, + ]); featuredTableService.listFeaturedTables.mockResolvedValue( testFeaturedTables, ); @@ -347,122 +369,148 @@ describe('ReleaseDataBlocksPage', () => { expect(screen.getByTestId('dataBlocks')).toBeInTheDocument(); }); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(dataBlockService.getDeleteBlockPlan).toHaveBeenCalledTimes(0); - - const buttons = screen.getAllByRole('button', { name: 'Delete block' }); - - await userEvent.click(buttons[0]); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); - - expect(dataBlockService.getDeleteBlockPlan).toHaveBeenCalledTimes(1); - - const modal = within(screen.getByRole('dialog')); - - expect(modal.getByTestId('deleteDataBlock-name')).toHaveTextContent( - 'Block 1', - ); expect( - modal.getByTestId('deleteDataBlock-contentSectionHeading'), - ).toHaveTextContent('Section 1'); + screen.getAllByRole('link', { name: 'Create data block' }), + ).toHaveLength(2); }); - test('clicking `Cancel` button hides modal', async () => { - dataBlockService.listDataBlocks.mockResolvedValue(testDataBlocks); - dataBlockService.getDeleteBlockPlan.mockResolvedValue(testBlock1DeletePlan); - featuredTableService.listFeaturedTables.mockResolvedValue( - testFeaturedTables, - ); + describe('deleting a data block', () => { + test('clicking `Delete block` button shows modal', async () => { + dataBlockService.listDataBlocks.mockResolvedValue(testDataBlocks); + dataBlockService.getDeleteBlockPlan.mockResolvedValue( + testBlock1DeletePlan, + ); + featuredTableService.listFeaturedTables.mockResolvedValue( + testFeaturedTables, + ); - renderPage(); + const { user } = renderPage(); - await waitFor(() => { - expect(screen.getByTestId('dataBlocks')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByTestId('dataBlocks')).toBeInTheDocument(); + }); - const buttons = screen.getAllByRole('button', { name: 'Delete block' }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(dataBlockService.getDeleteBlockPlan).toHaveBeenCalledTimes(0); - await userEvent.click(buttons[0]); + const buttons = screen.getAllByRole('button', { name: 'Delete block' }); - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); + await user.click(buttons[0]); - const modal = within(screen.getByRole('dialog')); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); - await userEvent.click(modal.getByRole('button', { name: 'Cancel' })); + expect(dataBlockService.getDeleteBlockPlan).toHaveBeenCalledTimes(1); - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + const modal = within(screen.getByRole('dialog')); + + expect(modal.getByTestId('deleteDataBlock-name')).toHaveTextContent( + 'Block 1', + ); + expect( + modal.getByTestId('deleteDataBlock-contentSectionHeading'), + ).toHaveTextContent('Section 1'); }); - }); - test('clicking `Confirm` button hides modal and deletes data block', async () => { - dataBlockService.listDataBlocks.mockResolvedValue(testDataBlocks); - dataBlockService.getDeleteBlockPlan.mockResolvedValue(testBlock1DeletePlan); - featuredTableService.listFeaturedTables.mockResolvedValue( - testFeaturedTables, - ); + test('clicking `Cancel` button hides modal', async () => { + dataBlockService.listDataBlocks.mockResolvedValue(testDataBlocks); + dataBlockService.getDeleteBlockPlan.mockResolvedValue( + testBlock1DeletePlan, + ); + featuredTableService.listFeaturedTables.mockResolvedValue( + testFeaturedTables, + ); - renderPage(); + const { user } = renderPage(); - await waitFor(() => { - expect(screen.getByTestId('dataBlocks')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByTestId('dataBlocks')).toBeInTheDocument(); + }); - const buttons = screen.getAllByRole('button', { name: 'Delete block' }); + const buttons = screen.getAllByRole('button', { name: 'Delete block' }); - await userEvent.click(buttons[0]); + await user.click(buttons[0]); - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); - const modal = within(screen.getByRole('dialog')); + const modal = within(screen.getByRole('dialog')); - await userEvent.click(modal.getByRole('button', { name: 'Confirm' })); + await user.click(modal.getByRole('button', { name: 'Cancel' })); - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); }); - expect(dataBlockService.deleteDataBlock).toHaveBeenCalledTimes(1); - expect(dataBlockService.deleteDataBlock).toHaveBeenCalledWith( - 'release-1', - 'block-1', - ); - - const featuredTablesTable = within(screen.getByTestId('featuredTables')); - const featuredTablesRows = featuredTablesTable.getAllByRole('row'); - expect(featuredTablesRows).toHaveLength(2); - - const featuredTablesRow1Cells = within(featuredTablesRows[1]).getAllByRole( - 'cell', - ); - expect(featuredTablesRow1Cells).toHaveLength(6); - expect(featuredTablesRow1Cells[0]).toHaveTextContent('Block 3'); - expect(featuredTablesRow1Cells[1]).toHaveTextContent('No'); - expect(featuredTablesRow1Cells[2]).toHaveTextContent('No'); - expect(featuredTablesRow1Cells[3]).toHaveTextContent('Featured 3'); - expect(featuredTablesRow1Cells[4]).toHaveTextContent( - '1 January 2021 15:00', - ); - expect( - within(featuredTablesRow1Cells[5]).getByRole('link', { - name: 'Edit block', - }), - ).toHaveAttribute( - 'href', - '/publication/publication-1/release/release-1/data-blocks/block-3', - ); - expect( - within(featuredTablesRow1Cells[5]).getByRole('button', { - name: 'Delete block', - }), - ).toBeInTheDocument(); + test('clicking `Confirm` button hides modal and deletes data block', async () => { + dataBlockService.listDataBlocks.mockResolvedValue(testDataBlocks); + dataBlockService.getDeleteBlockPlan.mockResolvedValue( + testBlock1DeletePlan, + ); + featuredTableService.listFeaturedTables.mockResolvedValue( + testFeaturedTables, + ); + + const { user } = renderPage(); + + await waitFor(() => { + expect(screen.getByTestId('dataBlocks')).toBeInTheDocument(); + }); + + const buttons = screen.getAllByRole('button', { name: 'Delete block' }); + + await user.click(buttons[0]); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + const modal = within(screen.getByRole('dialog')); + + await user.click(modal.getByRole('button', { name: 'Confirm' })); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + expect(dataBlockService.deleteDataBlock).toHaveBeenCalledTimes(1); + expect(dataBlockService.deleteDataBlock).toHaveBeenCalledWith( + 'release-1', + 'block-1', + ); + + const featuredTablesTable = within(screen.getByTestId('featuredTables')); + const featuredTablesRows = featuredTablesTable.getAllByRole('row'); + expect(featuredTablesRows).toHaveLength(2); + + const featuredTablesRow1Cells = within( + featuredTablesRows[1], + ).getAllByRole('cell'); + expect(featuredTablesRow1Cells).toHaveLength(6); + expect(featuredTablesRow1Cells[0]).toHaveTextContent('Block 3'); + expect(featuredTablesRow1Cells[1]).toHaveTextContent('No'); + expect(featuredTablesRow1Cells[2]).toHaveTextContent('No'); + expect(featuredTablesRow1Cells[3]).toHaveTextContent('Featured 3'); + expect(featuredTablesRow1Cells[4]).toHaveTextContent( + '1 January 2021 15:00', + ); + expect( + within(featuredTablesRow1Cells[5]).getByRole('link', { + name: 'Edit block', + }), + ).toHaveAttribute( + 'href', + '/publication/publication-1/release/release-1/data-blocks/block-3', + ); + expect( + within(featuredTablesRow1Cells[5]).getByRole('button', { + name: 'Delete block', + }), + ).toBeInTheDocument(); + }); }); const renderPage = () => { From 0778874667513128d715535d8173303317697ec8 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Fri, 13 Dec 2024 11:13:35 +0000 Subject: [PATCH 03/40] EES-5738 Create new DataSetFileGeographicLevels table --- ...taSetFileGeographicLevelsTable.Designer.cs | 2272 +++++++++++++++++ ..._CreateDataSetFileGeographicLevelsTable.cs | 43 + .../ContentDbContextModelSnapshot.cs | 29 + .../DataSetFileGeographicLevels.cs | 17 + .../Database/ContentDbContext.cs | 18 +- .../File.cs | 3 + 6 files changed, 2380 insertions(+), 2 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs new file mode 100644 index 00000000000..dcc66a1e236 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs @@ -0,0 +1,2272 @@ +// +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("20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable")] + partial class EES5738_CreateDataSetFileGeographicLevelsTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .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"); + + 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.DataSetFileGeographicLevel", b => + { + b.Property("DataSetFileVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("GeographicLevel") + .HasColumnType("int"); + + b.HasKey("DataSetFileVersionId", "GeographicLevel"); + + b.ToTable("DataSetFileGeographicLevels"); + }); + + 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("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.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("ThemeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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("ThemeId"); + + 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("Label") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(81) + .HasColumnType("nvarchar(81)"); + + b.Property("TimePeriodCoverage") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PublicationId", "Year", "TimePeriodCoverage", "Label") + .IsUnique(); + + 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("PublicApiDataSetId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicApiDataSetVersion") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ReleaseVersionId", "FileId") + .IsUnique(); + + b.HasIndex("ReleaseVersionId", "PublicApiDataSetId", "PublicApiDataSetVersion") + .IsUnique() + .HasFilter("[PublicApiDataSetId] IS NOT NULL AND [PublicApiDataSetVersion] IS NOT NULL"); + + b.ToTable("ReleaseFiles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseRedirect", b => + { + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.HasKey("ReleaseId", "Slug"); + + b.ToTable("ReleaseRedirects"); + }); + + 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.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("DeletedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeletedById"); + + 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") + .WithMany() + .HasForeignKey("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.DataSetFileGeographicLevel", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "DataSetFileVersion") + .WithMany("DataSetFileGeographicLevels") + .HasForeignKey("DataSetFileVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataSetFileVersion"); + }); + + 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("MethodologyRedirects") + .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.Theme", "Theme") + .WithMany("Publications") + .HasForeignKey("ThemeId") + .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("Theme"); + }); + + 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("PublicationRedirects") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("Releases") + .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.ReleaseRedirect", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("ReleaseRedirects") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Release"); + }); + + 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.NoAction) + .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.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.User", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById"); + + b.Navigation("DeletedBy"); + }); + + 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.File", b => + { + b.Navigation("DataSetFileGeographicLevels"); + }); + + 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("MethodologyRedirects"); + + b.Navigation("Notes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.Navigation("Methodologies"); + + b.Navigation("PublicationRedirects"); + + b.Navigation("ReleaseVersions"); + + b.Navigation("Releases"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.Navigation("ReleaseRedirects"); + + 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("Publications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs new file mode 100644 index 00000000000..b4a23fa687c --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations +{ + /// + public partial class EES5738_CreateDataSetFileGeographicLevelsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DataSetFileGeographicLevels", + columns: table => new + { + DataSetFileVersionId = table.Column(type: "uniqueidentifier", nullable: false), + GeographicLevel = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DataSetFileGeographicLevels", x => new { x.DataSetFileVersionId, x.GeographicLevel }); + table.ForeignKey( + name: "FK_DataSetFileGeographicLevels_Files_DataSetFileVersionId", + column: x => x.DataSetFileVersionId, + principalTable: "Files", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql("GRANT SELECT ON dbo.DataSetFileGeographicLevels TO [content];"); + migrationBuilder.Sql("GRANT INSERT ON dbo.DataSetFileGeographicLevels TO [importer];"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("REVOKE SELECT ON dbo.DataSetFileGeographicLevels TO [content];"); + migrationBuilder.Sql("REVOKE INSERT ON dbo.DataSetFileGeographicLevels TO [importer];"); + migrationBuilder.DropTable( + name: "DataSetFileGeographicLevels"); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs index 2877ed7a1a1..75b3a97870b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs @@ -328,6 +328,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DataImportErrors"); }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileGeographicLevel", b => + { + b.Property("DataSetFileVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("GeographicLevel") + .HasColumnType("int"); + + b.HasKey("DataSetFileVersionId", "GeographicLevel"); + + b.ToTable("DataSetFileGeographicLevels"); + }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", b => { b.Property("Id") @@ -1594,6 +1607,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("DataImport"); }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileGeographicLevel", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "DataSetFileVersion") + .WithMany("DataSetFileGeographicLevels") + .HasForeignKey("DataSetFileVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataSetFileVersion"); + }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", b => { b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") @@ -2180,6 +2204,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Errors"); }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => + { + b.Navigation("DataSetFileGeographicLevels"); + }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => { b.Navigation("Publications"); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs new file mode 100644 index 00000000000..ae83fb59cbc --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs @@ -0,0 +1,17 @@ +#nullable enable +using System; +using GovUk.Education.ExploreEducationStatistics.Common.Converters; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; +using Newtonsoft.Json; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model; + +public class DataSetFileGeographicLevel +{ + public Guid DataSetFileVersionId { get; set; } // Currently Files.Id, but will become DataSetFileVersion.Id in EES-5105 + + public File DataSetFileVersion { get; set; } = null!; + + [JsonConverter(typeof(EnumToEnumValueJsonConverter))] + public GeographicLevel GeographicLevel { get; set; } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs index a86f10300d4..61c71e4f880 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs @@ -55,6 +55,7 @@ private void Configure(bool updateTimestamps = true) public virtual DbSet ReleaseStatus { get; set; } public virtual DbSet ReleaseFiles { get; set; } public virtual DbSet Files { get; set; } + public virtual DbSet DataSetFileGeographicLevels { get; set; } public virtual DbSet ContentSections { get; set; } public virtual DbSet ContentBlocks { get; set; } public virtual DbSet KeyStatistics { get; set; } @@ -110,6 +111,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ConfigureReleaseStatus(modelBuilder); ConfigureReleaseFile(modelBuilder); ConfigureFile(modelBuilder); + ConfigureDataSetFileGeographicLevel(modelBuilder); ConfigureContentBlock(modelBuilder); ConfigureContentSection(modelBuilder); ConfigureReleaseVersion(modelBuilder); @@ -452,13 +454,25 @@ private static void ConfigureFile(ModelBuilder modelBuilder) v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : null); - entity.Property(p => p.DataSetFileMeta) - .HasConversion( // You might want to use EF8 JSON support instead of this + entity.Property(p => p.DataSetFileMeta) // EES-5666 + .HasConversion( v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject(v)); }); } + private static void ConfigureDataSetFileGeographicLevel(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(gl => new + { + gl.DataSetFileVersionId, + gl.GeographicLevel + }); + }); + } + private static void ConfigureContentBlock(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs index f9a9ac9fd22..8fc1696a46a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; using GovUk.Education.ExploreEducationStatistics.Common.Model; namespace GovUk.Education.ExploreEducationStatistics.Content.Model @@ -22,6 +23,8 @@ public class File : ICreatedTimestamp public int? DataSetFileVersion { get; set; } + public List? DataSetFileGeographicLevels { get; set; } + public DataSetFileMeta? DataSetFileMeta { get; set; } public Guid? ReplacedById { get; set; } From 6e32d38808bdb82be843d0b2634f074632316fb3 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Fri, 13 Dec 2024 11:40:51 +0000 Subject: [PATCH 04/40] EES-5738 Migration to move DataSetFileMeta geog lvls to new table --- ...FileGeographicLevelsMigrationController.cs | 83 +++++++++++++++++++ ...aSetFileGeographicLevelsTable.Designer.cs} | 6 +- ...CreateDataSetFileGeographicLevelsTable.cs} | 2 +- .../ContentDbContextModelSnapshot.cs | 4 +- .../DataSetFileGeographicLevels.cs | 3 - .../Database/ContentDbContext.cs | 3 + 6 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs rename src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/{20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs => 20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs} (99%) rename src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/{20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs => 20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs} (94%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs new file mode 100644 index 00000000000..f1adf49fd34 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Admin.Models; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Controllers.Api; + +[Route("api")] +[ApiController] +[Authorize(Roles = GlobalRoles.RoleNames.BauUser)] +public class DataSetFileMetaMigrationController(ContentDbContext contentDbContext) : ControllerBase +{ + public class MigrationResult + { + public bool IsDryRun; + public int Processed; + public string Errors; + } + + [HttpPut("bau/migrate-datasetfile-geographiclevels")] + public async Task DataSetFileGeographicLevelsMigration( + [FromQuery] bool isDryRun = true, + [FromQuery] int? num = null, + CancellationToken cancellationToken = default) + { + var queryable = contentDbContext.Files + .Where(f => f.DataSetFileMeta != null + && f.DataSetFileGeographicLevels.Count == 0); + + if (num != null) + { + queryable = queryable.Take(num.Value); + } + + var files = queryable.ToList(); + + var numProcessed = 0; + List errors = []; + + foreach (var file in files) + { + var meta = file.DataSetFileMeta; + + if (meta == null) + { + errors.Add($"No DataSetFileMeta found for File {file.Id}"); + continue; + } + + var dataSetFileGeographicLevels = meta!.GeographicLevels + .Distinct() + .Select(gl => new DataSetFileGeographicLevel + { + DataSetFileVersionId = file.Id, + GeographicLevel = gl, + }) + .ToList(); + + contentDbContext.DataSetFileGeographicLevels.AddRange(dataSetFileGeographicLevels); + + numProcessed++; + } + + if (!isDryRun) + { + await contentDbContext.SaveChangesAsync(cancellationToken); + } + + return new MigrationResult + { + IsDryRun = isDryRun, + Processed = numProcessed, + Errors = errors.IsNullOrEmpty() ? "No errors" : errors.JoinToString("\n"), + }; + } + +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs similarity index 99% rename from src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs rename to src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs index dcc66a1e236..9c4bfa60215 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs @@ -12,7 +12,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations { [DbContext(typeof(ContentDbContext))] - [Migration("20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable")] + [Migration("20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable")] partial class EES5738_CreateDataSetFileGeographicLevelsTable { /// @@ -336,8 +336,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("DataSetFileVersionId") .HasColumnType("uniqueidentifier"); - b.Property("GeographicLevel") - .HasColumnType("int"); + b.Property("GeographicLevel") + .HasColumnType("nvarchar(450)"); b.HasKey("DataSetFileVersionId", "GeographicLevel"); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs similarity index 94% rename from src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs rename to src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs index b4a23fa687c..1434036a2a3 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213112036_EES5738_CreateDataSetFileGeographicLevelsTable.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs @@ -14,7 +14,7 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { DataSetFileVersionId = table.Column(type: "uniqueidentifier", nullable: false), - GeographicLevel = table.Column(type: "int", nullable: false) + GeographicLevel = table.Column(type: "nvarchar(450)", nullable: false) }, constraints: table => { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs index 75b3a97870b..bc677140783 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs @@ -333,8 +333,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DataSetFileVersionId") .HasColumnType("uniqueidentifier"); - b.Property("GeographicLevel") - .HasColumnType("int"); + b.Property("GeographicLevel") + .HasColumnType("nvarchar(450)"); b.HasKey("DataSetFileVersionId", "GeographicLevel"); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs index ae83fb59cbc..2b23db07a4a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs @@ -1,8 +1,6 @@ #nullable enable using System; -using GovUk.Education.ExploreEducationStatistics.Common.Converters; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; -using Newtonsoft.Json; namespace GovUk.Education.ExploreEducationStatistics.Content.Model; @@ -12,6 +10,5 @@ public class DataSetFileGeographicLevel public File DataSetFileVersion { get; set; } = null!; - [JsonConverter(typeof(EnumToEnumValueJsonConverter))] public GeographicLevel GeographicLevel { get; set; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs index 61c71e4f880..2cec48a76d7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs @@ -470,6 +470,9 @@ private static void ConfigureDataSetFileGeographicLevel(ModelBuilder modelBuilde gl.DataSetFileVersionId, gl.GeographicLevel }); + + entity.Property(gl => gl.GeographicLevel) + .HasConversion(new EnumToEnumValueConverter()); }); } From 47a3c8e99ae957cfe938442c1bffa27cd0d2f322 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Fri, 13 Dec 2024 12:54:04 +0000 Subject: [PATCH 05/40] EES-5738 Create DataSetFileGL entries in WriteDataSetFileMeta --- .../Services/DataImportServiceTests.cs | 2 +- .../Services/FileImportServiceTests.cs | 5 ++++- .../Services/DataImportService.cs | 11 ++++++++++- .../Services/FileImportService.cs | 4 ++-- .../Services/Interfaces/IDataImportService.cs | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs index f0aabc50536..5aa9ee86309 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs @@ -284,7 +284,7 @@ public async Task WriteDataSetMetaFile_Success() contentDbContextId, statisticsDbContextId); - await service.WriteDataSetFileMeta(subject.Id); + await service.WriteDataSetFileMeta(file.Id, subject.Id); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/FileImportServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/FileImportServiceTests.cs index c33c514b8a8..eaaa1f0c9f7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/FileImportServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/FileImportServiceTests.cs @@ -60,6 +60,7 @@ public async Task CompleteImport() dataImportService .Setup(s => s.WriteDataSetFileMeta( + import.FileId, import.SubjectId)) .Returns(Task.CompletedTask); @@ -228,7 +229,9 @@ await FinishedStatuses var dataImportService = new Mock(Strict); if (finishedStatus == COMPLETE) { - dataImportService.Setup(mock => mock.WriteDataSetFileMeta(import.SubjectId)) + dataImportService.Setup(mock => mock.WriteDataSetFileMeta( + import.FileId, + import.SubjectId)) .Returns(Task.CompletedTask); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs index 301d89cd76a..acb15120605 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs @@ -151,7 +151,7 @@ public async Task UpdateStatus(Guid id, DataImportStatus newStatus, double perce await context.SaveChangesAsync(); } - public async Task WriteDataSetFileMeta(Guid subjectId) + public async Task WriteDataSetFileMeta(Guid fileId, Guid subjectId) { await using var contentDbContext = _dbContextSupplier.CreateDbContext(); await using var statisticsDbContext = _dbContextSupplier.CreateDbContext(); @@ -220,6 +220,15 @@ public async Task WriteDataSetFileMeta(Guid subjectId) .Single(f => f.Type == FileType.Data && f.SubjectId == subjectId); file.DataSetFileMeta = dataSetFileMeta; + + var dataSetFileGeographicLevels = geographicLevels + .Select(gl => new DataSetFileGeographicLevel + { + DataSetFileVersionId = fileId, + GeographicLevel = gl, + }).ToList(); + await contentDbContext.DataSetFileGeographicLevels.AddRangeAsync(dataSetFileGeographicLevels); + await contentDbContext.SaveChangesAsync(); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/FileImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/FileImportService.cs index 44daca0a939..c2c9550cdd0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/FileImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/FileImportService.cs @@ -105,7 +105,7 @@ public async Task CompleteImport(DataImport import, StatisticsDbContext context) if (import.Status == COMPLETE) { - await _dataImportService.WriteDataSetFileMeta(import.SubjectId); + await _dataImportService.WriteDataSetFileMeta(import.FileId, import.SubjectId); } return; @@ -139,7 +139,7 @@ await _dataImportService.FailImport(import.Id, if (import.Errors.Count == 0) { await _dataImportService.UpdateStatus(import.Id, COMPLETE, 100); - await _dataImportService.WriteDataSetFileMeta(import.SubjectId); + await _dataImportService.WriteDataSetFileMeta(import.FileId, import.SubjectId); } else { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/Interfaces/IDataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/Interfaces/IDataImportService.cs index e6ea8ba07ba..1ece36d23a5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/Interfaces/IDataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/Interfaces/IDataImportService.cs @@ -19,7 +19,7 @@ public interface IDataImportService Task UpdateStatus(Guid id, DataImportStatus newStatus, double percentageComplete); - Task WriteDataSetFileMeta(Guid subjectId); + Task WriteDataSetFileMeta(Guid fileId, Guid subjectId); Task Update( Guid id, From c614926726b51b09f43646ab980ac92eb68de86b Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Fri, 13 Dec 2024 14:29:24 +0000 Subject: [PATCH 06/40] EES-5738 Data Catalogue page Geog lvl filtering --- .../FunctionsIntegrationTest.cs | 1 - .../DataSetFilesControllerCachingTests.cs | 2 + .../Controllers/DataSetFilesController.cs | 2 + .../DataSetFileListRequest.cs | 12 ++++ .../DataSetFileService.cs | 62 ++++++++++--------- .../Interfaces/IDataSetFileService.cs | 2 + .../data-catalogue/DataCataloguePage.tsx | 5 ++ .../data-catalogue/components/Filters.tsx | 36 +++++++++++ .../utils/createDataSetFileListRequest.ts | 3 + .../utils/dataSetFileFilters.ts | 1 + .../src/services/dataSetFileService.ts | 2 + 11 files changed, 97 insertions(+), 31 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/FunctionsIntegrationTest.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/FunctionsIntegrationTest.cs index 0a7cda58edf..9516e767bbc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/FunctionsIntegrationTest.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/FunctionsIntegrationTest.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading.Tasks; using Azure.Data.Tables; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; 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 651ba30ac9d..efa82dcb7ee 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerCachingTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerCachingTests.cs @@ -31,6 +31,7 @@ public class ListDataSetsTests : DataSetFilesControllerCachingTests ThemeId: Guid.NewGuid(), PublicationId: Guid.NewGuid(), ReleaseId: Guid.NewGuid(), + GeographicLevel: GeographicLevel.Country.GetEnumValue(), LatestOnly: true, DataSetType: DataSetType.Api, SearchTerm: "term", @@ -116,6 +117,7 @@ public async Task NoCachedEntryExists_CreatesCache() _query.ThemeId, _query.PublicationId, _query.ReleaseId, + _query.GeographicLevelEnum, _query.LatestOnly, _query.DataSetType, _query.SearchTerm, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs index 015a79b28da..db48b8fc809 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/DataSetFilesController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Cache; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; using GovUk.Education.ExploreEducationStatistics.Content.Api.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Requests; @@ -39,6 +40,7 @@ public async Task + GeographicLevel == null + ? null + : EnumUtil.GetFromEnumValue(GeographicLevel); + public class Validator : AbstractValidator { public Validator() @@ -23,6 +32,9 @@ public Validator() .MinimumLength(3); RuleFor(request => request.ReleaseId).NotEmpty() .When(request => request.Sort == DataSetsListRequestSortBy.Natural); + RuleFor(request => request.GeographicLevel) + .AllowedValue(EnumUtil.GetEnumValues()) + .When(request => request.GeographicLevel != null); RuleFor(request => request.SearchTerm).NotEmpty() .When(request => request.Sort == DataSetsListRequestSortBy.Relevance); RuleFor(request => request.Page) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs index ab71aad330e..4a0775ccddd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs @@ -26,35 +26,25 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using static GovUk.Education.ExploreEducationStatistics.Common.Model.SortDirection; using static GovUk.Education.ExploreEducationStatistics.Content.Requests.DataSetsListRequestSortBy; using ReleaseVersion = GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion; namespace GovUk.Education.ExploreEducationStatistics.Content.Services; -public class DataSetFileService : IDataSetFileService +public class DataSetFileService( + ContentDbContext contentDbContext, + IReleaseVersionRepository releaseVersionRepository, + IPublicBlobStorageService publicBlobStorageService, + IFootnoteRepository footnoteRepository) + : IDataSetFileService { - private readonly ContentDbContext _contentDbContext; - private readonly IReleaseVersionRepository _releaseVersionRepository; - private readonly IPublicBlobStorageService _publicBlobStorageService; - private readonly IFootnoteRepository _footnoteRepository; - - public DataSetFileService( - ContentDbContext contentDbContext, - IReleaseVersionRepository releaseVersionRepository, - IPublicBlobStorageService publicBlobStorageService, - IFootnoteRepository footnoteRepository) - { - _contentDbContext = contentDbContext; - _releaseVersionRepository = releaseVersionRepository; - _publicBlobStorageService = publicBlobStorageService; - _footnoteRepository = footnoteRepository; - } - public async Task>> ListDataSetFiles( Guid? themeId, Guid? publicationId, Guid? releaseVersionId, + GeographicLevel? geographicLevel, bool? latestOnly, DataSetType? dataSetType, string? searchTerm, @@ -73,18 +63,19 @@ public async Task rf.Id, searchTerm); + .JoinFreeText(contentDbContext.ReleaseFilesFreeTextTable, rf => rf.Id, searchTerm); var results = await query .OrderBy(sort.Value, sortDirection.Value) @@ -144,10 +135,10 @@ private static Expression, DataSetFileSumm public async Task>> ListSitemapItems( CancellationToken cancellationToken = default) { - var latestReleaseVersions = _contentDbContext.ReleaseVersions + var latestReleaseVersions = contentDbContext.ReleaseVersions .LatestReleaseVersions(publishedOnly: true); - var latestReleaseFiles = _contentDbContext.ReleaseFiles + var latestReleaseFiles = contentDbContext.ReleaseFiles .AsNoTracking() .OfFileType(FileType.Data) .HavingNoDataReplacementInProgress() @@ -176,7 +167,7 @@ private static async Task> ChangeSummaryHtmlTo public async Task> GetDataSetFile(Guid dataSetFileId) { - var releaseFile = await _contentDbContext.ReleaseFiles + var releaseFile = await contentDbContext.ReleaseFiles .Include(rf => rf.ReleaseVersion.Publication.Theme) .Include(rf => rf.ReleaseVersion.Publication.SupersededBy) .Include(rf => rf.File) @@ -188,7 +179,7 @@ public async Task> GetDataSetFile(Gui .FirstOrDefaultAsync(); if (releaseFile == null - || !await _releaseVersionRepository.IsLatestPublishedReleaseVersion( + || !await releaseVersionRepository.IsLatestPublishedReleaseVersion( releaseFile.ReleaseVersionId)) { return new NotFoundResult(); @@ -198,7 +189,7 @@ public async Task> GetDataSetFile(Gui var variables = GetVariables(releaseFile.File.DataSetFileMeta!); - var footnotes = await _footnoteRepository.GetFootnotes( + var footnotes = await footnoteRepository.GetFootnotes( releaseFile.ReleaseVersionId, releaseFile.File.SubjectId); @@ -249,7 +240,7 @@ public async Task> GetDataSetFile(Gui public async Task DownloadDataSetFile( Guid dataSetFileId) { - var releaseFile = await _contentDbContext.ReleaseFiles + var releaseFile = await contentDbContext.ReleaseFiles .Include(rf => rf.File) .Where(rf => rf.File.DataSetFileId == dataSetFileId @@ -259,13 +250,13 @@ public async Task DownloadDataSetFile( .FirstOrDefaultAsync(); if (releaseFile == null - || !await _releaseVersionRepository.IsLatestPublishedReleaseVersion( + || !await releaseVersionRepository.IsLatestPublishedReleaseVersion( releaseFile.ReleaseVersionId)) { return new NotFoundResult(); } - var stream = await _publicBlobStorageService.StreamBlob( + var stream = await publicBlobStorageService.StreamBlob( containerName: BlobContainers.PublicReleaseFiles, path: releaseFile.PublicPath()); @@ -306,7 +297,7 @@ private static DataSetFileMetaViewModel BuildDataSetFileMetaViewModel( private async Task GetDataCsvPreview(ReleaseFile releaseFile) { - var datafileStreamProvider = () => _publicBlobStorageService.StreamBlob( + var datafileStreamProvider = () => publicBlobStorageService.StreamBlob( containerName: BlobContainers.PublicReleaseFiles, path: releaseFile.PublicPath()); @@ -491,6 +482,17 @@ internal static IQueryable HavingReleaseVersionId( return releaseVersionId.HasValue ? query.Where(rf => rf.ReleaseVersionId == releaseVersionId.Value) : query; } + internal static IQueryable HavingGeographicLevel( // @MarkFix write tests + this IQueryable query, + GeographicLevel? geographicLevel) + { + return geographicLevel.HasValue + ? query.Where(rf => rf.File.DataSetFileGeographicLevels!.Any( // @MarkFix null allowing + gl => gl.GeographicLevel == geographicLevel + && rf.FileId == gl.DataSetFileVersionId)) + : query; + } + internal static IQueryable OfDataSetType( this IQueryable query, DataSetType dataSetType) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IDataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IDataSetFileService.cs index 1b5cb2a3372..0a16d4cb669 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IDataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IDataSetFileService.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; using GovUk.Education.ExploreEducationStatistics.Content.Requests; using GovUk.Education.ExploreEducationStatistics.Content.ViewModels; @@ -17,6 +18,7 @@ Task>> Guid? themeId, Guid? publicationId, Guid? releaseVersionId, + GeographicLevel? geographicLevel, bool? latestOnly, DataSetType? dataSetType, string? searchTerm, diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx index e551420f4d2..c14872aa2e0 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx @@ -52,6 +52,7 @@ import omit from 'lodash/omit'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { ParsedUrlQuery } from 'querystring'; +import { GeographicLevelCode } from '@common/utils/locationLevelsMap'; const defaultPageTitle = 'Data catalogue'; @@ -61,6 +62,7 @@ export interface DataCataloguePageQuery { page?: number; publicationId?: string; releaseId?: string; + geographicLevel?: GeographicLevelCode; searchTerm?: string; sortBy?: DataSetFileSortOption; sortDirection?: SortDirection; @@ -85,6 +87,7 @@ const DataCataloguePage: NextPage = ({ showTypeFilter }) => { sortBy, publicationId, releaseId, + geographicLevel, searchTerm, themeId, } = getParamsFromQuery(router.query); @@ -131,6 +134,7 @@ const DataCataloguePage: NextPage = ({ showTypeFilter }) => { searchTerm, selectedTheme?.title, selectedPublication?.title, + geographicLevel, ]).join(', '); const updateQueryParams = async (nextQuery: DataCataloguePageQuery) => { @@ -327,6 +331,7 @@ const DataCataloguePage: NextPage = ({ showTypeFilter }) => { publicationId={publicationId} publications={publications} releaseId={releaseId} + geographicLevel={geographicLevel} releases={releases} showResetFiltersButton={!isMobileMedia && isFiltered} showTypeFilter={showTypeFilter} diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx index 1dc33f5d1ed..c564cbc8fd5 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx @@ -13,6 +13,10 @@ import styles from '@frontend/modules/data-catalogue/components/Filters.module.s import { DataSetFileFilter } from '@frontend/modules/data-catalogue/utils/dataSetFileFilters'; import React from 'react'; import classNames from 'classnames'; +import locationLevelsMap, { + GeographicLevelCode, +} from '@common/utils/locationLevelsMap'; +import typedKeys from '@common/utils/object/typedKeys'; const formId = 'filters-form'; @@ -23,6 +27,7 @@ interface Props { publications?: PublicationTreeSummary[]; releaseId?: string; releases?: ReleaseSummary[]; + geographicLevel?: GeographicLevelCode; showResetFiltersButton?: boolean; showTypeFilter?: boolean; themeId?: string; @@ -44,6 +49,7 @@ export default function Filters({ publicationId, releaseId, releases = [], + geographicLevel, showResetFiltersButton, showTypeFilter, themeId, @@ -140,6 +146,36 @@ export default function Filters({ /> + + + Filter by Geographic level + + } + name="geographicLevel" + options={[ + { label: 'All', value: 'all' }, + ...typedKeys(locationLevelsMap).map(key => { + return { + label: locationLevelsMap[key].label, + value: locationLevelsMap[key].code, + }; + }), + ]} + value={geographicLevel} + order={[]} + onChange={e => { + onChange({ + filterType: 'geographicLevel', + nextValue: e.target.value, + }); + }} + /> + + {showResetFiltersButton && ( Date: Fri, 13 Dec 2024 16:35:11 +0000 Subject: [PATCH 07/40] EES-5738 Add tests --- .../DataSetFilesControllerTests.cs | 47 +++++++++++++++++ .../Fixtures/DataImportGeneratorExtensions.cs | 34 ++++++++++++- ...tFileGeographicLevelGeneratorExtensions.cs | 50 +++++++++++++++++++ .../Fixtures/FileGeneratorExtensions.cs | 23 ++++++++- .../File.cs | 2 +- .../DataSetFileService.cs | 4 +- .../Functions/ProcessorStage3Tests.cs | 21 ++++---- .../Services/DataImportServiceTests.cs | 1 + .../Services/DataImportService.cs | 1 + .../data-catalogue/__data__/testDataSets.ts | 2 +- .../__tests__/DataCataloguePage.test.tsx | 26 ++++++++++ .../__tests__/DataSetSummary.test.tsx | 4 +- 12 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs 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 91a6b434769..b69ddb03ce6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs @@ -95,6 +95,51 @@ await TestApp.AddTestData(context => AssertResultsForExpectedReleaseFiles(publication1Release1Version1Files, pagedResult.Results); } + [Fact] + public async Task FilterByGeographicLevel_Success() + { + var (publication1, publication2) = _fixture + .DefaultPublication() + // Publications each have a published release version + .WithReleases(_fixture.DefaultRelease(publishedVersions: 1) + .Generate(1)) + .WithTheme(_fixture.DefaultTheme()) + .GenerateTuple2(); + + var publication1Release1Version1Files = _fixture.DefaultReleaseFile() + .WithReleaseVersion(publication1.ReleaseVersions[0]) + .WithFiles(_fixture.DefaultFile(FileType.Data) + .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta() + .WithGeographicLevels( + [GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.Institution])) + .WithDataSetFileGeographicLevels( + [GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.Institution]) + .GenerateList(1)) + .GenerateList(); + + var publication2Release1Version1Files = GenerateDataSetFilesForReleaseVersion(publication2.ReleaseVersions[0]); + + await TestApp.AddTestData(context => + { + context.ReleaseFiles.AddRange(publication1Release1Version1Files); + context.ReleaseFiles.AddRange(publication2Release1Version1Files); + }); + + MemoryCacheService + .SetupNotFoundForAnyKey>(); + + var query = new DataSetFileListRequest(GeographicLevel: GeographicLevel.Institution.GetEnumValue()); + var response = await ListDataSetFiles(query); + + MockUtils.VerifyAllMocks(MemoryCacheService); + + var pagedResult = response.AssertOk>(); + + pagedResult.AssertHasExpectedPagingAndResultCount( + expectedTotalResults: 1); + AssertResultsForExpectedReleaseFiles(publication1Release1Version1Files, pagedResult.Results); + } + [Fact] public async Task FilterByPublicationId_Success() { @@ -1675,6 +1720,7 @@ private async Task ListDataSetFiles( { "themeId", request.ThemeId?.ToString() }, { "publicationId", request.PublicationId?.ToString() }, { "releaseId", request.ReleaseId?.ToString() }, + { "geographicLevel", request.GeographicLevel }, { "latestOnly", request.LatestOnly?.ToString() }, { "searchTerm", request.SearchTerm }, { "sort", request.Sort?.ToString() }, @@ -1737,6 +1783,7 @@ private List GenerateDataSetFilesForReleaseVersion( .WithReleaseVersion(releaseVersion) .WithFiles(_fixture.DefaultFile(FileType.Data) .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta()) + .WithDataSetFileGeographicLevels([GeographicLevel.Country]) .GenerateList(numberOfDataSets)) .GenerateList(); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs index 7a02d7410cf..0eab870e422 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs @@ -19,8 +19,15 @@ public static Generator WithSubjectId( public static Generator WithDefaultFiles( this Generator generator, - string dataFileName) - => generator.ForInstance(s => s.SetDefaultFiles(dataFileName)); + string dataFileName, + bool metaSet = true) + { + if (metaSet) + { + return generator.ForInstance(s => s.SetDefaultFiles(dataFileName)); + } + return generator.ForInstance(s => s.SetDefaultFilesWithoutMeta(dataFileName)); + } public static Generator WithFile( this Generator generator, @@ -98,6 +105,29 @@ public static InstanceSetters SetDefaultFiles( ) .Set(d => d.MetaFileId, (_, d) => d.MetaFile.Id); + public static InstanceSetters SetDefaultFilesWithoutMeta( + this InstanceSetters setters, + string dataFileName) + => setters + .Set( + d => d.File, + (_, d, context) => context.Fixture + .DefaultFile(FileType.Data) + .WithDataSetFileMeta(null) + .WithDataSetFileGeographicLevels([]) + .WithFilename($"{dataFileName}.csv") + .WithSubjectId(d.SubjectId) + ) + .Set(d => d.FileId, (_, d) => d.File.Id) + .Set( + d => d.MetaFile, + (_, d, context) => context.Fixture + .DefaultFile(FileType.Metadata) + .WithFilename($"{dataFileName}.meta.csv") + .WithSubjectId(d.SubjectId) + ) + .Set(d => d.MetaFileId, (_, d) => d.MetaFile.Id); + public static InstanceSetters SetFile( this InstanceSetters setters, File file) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs new file mode 100644 index 00000000000..b9c7d83a357 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs @@ -0,0 +1,50 @@ +using System; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class DataSetFileGeographicLevelGeneratorExtensions +{ + public static Generator DefaultDataSetFileGeographicLevel(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static InstanceSetters SetDefaults( + this InstanceSetters setters) + => setters + .SetDataSetFileVersionId(Guid.NewGuid()) + .SetGeographicLevel(GeographicLevel.Country); + + public static Generator WithDataSetFileVersion( + this Generator generator, + File dataSetFileVersion) + => generator.ForInstance(s => s.SetDataSetFileVersion(dataSetFileVersion)); + + public static Generator WithDataSetFileVersionId( + this Generator generator, + Guid dataSetFileVersionId) + => generator.ForInstance(s => s.SetDataSetFileVersionId(dataSetFileVersionId)); + + public static Generator WithGeographicLevel( + this Generator generator, + GeographicLevel geographicLevel) + => generator.ForInstance(s => s.SetGeographicLevel(geographicLevel)); + + public static InstanceSetters SetDataSetFileVersion( + this InstanceSetters setters, + File dataSetFileVersion) + => setters.Set(f => f.DataSetFileVersion, dataSetFileVersion); + + public static InstanceSetters SetDataSetFileVersionId( + this InstanceSetters setters, + Guid dataSetFileVersionId) + => setters.Set(f => f.DataSetFileVersionId, dataSetFileVersionId); + + public static InstanceSetters SetGeographicLevel( + this InstanceSetters setters, + GeographicLevel geographicLevel) + => setters.Set(f => f.GeographicLevel, geographicLevel); +} 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 f45b0e99adc..b60fb4e6798 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs @@ -1,8 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; 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; @@ -84,6 +85,11 @@ public static Generator WithDataSetFileMeta( DataSetFileMeta? dataSetFileMeta) => generator.ForInstance(s => s.SetDataSetFileMeta(dataSetFileMeta)); + public static Generator WithDataSetFileGeographicLevels( + this Generator generator, + List geographicLevels) + => generator.ForInstance(s => s.SetDataSetFileGeographicLevels(geographicLevels)); + public static InstanceSetters SetDefaults(this InstanceSetters setters, FileType? fileType) => fileType switch { @@ -110,7 +116,8 @@ public static InstanceSetters SetDataFileDefaults(this InstanceSetters f.SubjectId) .SetContentType("text/csv") .SetDefault(f => f.DataSetFileId) - .Set(f => f.DataSetFileMeta, (_, _, context) => context.Fixture.DefaultDataSetFileMeta()); + .Set(f => f.DataSetFileMeta, (_, _, context) => context.Fixture.DefaultDataSetFileMeta()) + .SetDataSetFileGeographicLevels([GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.LocalAuthorityDistrict]); public static InstanceSetters SetMetaFileDefaults(this InstanceSetters setters) => setters @@ -200,4 +207,16 @@ public static InstanceSetters SetDataSetFileMeta( this InstanceSetters setters, DataSetFileMeta? dataSetFileMeta) => setters.Set(f => f.DataSetFileMeta, dataSetFileMeta); + + public static InstanceSetters SetDataSetFileGeographicLevels( + this InstanceSetters setters, + List geographicLevels) + => setters.Set( + file => file.DataSetFileGeographicLevels, + (_, file) => geographicLevels.Select( + gl => new DataSetFileGeographicLevel + { + DataSetFileVersionId = file.Id, + GeographicLevel = gl, + }).ToList()); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs index 8fc1696a46a..42c6f75b9dd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs @@ -23,7 +23,7 @@ public class File : ICreatedTimestamp public int? DataSetFileVersion { get; set; } - public List? DataSetFileGeographicLevels { get; set; } + public List DataSetFileGeographicLevels { get; set; } = []; public DataSetFileMeta? DataSetFileMeta { get; set; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs index 4a0775ccddd..b743fd1198e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs @@ -482,12 +482,12 @@ internal static IQueryable HavingReleaseVersionId( return releaseVersionId.HasValue ? query.Where(rf => rf.ReleaseVersionId == releaseVersionId.Value) : query; } - internal static IQueryable HavingGeographicLevel( // @MarkFix write tests + internal static IQueryable HavingGeographicLevel( this IQueryable query, GeographicLevel? geographicLevel) { return geographicLevel.HasValue - ? query.Where(rf => rf.File.DataSetFileGeographicLevels!.Any( // @MarkFix null allowing + ? query.Where(rf => rf.File.DataSetFileGeographicLevels.Any( gl => gl.GeographicLevel == geographicLevel && rf.FileId == gl.DataSetFileVersionId)) : query; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Functions/ProcessorStage3Tests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Functions/ProcessorStage3Tests.cs index 36f3fdc9e87..965c0c781e1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Functions/ProcessorStage3Tests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Functions/ProcessorStage3Tests.cs @@ -23,6 +23,7 @@ using GovUk.Education.ExploreEducationStatistics.Data.Processor.Services; using GovUk.Education.ExploreEducationStatistics.Data.Processor.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests.Services; +using GovUk.Education.ExploreEducationStatistics.Public.Data.Model.Migrations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -106,7 +107,7 @@ public async Task ProcessStage3() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("small-csv") + .WithDefaultFiles("small-csv", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -325,7 +326,7 @@ public async Task ProcessStage3_FailsImportIfRowCountsDontMatch() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("small-csv") + .WithDefaultFiles("small-csv", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -454,7 +455,7 @@ public async Task ProcessStage3_PartiallyImportedAlready() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("small-csv") + .WithDefaultFiles("small-csv", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -604,7 +605,7 @@ public async Task ProcessStage3_PartiallyImportedAlready_BatchSizeChanged() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("small-csv") + .WithDefaultFiles("small-csv", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -749,7 +750,7 @@ public async Task ProcessStage3_IgnoredRows() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("ignored-school-rows") + .WithDefaultFiles("ignored-school-rows", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(8) @@ -894,7 +895,7 @@ public async Task ProcessStage3_IgnoredRows_PartiallyImported() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("ignored-school-rows") + .WithDefaultFiles("ignored-school-rows", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(8) @@ -1052,7 +1053,7 @@ public async Task ProcessStage3_Cancelling() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("small-csv") + .WithDefaultFiles("small-csv", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -1190,7 +1191,7 @@ public async Task ProcessStage3_CancelledAlready() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("small-csv") + .WithDefaultFiles("small-csv", metaSet: false) .WithStatus(CANCELLED) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -1324,7 +1325,7 @@ public async Task ProcessStage3_AdditionalFiltersAndIndicators() var import = _fixture .DefaultDataImport() .WithSubjectId(_subject.Id) - .WithDefaultFiles("additional-filters-and-indicators") + .WithDefaultFiles("additional-filters-and-indicators", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(16) .WithExpectedImportedRows(16) @@ -1493,7 +1494,7 @@ public async Task ProcessStage3_SpecialFilterItemAndIndicatorValues() var import = _fixture .DefaultDataImport() .WithSubjectId(subject.Id) - .WithDefaultFiles("small-csv-with-special-data") + .WithDefaultFiles("small-csv-with-special-data", metaSet: false) .WithStatus(STAGE_3) .WithTotalRows(5) .WithExpectedImportedRows(5) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs index 5aa9ee86309..8463051e0f5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs @@ -220,6 +220,7 @@ public async Task WriteDataSetMetaFile_Success() var file = _fixture.DefaultFile(FileType.Data) .WithDataSetFileMeta(null) + .WithDataSetFileGeographicLevels([]) .WithSubjectId(subject.Id) .Generate(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs index acb15120605..162c826776d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs @@ -222,6 +222,7 @@ public async Task WriteDataSetFileMeta(Guid fileId, Guid subjectId) file.DataSetFileMeta = dataSetFileMeta; var dataSetFileGeographicLevels = geographicLevels + .Distinct() .Select(gl => new DataSetFileGeographicLevel { DataSetFileVersionId = fileId, 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 c73c983240c..c6a2e4a13ca 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 @@ -77,7 +77,7 @@ export const testDataSetFileSummaries: DataSetFileSummary[] = [ to: '2020', }, filters: ['Filter 1', 'Filter 2'], - geographicLevels: ['National', 'Regional'], + geographicLevels: ['National', 'Regional', 'Local authority'], indicators: ['Indicator 1', 'Indicator 2'], }, latestData: true, diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataCataloguePage.test.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataCataloguePage.test.tsx index 4026e9df8c6..e6b684958f7 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataCataloguePage.test.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/__tests__/DataCataloguePage.test.tsx @@ -1195,6 +1195,32 @@ describe('DataCataloguePage', () => { ).toBeInTheDocument(); }); + test('filters by geographic level', async () => { + mockRouter.setCurrentUrl('/data-catalogue?geographicLevel=LA'); + + dataSetService.listDataSetFiles.mockResolvedValueOnce({ + results: [testDataSetFileSummaries[1], testDataSetFileSummaries[2]], + paging: { ...testPaging, totalPages: 1, totalResults: 1 }, + }); + publicationService.getPublicationTree.mockResolvedValue(testThemes); + publicationService.listReleases.mockResolvedValue(testReleases); + + render(); + + await waitFor(() => { + expect(screen.getByText('1 data set')).toBeInTheDocument(); + }); + + expect(mockRouter).toMatchObject({ + pathname: '/data-catalogue', + query: { geographicLevel: 'LA' }, + }); + + expect(screen.getByLabelText('Filter by Geographic level')).toHaveValue( + 'LA', + ); + }); + test('filters by search term', async () => { mockRouter.setCurrentUrl('/data-catalogue?searchTerm=find+me'); diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetSummary.test.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetSummary.test.tsx index ddf431535b5..fd464e53a87 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetSummary.test.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/__tests__/DataSetSummary.test.tsx @@ -50,7 +50,7 @@ describe('DataSetFileSummary', () => { expect( within(screen.getByTestId('Geographic levels')).getByText( - 'National, Regional', + 'Local authority, National, Regional', ), ).toBeInTheDocument(); expect( @@ -83,7 +83,7 @@ describe('DataSetFileSummary', () => { expect( within(screen.getByTestId('Geographic levels')).getByText( - 'National, Regional', + 'Local authority, National, Regional', ), ).toBeInTheDocument(); expect( From 1d65b4b418c6ff75ea282e1fb3ba8c0799af53d4 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 17 Dec 2024 12:23:51 +0000 Subject: [PATCH 08/40] EES-5738 Changes in response to PR comments --- ...FileGeographicLevelsMigrationController.cs | 14 ++--- ...aSetFileGeographicLevelsTable.Designer.cs} | 15 +++--- ...CreateDataSetFileGeographicLevelsTable.cs} | 22 +++++--- .../ContentDbContextModelSnapshot.cs | 13 ++--- .../DataSetFilesControllerTests.cs | 22 +++----- .../Fixtures/DataImportGeneratorExtensions.cs | 2 +- ...tFileGeographicLevelGeneratorExtensions.cs | 50 ------------------ ...rsionGeographicLevelGeneratorExtensions.cs | 51 +++++++++++++++++++ .../Fixtures/FileGeneratorExtensions.cs | 13 ++--- ...s => DataSetFileVersionGeographicLevel.cs} | 2 +- .../Database/ContentDbContext.cs | 11 ++-- .../File.cs | 2 +- .../DataSetFileService.cs | 5 +- .../Services/DataImportServiceTests.cs | 2 +- .../Services/DataImportService.cs | 8 +-- .../src/utils/locationLevelsMap.ts | 19 +++++++ .../data-catalogue/DataCataloguePage.tsx | 22 +++++++- .../data-catalogue/components/Filters.tsx | 4 +- .../admin_and_public/bau/data_catalogue.robot | 16 ++++++ 19 files changed, 177 insertions(+), 116 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/{20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs => 20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs} (99%) rename src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/{20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs => 20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.cs} (62%) delete mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileVersionGeographicLevelGeneratorExtensions.cs rename src/GovUk.Education.ExploreEducationStatistics.Content.Model/{DataSetFileGeographicLevels.cs => DataSetFileVersionGeographicLevel.cs} (89%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs index f1adf49fd34..b4ceec2eb98 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/DataSetFileGeographicLevelsMigrationController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Models; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.AspNetCore.Authorization; @@ -24,14 +25,14 @@ public class MigrationResult } [HttpPut("bau/migrate-datasetfile-geographiclevels")] - public async Task DataSetFileGeographicLevelsMigration( + public async Task DataSetFileVersionGeographicLevelsMigration( [FromQuery] bool isDryRun = true, [FromQuery] int? num = null, CancellationToken cancellationToken = default) { var queryable = contentDbContext.Files - .Where(f => f.DataSetFileMeta != null - && f.DataSetFileGeographicLevels.Count == 0); + .Where(f => f.Type == FileType.Data + && f.DataSetFileVersionGeographicLevels.Count == 0); if (num != null) { @@ -53,16 +54,17 @@ public async Task DataSetFileGeographicLevelsMigration( continue; } - var dataSetFileGeographicLevels = meta!.GeographicLevels + var dataSetFileVersionGeographicLevels = meta!.GeographicLevels .Distinct() - .Select(gl => new DataSetFileGeographicLevel + .Select(gl => new DataSetFileVersionGeographicLevel { DataSetFileVersionId = file.Id, GeographicLevel = gl, }) .ToList(); - contentDbContext.DataSetFileGeographicLevels.AddRange(dataSetFileGeographicLevels); + contentDbContext.DataSetFileVersionGeographicLevels.AddRange( + dataSetFileVersionGeographicLevels); numProcessed++; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs similarity index 99% rename from src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs rename to src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs index 9c4bfa60215..739bb0e01e7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.Designer.cs @@ -12,7 +12,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations { [DbContext(typeof(ContentDbContext))] - [Migration("20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable")] + [Migration("20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable")] partial class EES5738_CreateDataSetFileGeographicLevelsTable { /// @@ -331,17 +331,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("DataImportErrors"); }); - modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileGeographicLevel", b => + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileVersionGeographicLevel", b => { b.Property("DataSetFileVersionId") .HasColumnType("uniqueidentifier"); b.Property("GeographicLevel") - .HasColumnType("nvarchar(450)"); + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); b.HasKey("DataSetFileVersionId", "GeographicLevel"); - b.ToTable("DataSetFileGeographicLevels"); + b.ToTable("DataSetFileVersionGeographicLevels"); }); modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", b => @@ -1610,10 +1611,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("DataImport"); }); - modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileGeographicLevel", b => + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileVersionGeographicLevel", b => { b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "DataSetFileVersion") - .WithMany("DataSetFileGeographicLevels") + .WithMany("DataSetFileVersionGeographicLevels") .HasForeignKey("DataSetFileVersionId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -2209,7 +2210,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => { - b.Navigation("DataSetFileGeographicLevels"); + b.Navigation("DataSetFileVersionGeographicLevels"); }); modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.cs similarity index 62% rename from src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs rename to src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.cs index 1434036a2a3..7a652fbef83 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241213123442_EES5738_CreateDataSetFileGeographicLevelsTable.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241219093820_EES5738_CreateDataSetFileGeographicLevelsTable.cs @@ -1,6 +1,8 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +#nullable disable + namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations { /// @@ -10,25 +12,27 @@ public partial class EES5738_CreateDataSetFileGeographicLevelsTable : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "DataSetFileGeographicLevels", + name: "DataSetFileVersionGeographicLevels", columns: table => new { DataSetFileVersionId = table.Column(type: "uniqueidentifier", nullable: false), - GeographicLevel = table.Column(type: "nvarchar(450)", nullable: false) + GeographicLevel = table.Column(type: "nvarchar(6)", maxLength: 6, nullable: false) }, constraints: table => { - table.PrimaryKey("PK_DataSetFileGeographicLevels", x => new { x.DataSetFileVersionId, x.GeographicLevel }); + table.PrimaryKey("PK_DataSetFileVersionGeographicLevels", x => new { x.DataSetFileVersionId, x.GeographicLevel }); table.ForeignKey( - name: "FK_DataSetFileGeographicLevels_Files_DataSetFileVersionId", + name: "FK_DataSetFileVersionGeographicLevels_Files_DataSetFileVersionId", column: x => x.DataSetFileVersionId, principalTable: "Files", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); + migrationBuilder.Sql("GRANT SELECT ON dbo.DataSetFileVersionGeographicLevels TO [content];"); + migrationBuilder.Sql("GRANT INSERT ON dbo.DataSetFileVersionGeographicLevels TO [importer];"); - migrationBuilder.Sql("GRANT SELECT ON dbo.DataSetFileGeographicLevels TO [content];"); - migrationBuilder.Sql("GRANT INSERT ON dbo.DataSetFileGeographicLevels TO [importer];"); + migrationBuilder.Sql("GRANT SELECT ON dbo.DataSetFileVersionGeographicLevels TO [data];"); + migrationBuilder.Sql("GRANT SELECT ON dbo.DataSetFileVersionGeographicLevels TO [publisher];"); } /// @@ -36,8 +40,12 @@ protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.Sql("REVOKE SELECT ON dbo.DataSetFileGeographicLevels TO [content];"); migrationBuilder.Sql("REVOKE INSERT ON dbo.DataSetFileGeographicLevels TO [importer];"); + + migrationBuilder.Sql("REVOKE SELECT ON dbo.DataSetFileGeographicLevels TO [data];"); + migrationBuilder.Sql("REVOKE SELECT ON dbo.DataSetFileGeographicLevels TO [publisher];"); + migrationBuilder.DropTable( - name: "DataSetFileGeographicLevels"); + name: "DataSetFileVersionGeographicLevels"); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs index bc677140783..292daec9d7f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs @@ -328,17 +328,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DataImportErrors"); }); - modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileGeographicLevel", b => + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileVersionGeographicLevel", b => { b.Property("DataSetFileVersionId") .HasColumnType("uniqueidentifier"); b.Property("GeographicLevel") - .HasColumnType("nvarchar(450)"); + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); b.HasKey("DataSetFileVersionId", "GeographicLevel"); - b.ToTable("DataSetFileGeographicLevels"); + b.ToTable("DataSetFileVersionGeographicLevels"); }); modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", b => @@ -1607,10 +1608,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("DataImport"); }); - modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileGeographicLevel", b => + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataSetFileVersionGeographicLevel", b => { b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "DataSetFileVersion") - .WithMany("DataSetFileGeographicLevels") + .WithMany("DataSetFileVersionGeographicLevels") .HasForeignKey("DataSetFileVersionId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -2206,7 +2207,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => { - b.Navigation("DataSetFileGeographicLevels"); + b.Navigation("DataSetFileVersionGeographicLevels"); }); modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => 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 b69ddb03ce6..b32dde93656 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api.Tests/Controllers/DataSetFilesControllerTests.cs @@ -101,27 +101,21 @@ public async Task FilterByGeographicLevel_Success() var (publication1, publication2) = _fixture .DefaultPublication() // Publications each have a published release version - .WithReleases(_fixture.DefaultRelease(publishedVersions: 1) - .Generate(1)) + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]) .WithTheme(_fixture.DefaultTheme()) .GenerateTuple2(); - var publication1Release1Version1Files = _fixture.DefaultReleaseFile() + ReleaseFile publication1Release1Version1File = _fixture.DefaultReleaseFile() .WithReleaseVersion(publication1.ReleaseVersions[0]) - .WithFiles(_fixture.DefaultFile(FileType.Data) - .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta() - .WithGeographicLevels( - [GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.Institution])) - .WithDataSetFileGeographicLevels( - [GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.Institution]) - .GenerateList(1)) - .GenerateList(); + .WithFile(_fixture.DefaultFile(FileType.Data) + .WithDataSetFileVersionGeographicLevels( + [GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.Institution])); var publication2Release1Version1Files = GenerateDataSetFilesForReleaseVersion(publication2.ReleaseVersions[0]); await TestApp.AddTestData(context => { - context.ReleaseFiles.AddRange(publication1Release1Version1Files); + context.ReleaseFiles.Add(publication1Release1Version1File); context.ReleaseFiles.AddRange(publication2Release1Version1Files); }); @@ -137,7 +131,7 @@ await TestApp.AddTestData(context => pagedResult.AssertHasExpectedPagingAndResultCount( expectedTotalResults: 1); - AssertResultsForExpectedReleaseFiles(publication1Release1Version1Files, pagedResult.Results); + AssertResultsForExpectedReleaseFiles([publication1Release1Version1File], pagedResult.Results); } [Fact] @@ -1783,7 +1777,7 @@ private List GenerateDataSetFilesForReleaseVersion( .WithReleaseVersion(releaseVersion) .WithFiles(_fixture.DefaultFile(FileType.Data) .WithDataSetFileMeta(_fixture.DefaultDataSetFileMeta()) - .WithDataSetFileGeographicLevels([GeographicLevel.Country]) + .WithDataSetFileVersionGeographicLevels([GeographicLevel.Country]) .GenerateList(numberOfDataSets)) .GenerateList(); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs index 0eab870e422..5d13f4f4164 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataImportGeneratorExtensions.cs @@ -114,7 +114,7 @@ public static InstanceSetters SetDefaultFilesWithoutMeta( (_, d, context) => context.Fixture .DefaultFile(FileType.Data) .WithDataSetFileMeta(null) - .WithDataSetFileGeographicLevels([]) + .WithDataSetFileVersionGeographicLevels([]) .WithFilename($"{dataFileName}.csv") .WithSubjectId(d.SubjectId) ) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs deleted file mode 100644 index b9c7d83a357..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileGeographicLevelGeneratorExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; -using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; - -namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; - -public static class DataSetFileGeographicLevelGeneratorExtensions -{ - public static Generator DefaultDataSetFileGeographicLevel(this DataFixture fixture) - => fixture.Generator().WithDefaults(); - - public static Generator WithDefaults(this Generator generator) - => generator.ForInstance(d => d.SetDefaults()); - - public static InstanceSetters SetDefaults( - this InstanceSetters setters) - => setters - .SetDataSetFileVersionId(Guid.NewGuid()) - .SetGeographicLevel(GeographicLevel.Country); - - public static Generator WithDataSetFileVersion( - this Generator generator, - File dataSetFileVersion) - => generator.ForInstance(s => s.SetDataSetFileVersion(dataSetFileVersion)); - - public static Generator WithDataSetFileVersionId( - this Generator generator, - Guid dataSetFileVersionId) - => generator.ForInstance(s => s.SetDataSetFileVersionId(dataSetFileVersionId)); - - public static Generator WithGeographicLevel( - this Generator generator, - GeographicLevel geographicLevel) - => generator.ForInstance(s => s.SetGeographicLevel(geographicLevel)); - - public static InstanceSetters SetDataSetFileVersion( - this InstanceSetters setters, - File dataSetFileVersion) - => setters.Set(f => f.DataSetFileVersion, dataSetFileVersion); - - public static InstanceSetters SetDataSetFileVersionId( - this InstanceSetters setters, - Guid dataSetFileVersionId) - => setters.Set(f => f.DataSetFileVersionId, dataSetFileVersionId); - - public static InstanceSetters SetGeographicLevel( - this InstanceSetters setters, - GeographicLevel geographicLevel) - => setters.Set(f => f.GeographicLevel, geographicLevel); -} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileVersionGeographicLevelGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileVersionGeographicLevelGeneratorExtensions.cs new file mode 100644 index 00000000000..eb49d2897ec --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/DataSetFileVersionGeographicLevelGeneratorExtensions.cs @@ -0,0 +1,51 @@ +using System; +using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class DataSetFileVersionGeographicLevelGeneratorExtensions +{ + public static Generator DefaultDataSetFileVersionGeographicLevel(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static InstanceSetters SetDefaults( + this InstanceSetters setters) + => setters + .SetDefault(p => p.DataSetFileVersionId); + + public static Generator WithDataSetFileVersion( + this Generator generator, + File dataSetFileVersion) + => generator.ForInstance(s => s.SetDataSetFileVersion(dataSetFileVersion)); + + public static Generator WithDataSetFileVersionId( + this Generator generator, + Guid dataSetFileVersionId) + => generator.ForInstance(s => s.SetDataSetFileVersionId(dataSetFileVersionId)); + + public static Generator WithGeographicLevel( + this Generator generator, + GeographicLevel geographicLevel) + => generator.ForInstance(s => s.SetGeographicLevel(geographicLevel)); + + public static InstanceSetters SetDataSetFileVersion( + this InstanceSetters setters, + File dataSetFileVersion) + => setters + .Set(f => f.DataSetFileVersion, dataSetFileVersion) + .Set(f => f.DataSetFileVersionId, dataSetFileVersion.Id); + + public static InstanceSetters SetDataSetFileVersionId( + this InstanceSetters setters, + Guid dataSetFileVersionId) + => setters.Set(f => f.DataSetFileVersionId, dataSetFileVersionId); + + public static InstanceSetters SetGeographicLevel( + this InstanceSetters setters, + GeographicLevel geographicLevel) + => setters.Set(f => f.GeographicLevel, geographicLevel); +} 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 b60fb4e6798..320c2950442 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/FileGeneratorExtensions.cs @@ -85,10 +85,10 @@ public static Generator WithDataSetFileMeta( DataSetFileMeta? dataSetFileMeta) => generator.ForInstance(s => s.SetDataSetFileMeta(dataSetFileMeta)); - public static Generator WithDataSetFileGeographicLevels( + public static Generator WithDataSetFileVersionGeographicLevels( this Generator generator, List geographicLevels) - => generator.ForInstance(s => s.SetDataSetFileGeographicLevels(geographicLevels)); + => generator.ForInstance(s => s.SetDataSetFileVersionGeographicLevels(geographicLevels)); public static InstanceSetters SetDefaults(this InstanceSetters setters, FileType? fileType) => fileType switch @@ -117,7 +117,7 @@ public static InstanceSetters SetDataFileDefaults(this InstanceSetters f.DataSetFileId) .Set(f => f.DataSetFileMeta, (_, _, context) => context.Fixture.DefaultDataSetFileMeta()) - .SetDataSetFileGeographicLevels([GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.LocalAuthorityDistrict]); + .SetDataSetFileVersionGeographicLevels([GeographicLevel.Country, GeographicLevel.LocalAuthority, GeographicLevel.LocalAuthorityDistrict]); public static InstanceSetters SetMetaFileDefaults(this InstanceSetters setters) => setters @@ -208,15 +208,16 @@ public static InstanceSetters SetDataSetFileMeta( DataSetFileMeta? dataSetFileMeta) => setters.Set(f => f.DataSetFileMeta, dataSetFileMeta); - public static InstanceSetters SetDataSetFileGeographicLevels( + public static InstanceSetters SetDataSetFileVersionGeographicLevels( this InstanceSetters setters, List geographicLevels) => setters.Set( - file => file.DataSetFileGeographicLevels, + file => file.DataSetFileVersionGeographicLevels, (_, file) => geographicLevels.Select( - gl => new DataSetFileGeographicLevel + gl => new DataSetFileVersionGeographicLevel { DataSetFileVersionId = file.Id, + DataSetFileVersion = file, GeographicLevel = gl, }).ToList()); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileVersionGeographicLevel.cs similarity index 89% rename from src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs rename to src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileVersionGeographicLevel.cs index 2b23db07a4a..d677283dbe6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileGeographicLevels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileVersionGeographicLevel.cs @@ -4,7 +4,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.Model; -public class DataSetFileGeographicLevel +public class DataSetFileVersionGeographicLevel { public Guid DataSetFileVersionId { get; set; } // Currently Files.Id, but will become DataSetFileVersion.Id in EES-5105 diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs index 2cec48a76d7..8521a3ede07 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs @@ -55,7 +55,7 @@ private void Configure(bool updateTimestamps = true) public virtual DbSet ReleaseStatus { get; set; } public virtual DbSet ReleaseFiles { get; set; } public virtual DbSet Files { get; set; } - public virtual DbSet DataSetFileGeographicLevels { get; set; } + public virtual DbSet DataSetFileVersionGeographicLevels { get; set; } public virtual DbSet ContentSections { get; set; } public virtual DbSet ContentBlocks { get; set; } public virtual DbSet KeyStatistics { get; set; } @@ -111,7 +111,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ConfigureReleaseStatus(modelBuilder); ConfigureReleaseFile(modelBuilder); ConfigureFile(modelBuilder); - ConfigureDataSetFileGeographicLevel(modelBuilder); + ConfigureDataSetFileVersionGeographicLevel(modelBuilder); ConfigureContentBlock(modelBuilder); ConfigureContentSection(modelBuilder); ConfigureReleaseVersion(modelBuilder); @@ -454,16 +454,16 @@ private static void ConfigureFile(ModelBuilder modelBuilder) v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : null); - entity.Property(p => p.DataSetFileMeta) // EES-5666 + entity.Property(p => p.DataSetFileMeta) .HasConversion( v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject(v)); }); } - private static void ConfigureDataSetFileGeographicLevel(ModelBuilder modelBuilder) + private static void ConfigureDataSetFileVersionGeographicLevel(ModelBuilder modelBuilder) { - modelBuilder.Entity(entity => + modelBuilder.Entity(entity => { entity.HasKey(gl => new { @@ -472,6 +472,7 @@ private static void ConfigureDataSetFileGeographicLevel(ModelBuilder modelBuilde }); entity.Property(gl => gl.GeographicLevel) + .HasMaxLength(6) .HasConversion(new EnumToEnumValueConverter()); }); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs index 42c6f75b9dd..7ada31a8934 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs @@ -23,7 +23,7 @@ public class File : ICreatedTimestamp public int? DataSetFileVersion { get; set; } - public List DataSetFileGeographicLevels { get; set; } = []; + public List DataSetFileVersionGeographicLevels { get; set; } = []; public DataSetFileMeta? DataSetFileMeta { get; set; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs index b743fd1198e..e6d0e9d85d9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/DataSetFileService.cs @@ -487,9 +487,8 @@ internal static IQueryable HavingGeographicLevel( GeographicLevel? geographicLevel) { return geographicLevel.HasValue - ? query.Where(rf => rf.File.DataSetFileGeographicLevels.Any( - gl => gl.GeographicLevel == geographicLevel - && rf.FileId == gl.DataSetFileVersionId)) + ? query.Where(rf => rf.File.DataSetFileVersionGeographicLevels.Any( + gl => gl.GeographicLevel == geographicLevel)) : query; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs index 8463051e0f5..de19c60ad62 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs @@ -220,7 +220,7 @@ public async Task WriteDataSetMetaFile_Success() var file = _fixture.DefaultFile(FileType.Data) .WithDataSetFileMeta(null) - .WithDataSetFileGeographicLevels([]) + .WithDataSetFileVersionGeographicLevels([]) .WithSubjectId(subject.Id) .Generate(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs index 162c826776d..5b29fd83184 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs @@ -221,14 +221,14 @@ public async Task WriteDataSetFileMeta(Guid fileId, Guid subjectId) && f.SubjectId == subjectId); file.DataSetFileMeta = dataSetFileMeta; - var dataSetFileGeographicLevels = geographicLevels - .Distinct() - .Select(gl => new DataSetFileGeographicLevel + var dataSetFileVersionGeographicLevels = geographicLevels + .Select(gl => new DataSetFileVersionGeographicLevel { DataSetFileVersionId = fileId, GeographicLevel = gl, }).ToList(); - await contentDbContext.DataSetFileGeographicLevels.AddRangeAsync(dataSetFileGeographicLevels); + contentDbContext.DataSetFileVersionGeographicLevels.AddRange( + dataSetFileVersionGeographicLevels); await contentDbContext.SaveChangesAsync(); } diff --git a/src/explore-education-statistics-common/src/utils/locationLevelsMap.ts b/src/explore-education-statistics-common/src/utils/locationLevelsMap.ts index 6f54b04f835..947dd7b3049 100644 --- a/src/explore-education-statistics-common/src/utils/locationLevelsMap.ts +++ b/src/explore-education-statistics-common/src/utils/locationLevelsMap.ts @@ -22,6 +22,7 @@ export type GeographicLevelCode = | 'WARD'; export interface LocationLevelDetails { + filterLabel: string; // used in the dropdown that filters data sets by geographic level label: string; plural: string; prefix: string; @@ -30,108 +31,126 @@ export interface LocationLevelDetails { const locationLevelsMap = { country: { + filterLabel: 'National', label: 'Country', plural: 'Countries', prefix: 'a', code: 'NAT', }, englishDevolvedArea: { + filterLabel: 'English Devolved Area', label: 'English Devolved Area', plural: 'English Devolved Areas', prefix: 'an', code: 'EDA', }, institution: { + filterLabel: 'Institution', label: 'Institution', plural: 'Institutions', prefix: 'an', code: 'INST', }, localAuthority: { + filterLabel: 'Local Authority', label: 'Local Authority', plural: 'Local Authorities', prefix: 'a', code: 'LA', }, localAuthorityDistrict: { + filterLabel: 'Local Authority District', label: 'Local Authority District', plural: 'Local Authority Districts', prefix: 'a', code: 'LAD', }, localEnterprisePartnership: { + filterLabel: 'Local Enterprise Partnership', label: 'Local Enterprise Partnership', plural: 'Local Enterprise Partnerships', prefix: 'a', code: 'LEP', }, localSkillsImprovementPlanArea: { + filterLabel: 'Local Skills Improvement Plan Area', label: 'Local Skills Improvement Plan Area', plural: 'Local Skills Improvement Plan Areas', prefix: 'a', code: 'LSIP', }, mayoralCombinedAuthority: { + filterLabel: 'Mayoral Combined Authority', label: 'Mayoral Combined Authority', plural: 'Mayoral Combined Authorities', prefix: 'a', code: 'MCA', }, multiAcademyTrust: { + filterLabel: 'Multi Academy Trust', label: 'Multi Academy Trust', plural: 'Multi Academy Trusts', prefix: 'a', code: 'MAT', }, opportunityArea: { + filterLabel: 'Opportunity Area', label: 'Opportunity Area', plural: 'Opportunity Areas', prefix: 'an', code: 'OA', }, parliamentaryConstituency: { + filterLabel: 'Parliamentary Constituency', label: 'Parliamentary Constituency', plural: 'Parliamentary Constituencies', prefix: 'a', code: 'PCON', }, planningArea: { + filterLabel: 'Planning Area', label: 'Planning Area', plural: 'Planning Areas', prefix: 'a', code: 'PA', }, provider: { + filterLabel: 'Provider', label: 'Provider', plural: 'Providers', prefix: 'a', code: 'PROV', }, region: { + filterLabel: 'Region', label: 'Region', plural: 'Regions', prefix: 'a', code: 'REG', }, rscRegion: { + filterLabel: 'RSC Region', label: 'RSC Region', plural: 'RSC Regions', prefix: 'an', code: 'RSC', }, school: { + filterLabel: 'School', label: 'School', plural: 'Schools', prefix: 'a', code: 'SCH', }, sponsor: { + filterLabel: 'Sponsor', label: 'Sponsor', plural: 'Sponsors', prefix: 'a', code: 'SPON', }, ward: { + filterLabel: 'Ward', label: 'Ward', plural: 'Wards', prefix: 'a', diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx index c14872aa2e0..039b1dd1c12 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx @@ -52,7 +52,11 @@ import omit from 'lodash/omit'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { ParsedUrlQuery } from 'querystring'; -import { GeographicLevelCode } from '@common/utils/locationLevelsMap'; +import locationLevelsMap, { + GeographicLevelCode, + geographicLevelCodesMap, + LocationLevelKey, +} from '@common/utils/locationLevelsMap'; const defaultPageTitle = 'Data catalogue'; @@ -124,11 +128,16 @@ const DataCataloguePage: NextPage = ({ showTypeFilter }) => { const selectedRelease = releases.find(release => release.id === releaseId); + const selectedGeographicLevel = geographicLevel + ? geographicLevelCodesMap[geographicLevel] + : undefined; + const { paging, results: dataSets = [] } = dataSetsData ?? {}; const { page, totalPages, totalResults = 0 } = paging ?? {}; const [showAllDetails, toggleAllDetails] = useToggle(false); - const isFiltered = !!publicationId || !!searchTerm || !!themeId; + const isFiltered = + !!publicationId || !!searchTerm || !!themeId || !!geographicLevel; const filteredByString = compact([ searchTerm, @@ -420,6 +429,15 @@ const DataCataloguePage: NextPage = ({ showTypeFilter }) => { } /> )} + {selectedGeographicLevel && ( + + handleResetFilter({ filterType: 'geographicLevel' }) + } + /> + )} )} diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx index c564cbc8fd5..a5e34d16eb0 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/components/Filters.tsx @@ -160,12 +160,12 @@ export default function Filters({ { label: 'All', value: 'all' }, ...typedKeys(locationLevelsMap).map(key => { return { - label: locationLevelsMap[key].label, + label: locationLevelsMap[key].filterLabel, value: locationLevelsMap[key].code, }; }), ]} - value={geographicLevel} + value={geographicLevel ?? 'all'} order={[]} onChange={e => { onChange({ diff --git a/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot b/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot index d1781d21f42..ceff983c154 100644 --- a/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot +++ b/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot @@ -212,6 +212,22 @@ Remove release filter user checks page does not contain button ${PUPIL_ABSENCE_RELEASE_NAME} user checks selected option label id:filters-form-release All releases +Filter by geographic level + user wait for option to be available and select it css:select[id="filters-form-geographic-level"] + ... Local Authority District + + user checks page contains button Local Authority District + user checks testid element contains total-results 1 data set + +Remove geographic level filter + user clicks button Local Authority District + + user checks page does not contain button Local Authority District + user checks selected option label id:filters-form-geographic-level All + + user checks page contains button ${PUPILS_AND_SCHOOLS_THEME_TITLE} + user checks page contains button ${PUPIL_ABSENCE_PUBLICATION_TITLE} + Reset all filters user clicks element id:searchForm-search user presses keys pupil From ddf75967cd613c7190d005afc97bcb3ea8048bfe Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Thu, 19 Dec 2024 14:18:18 +0000 Subject: [PATCH 09/40] EES-XXXX - added ability to specific workload profiles and container resource limits for Container Apps --- .../application/public-api/publicApiApp.bicep | 9 +++- .../shared/containerAppEnvironment.bicep | 8 +++- .../public-api/components/containerApp.bicep | 47 ++++++++++--------- .../components/containerAppEnvironment.bicep | 19 ++++---- .../templates/public-api/main.bicep | 16 ++++++- .../public-api/parameters/main-dev.bicepparam | 8 ++++ .../templates/public-api/types.bicep | 17 +++++++ 7 files changed, 89 insertions(+), 35 deletions(-) diff --git a/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep b/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep index 414920edf6e..04edeef331b 100644 --- a/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep +++ b/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep @@ -1,4 +1,4 @@ -import { ResourceNames } from '../../types.bicep' +import { ResourceNames, ContainerAppResourceConfig } from '../../types.bicep' @description('Specifies common resource naming variables.') param resourceNames ResourceNames @@ -30,6 +30,9 @@ param apiAppRegistrationClientId string @description('Specifies the Application Insights connection string for this Container App to use for its monitoring.') param appInsightsConnectionString string +@description('Resource limits and scaling configuration.') +param resourceAndScalingConfig ContainerAppResourceConfig + @description('Whether to create or update Azure Monitor alerts during this deploy') param deployAlerts bool @@ -145,6 +148,10 @@ module apiContainerAppModule '../../components/containerApp.bicep' = { ] requireAuthentication: false } + cpuCores: resourceAndScalingConfig.cpuCores + memorySizeGis: resourceAndScalingConfig.memoryGis + minReplicas: resourceAndScalingConfig.minReplicas + maxReplicas: resourceAndScalingConfig.maxReplicas tagValues: tagValues } } diff --git a/infrastructure/templates/public-api/application/shared/containerAppEnvironment.bicep b/infrastructure/templates/public-api/application/shared/containerAppEnvironment.bicep index 6222b2ea4cf..4be0593da07 100644 --- a/infrastructure/templates/public-api/application/shared/containerAppEnvironment.bicep +++ b/infrastructure/templates/public-api/application/shared/containerAppEnvironment.bicep @@ -1,4 +1,4 @@ -import { ResourceNames } from '../../types.bicep' +import { ResourceNames, ContainerAppWorkloadProfile } from '../../types.bicep' @description('Specifies common resource naming variables.') param resourceNames ResourceNames @@ -9,6 +9,9 @@ param location string @description('Specifies the Application Insights key that is associated with this resource.') param applicationInsightsKey string +@description('Specifies the workload profiles for this Container App Environment - the default Consumption plan is always included') +param workloadProfiles ContainerAppWorkloadProfile[] = [] + @description('Specifies a set of tags with which to tag the resource in Azure.') param tagValues object @@ -33,7 +36,6 @@ module containerAppEnvironmentModule '../../components/containerAppEnvironment.b subnetId: subnet.id logAnalyticsWorkspaceName: resourceNames.sharedResources.logAnalyticsWorkspace applicationInsightsKey: applicationInsightsKey - tagValues: tagValues azureFileStorages: [ { storageName: resourceNames.publicApi.publicApiFileShare @@ -43,6 +45,8 @@ module containerAppEnvironmentModule '../../components/containerAppEnvironment.b accessMode: 'ReadWrite' } ] + workloadProfiles: workloadProfiles + tagValues: tagValues } } diff --git a/infrastructure/templates/public-api/components/containerApp.bicep b/infrastructure/templates/public-api/components/containerApp.bicep index cdb8f6804bf..24ad3fe1562 100644 --- a/infrastructure/templates/public-api/components/containerApp.bicep +++ b/infrastructure/templates/public-api/components/containerApp.bicep @@ -25,37 +25,40 @@ param corsPolicy { allowedOrigins: string[]? } +@description('Name of the workload profile under which this Container App will be deployed. Defaults to Consumption.') +param workloadProfileName string = 'Consumption' + @description('Number of CPU cores the container can use. Can be with a maximum of two decimals.') @allowed([ - '1' - '2' - '3' - '4' + 1 + 2 + 3 + 4 ]) -param cpuCore string = '4' +param cpuCores int = 4 @description('Amount of memory (in gibibytes, GiB) allocated to the container up to 4GiB. Can be with a maximum of two decimals. Ratio with CPU cores must be equal to 2.') @allowed([ - '1' - '2' - '3' - '4' - '5' - '6' - '7' - '8' + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 ]) -param memorySize string = '8' +param memorySizeGis int = 8 @description('Minimum number of replicas that will be deployed') @minValue(0) @maxValue(25) -param minReplica int = 1 +param minReplicas int = 1 @description('Maximum number of replicas that will be deployed') @minValue(0) @maxValue(25) -param maxReplica int = 3 +param maxReplicas int = 3 @description('Specifies the database connection string') param appSettings { @@ -113,7 +116,7 @@ param tagValues object var containerImageName = '${acrLoginServer}/${containerAppImageName}' -resource containerApp 'Microsoft.App/containerApps@2023-11-02-preview' = { +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { name: containerAppName location: location identity: { @@ -160,15 +163,15 @@ resource containerApp 'Microsoft.App/containerApps@2023-11-02-preview' = { image: containerImageName env: appSettings resources: { - cpu: json(cpuCore) - memory: '${memorySize}Gi' + cpu: json(string(cpuCores)) + memory: '${memorySizeGis}Gi' } volumeMounts: volumeMounts } ] scale: { - minReplicas: minReplica - maxReplicas: maxReplica + minReplicas: minReplicas + maxReplicas: maxReplicas rules: [ { name: 'http-requests' @@ -182,7 +185,7 @@ resource containerApp 'Microsoft.App/containerApps@2023-11-02-preview' = { } volumes: volumes } - workloadProfileName: 'Consumption' + workloadProfileName: workloadProfileName } tags: tagValues } diff --git a/infrastructure/templates/public-api/components/containerAppEnvironment.bicep b/infrastructure/templates/public-api/components/containerAppEnvironment.bicep index 93442b6b20a..39436e24765 100644 --- a/infrastructure/templates/public-api/components/containerAppEnvironment.bicep +++ b/infrastructure/templates/public-api/components/containerAppEnvironment.bicep @@ -1,3 +1,5 @@ +import { ContainerAppWorkloadProfile } from '../types.bicep' + @description('Specifies the location of the Container App Environment - defaults to that of the Resource Group') param location string @@ -13,14 +15,8 @@ param logAnalyticsWorkspaceName string @description('Specifies the Application Insights key that is associated with this resource') param applicationInsightsKey string -@description('Specifies the workload profiles for this Container App Environment - defaults to Consumption') -param workloadProfiles { - name: string - workloadProfileType: string -}[] = [{ - name: 'Consumption' - workloadProfileType: 'Consumption' -}] +@description('Specifies the workload profiles for this Container App Environment - the default Consumption plan is always included') +param workloadProfiles ContainerAppWorkloadProfile[] = [] @description('Specifies a set of tags with which to tag the resource in Azure') param tagValues object @@ -57,7 +53,12 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey } } - workloadProfiles: workloadProfiles + workloadProfiles: union([{ + name: 'Consumption' + workloadProfileType: 'Consumption' + }], + workloadProfiles + ) } tags: tagValues diff --git a/infrastructure/templates/public-api/main.bicep b/infrastructure/templates/public-api/main.bicep index 4426a92c6ac..188456f9e8a 100644 --- a/infrastructure/templates/public-api/main.bicep +++ b/infrastructure/templates/public-api/main.bicep @@ -1,5 +1,5 @@ import { abbreviations } from 'abbreviations.bicep' -import { IpRange, PrincipalNameAndId, StaticWebAppSku } from 'types.bicep' +import { ContainerAppResourceConfig, IpRange, PrincipalNameAndId, StaticWebAppSku, ContainerAppWorkloadProfile } from 'types.bicep' @description('Environment : Subscription name e.g. s101d01. Used as a prefix for created resources.') param subscription string = '' @@ -107,6 +107,18 @@ param devopsServicePrincipalId string = '' @description('Specifies whether or not test Themes can be deleted in the environment.') param enableThemeDeletion bool = false +@description('Specifies the workload profiles for this Container App Environment - the default Consumption plan is always included') +param publicApiContainerAppWorkloadProfiles ContainerAppWorkloadProfile[] = [] + +@description('Resource configuration for the Public API Container App.') +param publicApiContainerAppConfig ContainerAppResourceConfig = { + cpuCores: 4 + memoryGis: 8 + minReplicas: 0 + maxReplicas: 3 + workloadProfileName: 'Consumption' +} + var tagValues = union(resourceTags ?? {}, { Environment: environmentName DateProvisioned: dateProvisioned @@ -254,6 +266,7 @@ module containerAppEnvironmentModule 'application/shared/containerAppEnvironment location: location resourceNames: resourceNames applicationInsightsKey: appInsightsModule.outputs.appInsightsKey + workloadProfiles: publicApiContainerAppWorkloadProfiles tagValues: tagValues } dependsOn: [ @@ -286,6 +299,7 @@ module apiAppModule 'application/public-api/publicApiApp.bicep' = if (deployCont dockerImagesTag: dockerImagesTag appInsightsConnectionString: appInsightsModule.outputs.appInsightsConnectionString deployAlerts: deployAlerts + resourceAndScalingConfig: publicApiContainerAppConfig tagValues: tagValues } dependsOn: [ diff --git a/infrastructure/templates/public-api/parameters/main-dev.bicepparam b/infrastructure/templates/public-api/parameters/main-dev.bicepparam index 2b7fd120b21..d9b2425967e 100644 --- a/infrastructure/templates/public-api/parameters/main-dev.bicepparam +++ b/infrastructure/templates/public-api/parameters/main-dev.bicepparam @@ -14,4 +14,12 @@ param postgreSqlSkuName = 'Standard_B1ms' param postgreSqlStorageSizeGB = 32 param postgreSqlAutoGrowStatus = 'Disabled' +param publicApiContainerAppConfig = { + cpuCores: 4 + memoryGis: 8 + minReplicas: 1 + maxReplicas: 16 + workloadProfileName: 'Consumption' +} + param enableThemeDeletion = true diff --git a/infrastructure/templates/public-api/types.bicep b/infrastructure/templates/public-api/types.bicep index d21d724fad2..44ea8544cfa 100644 --- a/infrastructure/templates/public-api/types.bicep +++ b/infrastructure/templates/public-api/types.bicep @@ -222,3 +222,20 @@ type KeyVaultRole = 'Secrets User' | 'Certificate User' @export() type StaticWebAppSku = 'Free' | 'Standard' + +@export() +type ContainerAppResourceConfig = { + workloadProfileName: string + minReplicas: int + maxReplicas: int + cpuCores: int + memoryGis: int +} + +@export() +type ContainerAppWorkloadProfile = { + name: string + workloadProfileType: 'D4' | 'D8' | 'D16' | 'D32' | 'E4' | 'E8' | 'E16' | 'E32' + minimumCount: int + maximumCount: int +} From 1f7d7164e26fe710e80ed7f3205c4b00b6d0eb41 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Tue, 10 Dec 2024 10:30:50 +0000 Subject: [PATCH 10/40] EES-5656 Change PublicationService to use primary constructor --- .../Services/PublicationService.cs | 214 ++++++++---------- 1 file changed, 95 insertions(+), 119 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs index 1341e018e56..f3cb0a8689d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs @@ -31,59 +31,35 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services { - public class PublicationService : IPublicationService + public class PublicationService( + ContentDbContext context, + IMapper mapper, + IPersistenceHelper persistenceHelper, + IUserService userService, + IPublicationRepository publicationRepository, + IReleaseVersionRepository releaseVersionRepository, + IMethodologyService methodologyService, + IPublicationCacheService publicationCacheService, + IMethodologyCacheService methodologyCacheService, + IRedirectsCacheService redirectsCacheService) + : IPublicationService { - private readonly ContentDbContext _context; - private readonly IMapper _mapper; - private readonly IPersistenceHelper _persistenceHelper; - private readonly IUserService _userService; - private readonly IPublicationRepository _publicationRepository; - private readonly IReleaseVersionRepository _releaseVersionRepository; - private readonly IMethodologyService _methodologyService; - private readonly IPublicationCacheService _publicationCacheService; - private readonly IMethodologyCacheService _methodologyCacheService; - private readonly IRedirectsCacheService _redirectsCacheService; - - public PublicationService( - ContentDbContext context, - IMapper mapper, - IPersistenceHelper persistenceHelper, - IUserService userService, - IPublicationRepository publicationRepository, - IReleaseVersionRepository releaseVersionRepository, - IMethodologyService methodologyService, - IPublicationCacheService publicationCacheService, - IMethodologyCacheService methodologyCacheService, - IRedirectsCacheService redirectsCacheService) - { - _context = context; - _mapper = mapper; - _persistenceHelper = persistenceHelper; - _userService = userService; - _publicationRepository = publicationRepository; - _releaseVersionRepository = releaseVersionRepository; - _methodologyService = methodologyService; - _publicationCacheService = publicationCacheService; - _methodologyCacheService = methodologyCacheService; - _redirectsCacheService = redirectsCacheService; - } - public async Task>> ListPublications( Guid? themeId = null) { - return await _userService + return await userService .CheckCanAccessSystem() - .OnSuccess(_ => _userService.CheckCanViewAllPublications() + .OnSuccess(_ => userService.CheckCanViewAllPublications() .OnSuccess(async () => { var hydratedPublication = HydratePublication( - _publicationRepository.QueryPublicationsForTheme(themeId)); + publicationRepository.QueryPublicationsForTheme(themeId)); return await hydratedPublication.ToListAsync(); }) .OrElse(() => { - var userId = _userService.GetUserId(); - return _publicationRepository.ListPublicationsForUser(userId, themeId); + var userId = userService.GetUserId(); + return publicationRepository.ListPublicationsForUser(userId, themeId); }) ) .OnSuccess(async publications => @@ -98,11 +74,11 @@ public async Task>> ListPublicat public async Task>> ListPublicationSummaries() { - return await _userService + return await userService .CheckCanViewAllPublications() .OnSuccess(_ => { - return _context.Publications + return context.Publications .Select(publication => new PublicationSummaryViewModel(publication)) .ToList(); }); @@ -115,7 +91,7 @@ public async Task> CreatePublic .OnSuccess(_ => ValidatePublicationSlug(publication.Slug)) .OnSuccess(async _ => { - var contact = await _context.Contacts.AddAsync(new Contact + var contact = await context.Contacts.AddAsync(new Contact { ContactName = publication.Contact.ContactName, ContactTelNo = string.IsNullOrWhiteSpace(publication.Contact.ContactTelNo) @@ -125,7 +101,7 @@ public async Task> CreatePublic TeamEmail = publication.Contact.TeamEmail }); - var saved = await _context.Publications.AddAsync(new Publication + var saved = await context.Publications.AddAsync(new Publication { Contact = contact.Entity, Title = publication.Title, @@ -134,9 +110,9 @@ public async Task> CreatePublic Slug = publication.Slug, }); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(saved.Entity.Id, HydratePublication) .OnSuccess(GeneratePublicationCreateViewModel); }); @@ -146,14 +122,14 @@ public async Task> UpdatePublication( Guid publicationId, PublicationSaveRequest updatedPublication) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId) - .OnSuccess(_userService.CheckCanUpdatePublicationSummary) + .OnSuccess(userService.CheckCanUpdatePublicationSummary) .OnSuccessDo(async publication => { if (publication.Title != updatedPublication.Title) { - return await _userService.CheckCanUpdatePublication(); + return await userService.CheckCanUpdatePublication(); } return Unit.Instance; @@ -162,7 +138,7 @@ public async Task> UpdatePublication( { if (publication.SupersededById != updatedPublication.SupersededById) { - return await _userService.CheckCanUpdatePublication(); + return await userService.CheckCanUpdatePublication(); } return Unit.Instance; @@ -180,7 +156,7 @@ public async Task> UpdatePublication( { if (publication.ThemeId != updatedPublication.ThemeId) { - return await _userService.CheckCanUpdatePublication(); + return await userService.CheckCanUpdatePublication(); } return Unit.Instance; @@ -206,7 +182,7 @@ public async Task> UpdatePublication( publication.Slug = updatedPublication.Slug; if (publication.Live - && _context.PublicationRedirects.All(pr => + && context.PublicationRedirects.All(pr => !(pr.PublicationId == publicationId && pr.Slug == originalSlug))) // don't create duplicate redirect { var publicationRedirect = new PublicationRedirect @@ -215,16 +191,16 @@ public async Task> UpdatePublication( Publication = publication, PublicationId = publication.Id, }; - _context.PublicationRedirects.Add(publicationRedirect); + context.PublicationRedirects.Add(publicationRedirect); } // If there is an existing redirects for the new slug, they're redundant. Remove them - var redundantRedirects = await _context.PublicationRedirects + var redundantRedirects = await context.PublicationRedirects .Where(pr => pr.Slug == updatedPublication.Slug) .ToListAsync(); if (redundantRedirects.Count > 0) { - _context.PublicationRedirects.RemoveRange(redundantRedirects); + context.PublicationRedirects.RemoveRange(redundantRedirects); } } @@ -234,13 +210,13 @@ public async Task> UpdatePublication( publication.Updated = DateTime.UtcNow; publication.SupersededById = updatedPublication.SupersededById; - _context.Publications.Update(publication); + context.Publications.Update(publication); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); if (titleChanged || slugChanged) { - await _methodologyService.PublicationTitleOrSlugChanged(publicationId, + await methodologyService.PublicationTitleOrSlugChanged(publicationId, originalSlug, publication.Title, publication.Slug); @@ -248,14 +224,14 @@ await _methodologyService.PublicationTitleOrSlugChanged(publicationId, if (publication.Live) { - await _methodologyCacheService.UpdateSummariesTree(); - await _publicationCacheService.UpdatePublicationTree(); - await _publicationCacheService.UpdatePublication(publication.Slug); + await methodologyCacheService.UpdateSummariesTree(); + await publicationCacheService.UpdatePublicationTree(); + await publicationCacheService.UpdatePublication(publication.Slug); if (slugChanged) { - await _publicationCacheService.RemovePublication(originalSlug); - await _redirectsCacheService.UpdateRedirects(); + await publicationCacheService.RemovePublication(originalSlug); + await redirectsCacheService.UpdateRedirects(); } await UpdateCachedSupersededPublications(publication); @@ -269,42 +245,42 @@ private async Task UpdateCachedSupersededPublications(Publication publication) { // NOTE: When a publication is updated, any publication that is superseded by it can be affected, so // update any superseded publications that are cached - var supersededPublications = await _context.Publications + var supersededPublications = await context.Publications .Where(p => p.SupersededById == publication.Id) .ToListAsync(); await supersededPublications .ToAsyncEnumerable() - .ForEachAwaitAsync(p => _publicationCacheService.UpdatePublication(p.Slug)); + .ForEachAwaitAsync(p => publicationCacheService.UpdatePublication(p.Slug)); } private async Task> ValidateSelectedTheme(Guid themeId) { - var theme = await _context.Themes.FindAsync(themeId); + var theme = await context.Themes.FindAsync(themeId); if (theme is null) { return ValidationActionResult(ThemeDoesNotExist); } - return await _userService.CheckCanCreatePublicationForTheme(theme) + return await userService.CheckCanCreatePublicationForTheme(theme) .OnSuccess(_ => Unit.Instance); } public async Task> GetPublication( Guid publicationId, bool includePermissions = false) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId, HydratePublication) - .OnSuccess(_userService.CheckCanViewPublication) + .OnSuccess(userService.CheckCanViewPublication) .OnSuccess(publication => GeneratePublicationViewModel(publication, includePermissions)); } public async Task> GetExternalMethodology(Guid publicationId) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId) - .OnSuccessDo(_userService.CheckCanViewPublication) + .OnSuccessDo(userService.CheckCanViewPublication) .OnSuccess(publication => publication.ExternalMethodology != null ? new ExternalMethodologyViewModel(publication.ExternalMethodology) : NotFound()); @@ -313,19 +289,19 @@ public async Task> GetExterna public async Task> UpdateExternalMethodology( Guid publicationId, ExternalMethodologySaveRequest updatedExternalMethodology) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId) - .OnSuccessDo(_userService.CheckCanManageExternalMethodologyForPublication) + .OnSuccessDo(userService.CheckCanManageExternalMethodologyForPublication) .OnSuccess(async publication => { - _context.Update(publication); + context.Update(publication); publication.ExternalMethodology ??= new ExternalMethodology(); publication.ExternalMethodology.Title = updatedExternalMethodology.Title; publication.ExternalMethodology.Url = updatedExternalMethodology.Url; - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); // Update publication cache because ExternalMethodology is in Content.Services.ViewModels.PublicationViewModel - await _publicationCacheService.UpdatePublication(publication.Slug); + await publicationCacheService.UpdatePublication(publication.Slug); return new ExternalMethodologyViewModel(publication.ExternalMethodology); }); @@ -334,17 +310,17 @@ public async Task> UpdateExte public async Task> RemoveExternalMethodology( Guid publicationId) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId) - .OnSuccessDo(_userService.CheckCanManageExternalMethodologyForPublication) + .OnSuccessDo(userService.CheckCanManageExternalMethodologyForPublication) .OnSuccess(async publication => { - _context.Update(publication); + context.Update(publication); publication.ExternalMethodology = null; - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); // Clear cache because ExternalMethodology is in Content.Services.ViewModels.PublicationViewModel - await _publicationCacheService.UpdatePublication(publication.Slug); + await publicationCacheService.UpdatePublication(publication.Slug); return Unit.Instance; }); @@ -352,24 +328,24 @@ public async Task> RemoveExternalMethodology( public async Task> GetContact(Guid publicationId) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId, query => query.Include(p => p.Contact)) - .OnSuccessDo(_userService.CheckCanViewPublication) - .OnSuccess(publication => _mapper.Map(publication.Contact)); + .OnSuccessDo(userService.CheckCanViewPublication) + .OnSuccess(publication => mapper.Map(publication.Contact)); } public async Task> UpdateContact(Guid publicationId, ContactSaveRequest updatedContact) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId, query => query.Include(p => p.Contact)) - .OnSuccessDo(_userService.CheckCanUpdateContact) + .OnSuccessDo(userService.CheckCanUpdateContact) .OnSuccess(async publication => { // Replace existing contact that is shared with another publication with a new // contact, as we want each publication to have its own contact. - if (_context.Publications + if (context.Publications .Any(p => p.ContactId == publication.ContactId && p.Id != publication.Id)) { publication.Contact = new Contact(); @@ -381,12 +357,12 @@ public async Task> UpdateContact(Guid pub : updatedContact.ContactTelNo; publication.Contact.TeamName = updatedContact.TeamName; publication.Contact.TeamEmail = updatedContact.TeamEmail; - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); // Clear cache because Contact is in Content.Services.ViewModels.PublicationViewModel - await _publicationCacheService.UpdatePublication(publication.Slug); + await publicationCacheService.UpdatePublication(publication.Slug); - return _mapper.Map(publication.Contact); + return mapper.Map(publication.Contact); }); } @@ -415,14 +391,14 @@ public async Task>> ListLates bool? live = null, bool includePermissions = false) { - return await _persistenceHelper + return await persistenceHelper .CheckEntityExists(publicationId) - .OnSuccess(_userService.CheckCanViewPublication) + .OnSuccess(userService.CheckCanViewPublication) .OnSuccess(async () => { // Note the 'live' filter is applied after the latest release versions are retrieved. // A published release with a current draft version is deliberately not returned when 'live' is true. - var releaseVersions = (await _releaseVersionRepository.ListLatestReleaseVersions(publicationId)) + var releaseVersions = (await releaseVersionRepository.ListLatestReleaseVersions(publicationId)) .Where(rv => live == null || rv.Live == live) .ToList(); @@ -436,9 +412,9 @@ public async Task>> ListLates public async Task>> GetReleaseSeries( Guid publicationId) { - return await _context.Publications + return await context.Publications .FirstOrNotFoundAsync(p => p.Id == publicationId) - .OnSuccess(_userService.CheckCanViewPublication) + .OnSuccess(userService.CheckCanViewPublication) .OnSuccess(async publication => { var result = new List(); @@ -455,10 +431,10 @@ public async Task>> } else { - var release = await _context.Releases + var release = await context.Releases .SingleAsync(r => r.Id == seriesItem.ReleaseId); - var latestPublishedReleaseVersion = await _context.ReleaseVersions + var latestPublishedReleaseVersion = await context.ReleaseVersions .LatestReleaseVersion(releaseId: seriesItem.ReleaseId!.Value, publishedOnly: true) .SingleOrDefaultAsync(); @@ -483,9 +459,9 @@ public async Task>> Guid publicationId, ReleaseSeriesLegacyLinkAddRequest newLegacyLink) { - return await _context.Publications + return await context.Publications .FirstOrNotFoundAsync(p => p.Id == publicationId) - .OnSuccess(_userService.CheckCanManageReleaseSeries) + .OnSuccess(userService.CheckCanManageReleaseSeries) .OnSuccess(async publication => { publication.ReleaseSeries.Add(new ReleaseSeriesItem @@ -495,10 +471,10 @@ public async Task>> LegacyLinkUrl = newLegacyLink.Url, }); - _context.Publications.Update(publication); - await _context.SaveChangesAsync(); + context.Publications.Update(publication); + await context.SaveChangesAsync(); - await _publicationCacheService.UpdatePublication(publication.Slug); + await publicationCacheService.UpdatePublication(publication.Slug); return await GetReleaseSeries(publication.Id); }); @@ -508,9 +484,9 @@ public async Task>> Guid publicationId, List updatedReleaseSeriesItems) { - return await _context.Publications + return await context.Publications .FirstOrNotFoundAsync(p => p.Id == publicationId) - .OnSuccess(_userService.CheckCanManageReleaseSeries) + .OnSuccess(userService.CheckCanManageReleaseSeries) .OnSuccess(async publication => { // Check new series items details are correct @@ -530,7 +506,7 @@ public async Task>> } // Check all publication releases are included in updatedReleaseSeriesItems - var publicationReleaseIds = await _context.Releases + var publicationReleaseIds = await context.Releases .Where(r => r.PublicationId == publicationId) .Select(r => r.Id) .ToListAsync(); @@ -556,9 +532,9 @@ public async Task>> LegacyLinkUrl = request.LegacyLinkUrl, }).ToList(); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); - await _publicationCacheService.UpdatePublication(publication.Slug); + await publicationCacheService.UpdatePublication(publication.Slug); return await GetReleaseSeries(publication.Id); }); @@ -567,7 +543,7 @@ public async Task>> private async Task> ValidatePublicationSlug( string newSlug, Guid? publicationId = null) { - if (await _context.Publications + if (await context.Publications .AnyAsync(publication => publication.Id != publicationId && publication.Slug == newSlug)) @@ -575,7 +551,7 @@ private async Task> ValidatePublicationSlug( return ValidationActionResult(PublicationSlugNotUnique); } - var hasRedirect = await _context.PublicationRedirects + var hasRedirect = await context.PublicationRedirects .AnyAsync(pr => pr.PublicationId != publicationId // If publication previously used this slug, can change it back && pr.Slug == newSlug); @@ -586,7 +562,7 @@ private async Task> ValidatePublicationSlug( } if (publicationId.HasValue && - _context.PublicationMethodologies.Any(pm => + context.PublicationMethodologies.Any(pm => pm.Publication.Id == publicationId && pm.Owner) // Strictly, we should also check whether the owned methodology inherits the publication slug - we don't @@ -594,7 +570,7 @@ private async Task> ValidatePublicationSlug( // this check is expensive and an unlikely edge case, so doesn't seem worth it. ) { - var methodologySlugValidation = await _methodologyService + var methodologySlugValidation = await methodologyService .ValidateMethodologySlug(newSlug); if (methodologySlugValidation.IsLeft) { @@ -614,14 +590,14 @@ public static IQueryable HydratePublication(IQueryable private async Task GeneratePublicationViewModel(Publication publication, bool includePermissions = false) { - var publicationViewModel = _mapper.Map(publication); + var publicationViewModel = mapper.Map(publication); - publicationViewModel.IsSuperseded = await _publicationRepository.IsSuperseded(publication.Id); + publicationViewModel.IsSuperseded = await publicationRepository.IsSuperseded(publication.Id); if (includePermissions) { publicationViewModel.Permissions = - await PermissionsUtils.GetPublicationPermissions(_userService, publication); + await PermissionsUtils.GetPublicationPermissions(userService, publication); } return publicationViewModel; @@ -629,9 +605,9 @@ private async Task GeneratePublicationViewModel(Publicatio private async Task GeneratePublicationCreateViewModel(Publication publication) { - var publicationCreateViewModel = _mapper.Map(publication); + var publicationCreateViewModel = mapper.Map(publication); - publicationCreateViewModel.IsSuperseded = await _publicationRepository.IsSuperseded(publication.Id); + publicationCreateViewModel.IsSuperseded = await publicationRepository.IsSuperseded(publication.Id); return publicationCreateViewModel; } @@ -639,11 +615,11 @@ private async Task GeneratePublicationCreateViewMode private async Task HydrateReleaseListItemViewModel(ReleaseVersion releaseVersion, bool includePermissions) { - var viewModel = _mapper.Map(releaseVersion); + var viewModel = mapper.Map(releaseVersion); if (includePermissions) { - viewModel.Permissions = await PermissionsUtils.GetReleasePermissions(_userService, releaseVersion); + viewModel.Permissions = await PermissionsUtils.GetReleasePermissions(userService, releaseVersion); } return viewModel; From 3043196c08ad068ac2702c469f5083ee0880067a Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Tue, 10 Dec 2024 09:40:52 +0000 Subject: [PATCH 11/40] EES-5656 Set the latest published release version for publication based on the latest release with a published version --- .../PublicationServicePermissionTests.cs | 2 + .../Services/PublicationServiceTests.cs | 169 ++++++++++++++++-- .../Services/PublicationService.cs | 29 +++ 3 files changed, 184 insertions(+), 16 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs index 75477c2195e..62cc49678fc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs @@ -627,6 +627,7 @@ private static PublicationService BuildPublicationService( IReleaseVersionRepository? releaseVersionRepository = null, IMethodologyService? methodologyService = null, IPublicationCacheService? publicationCacheService = null, + IReleaseCacheService? releaseCacheService = null, IMethodologyCacheService? methodologyCacheService = null, IRedirectsCacheService? redirectsCacheService = null) { @@ -641,6 +642,7 @@ private static PublicationService BuildPublicationService( releaseVersionRepository ?? Mock.Of(Strict), methodologyService ?? Mock.Of(Strict), publicationCacheService ?? Mock.Of(Strict), + releaseCacheService ?? Mock.Of(Strict), methodologyCacheService ?? Mock.Of(Strict), redirectsCacheService ?? Mock.Of(Strict)); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs index 39fb7872c3f..768316817bd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs @@ -2942,7 +2942,7 @@ public async Task UpdateReleaseSeries() _dataFixture .DefaultRelease(publishedVersions: 1, year: 2020), _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + .DefaultRelease(publishedVersions: 1, draftVersion: true, year: 2021), _dataFixture .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022) ]) @@ -2953,6 +2953,16 @@ public async Task UpdateReleaseSeries() var release2021 = publication.Releases.Single(r => r.Year == 2021); var release2022 = publication.Releases.Single(r => r.Year == 2022); + // Check the publication's latest published release version in the generated test data setup + Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); + + // Check the expected order of the release series items in the generated test data setup + Assert.Equal(4, publication.ReleaseSeries.Count); + Assert.Equal(release2022.Id, publication.ReleaseSeries[0].ReleaseId); + Assert.Equal(release2021.Id, publication.ReleaseSeries[1].ReleaseId); + Assert.Equal(release2020.Id, publication.ReleaseSeries[2].ReleaseId); + Assert.True(publication.ReleaseSeries[3].IsLegacyLink); + var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { @@ -2978,11 +2988,11 @@ public async Task UpdateReleaseSeries() new ReleaseSeriesItemUpdateRequest { LegacyLinkDescription = "Legacy link new", - LegacyLinkUrl = "https://test.com/new", + LegacyLinkUrl = "https://test.com/new" }, new ReleaseSeriesItemUpdateRequest { - ReleaseId = release2021.Id + ReleaseId = release2022.Id }, new ReleaseSeriesItemUpdateRequest { @@ -2990,14 +3000,13 @@ public async Task UpdateReleaseSeries() }, new ReleaseSeriesItemUpdateRequest { - ReleaseId = release2022.Id + ReleaseId = release2021.Id } ]); VerifyAllMocks(publicationCacheService); var viewModels = result.AssertRight(); - Assert.Equal(4, viewModels.Count); Assert.True(viewModels[0].IsLegacyLink); @@ -3009,11 +3018,11 @@ public async Task UpdateReleaseSeries() Assert.Equal("https://test.com/new", viewModels[0].LegacyLinkUrl); Assert.False(viewModels[1].IsLegacyLink); - Assert.Equal(release2021.Title, viewModels[1].Description); - Assert.Equal(release2021.Id, viewModels[1].ReleaseId); - Assert.Equal(release2021.Slug, viewModels[1].ReleaseSlug); - Assert.False(viewModels[1].IsLatest); - Assert.False(viewModels[1].IsPublished); + Assert.Equal(release2022.Title, viewModels[1].Description); + Assert.Equal(release2022.Id, viewModels[1].ReleaseId); + Assert.Equal(release2022.Slug, viewModels[1].ReleaseSlug); + Assert.True(viewModels[1].IsLatest); + Assert.True(viewModels[1].IsPublished); Assert.Null(viewModels[1].LegacyLinkUrl); Assert.False(viewModels[2].IsLegacyLink); @@ -3025,10 +3034,10 @@ public async Task UpdateReleaseSeries() Assert.Null(viewModels[2].LegacyLinkUrl); Assert.False(viewModels[3].IsLegacyLink); - Assert.Equal(release2022.Title, viewModels[3].Description); - Assert.Equal(release2022.Id, viewModels[3].ReleaseId); - Assert.Equal(release2022.Slug, viewModels[3].ReleaseSlug); - Assert.True(viewModels[3].IsLatest); + Assert.Equal(release2021.Title, viewModels[3].Description); + Assert.Equal(release2021.Id, viewModels[3].ReleaseId); + Assert.Equal(release2021.Slug, viewModels[3].ReleaseSlug); + Assert.False(viewModels[3].IsLatest); Assert.True(viewModels[3].IsPublished); Assert.Null(viewModels[3].LegacyLinkUrl); } @@ -3044,9 +3053,135 @@ public async Task UpdateReleaseSeries() Assert.Equal("Legacy link new", actualReleaseSeries[0].LegacyLinkDescription); Assert.Equal("https://test.com/new", actualReleaseSeries[0].LegacyLinkUrl); - Assert.Equal(release2021.Id, actualReleaseSeries[1].ReleaseId); + Assert.Equal(release2022.Id, actualReleaseSeries[1].ReleaseId); Assert.Equal(release2020.Id, actualReleaseSeries[2].ReleaseId); - Assert.Equal(release2022.Id, actualReleaseSeries[3].ReleaseId); + Assert.Equal(release2021.Id, actualReleaseSeries[3].ReleaseId); + + // The publication's latest published release version should be unchanged as 2022 was positioned + // as the first release after the legacy link + Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); + } + } + + [Fact] + public async Task UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion() + { + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases([ + _dataFixture + .DefaultRelease(publishedVersions: 1, year: 2020), + _dataFixture + .DefaultRelease(publishedVersions: 1, draftVersion: true, year: 2021), + _dataFixture + .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022) + ]) + .WithTheme(_dataFixture.DefaultTheme()); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + var release2021 = publication.Releases.Single(r => r.Year == 2021); + var release2022 = publication.Releases.Single(r => r.Year == 2022); + + var expectedLatestPublishedReleaseVersionId = release2021.Versions[0].Id; + + // Check the publication's latest published release version in the generated test data setup + Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); + + // Check the expected order of the release series items in the generated test data setup + Assert.Equal(3, publication.ReleaseSeries.Count); + Assert.Equal(release2022.Id, publication.ReleaseSeries[0].ReleaseId); + Assert.Equal(release2021.Id, publication.ReleaseSeries[1].ReleaseId); + Assert.Equal(release2020.Id, publication.ReleaseSeries[2].ReleaseId); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + { + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + { + var publicationCacheService = new Mock(Strict); + publicationCacheService.Setup(mock => + mock.UpdatePublication(publication.Slug)) + .ReturnsAsync(new PublicationCacheViewModel()); + + var releaseCacheService = new Mock(Strict); + releaseCacheService.Setup(mock => mock.UpdateRelease( + expectedLatestPublishedReleaseVersionId, + publication.Slug, + null)) + .ReturnsAsync(new ReleaseCacheViewModel(expectedLatestPublishedReleaseVersionId)); + + var publicationService = BuildPublicationService( + contentDbContext, + publicationCacheService: publicationCacheService.Object, + releaseCacheService: releaseCacheService.Object); + + var result = await publicationService.UpdateReleaseSeries( + publication.Id, + updatedReleaseSeriesItems: + [ + new ReleaseSeriesItemUpdateRequest + { + ReleaseId = release2021.Id + }, + new ReleaseSeriesItemUpdateRequest + { + ReleaseId = release2020.Id + }, + new ReleaseSeriesItemUpdateRequest + { + ReleaseId = release2022.Id + } + ]); + + VerifyAllMocks(publicationCacheService, releaseCacheService); + + var viewModels = result.AssertRight(); + Assert.Equal(3, viewModels.Count); + + Assert.False(viewModels[0].IsLegacyLink); + Assert.Equal(release2021.Title, viewModels[0].Description); + Assert.Equal(release2021.Id, viewModels[0].ReleaseId); + Assert.Equal(release2021.Slug, viewModels[0].ReleaseSlug); + Assert.True(viewModels[0].IsLatest); + Assert.True(viewModels[0].IsPublished); + Assert.Null(viewModels[0].LegacyLinkUrl); + + Assert.False(viewModels[1].IsLegacyLink); + Assert.Equal(release2020.Title, viewModels[1].Description); + Assert.Equal(release2020.Id, viewModels[1].ReleaseId); + Assert.Equal(release2020.Slug, viewModels[1].ReleaseSlug); + Assert.False(viewModels[1].IsLatest); + Assert.True(viewModels[1].IsPublished); + Assert.Null(viewModels[1].LegacyLinkUrl); + + Assert.False(viewModels[2].IsLegacyLink); + Assert.Equal(release2022.Title, viewModels[2].Description); + Assert.Equal(release2022.Id, viewModels[2].ReleaseId); + Assert.Equal(release2022.Slug, viewModels[2].ReleaseSlug); + Assert.False(viewModels[2].IsLatest); + Assert.True(viewModels[2].IsPublished); + Assert.Null(viewModels[2].LegacyLinkUrl); + } + + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + { + var actualPublication = await contentDbContext.Publications + .SingleAsync(p => p.Id == publication.Id); + + var actualReleaseSeries = actualPublication.ReleaseSeries; + Assert.Equal(3, actualReleaseSeries.Count); + + Assert.Equal(release2021.Id, actualReleaseSeries[0].ReleaseId); + Assert.Equal(release2020.Id, actualReleaseSeries[1].ReleaseId); + Assert.Equal(release2022.Id, actualReleaseSeries[2].ReleaseId); + + // The latest published version of 2021 should now be the publication's latest published release + // version since it was positioned as the first release + Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); } } @@ -3246,6 +3381,7 @@ private static PublicationService BuildPublicationService( IReleaseVersionRepository? releaseVersionRepository = null, IMethodologyService? methodologyService = null, IPublicationCacheService? publicationCacheService = null, + IReleaseCacheService? releaseCacheService = null, IMethodologyCacheService? methodologyCacheService = null, IRedirectsCacheService? redirectsCacheService = null) { @@ -3258,6 +3394,7 @@ private static PublicationService BuildPublicationService( releaseVersionRepository ?? new ReleaseVersionRepository(context), methodologyService ?? Mock.Of(Strict), publicationCacheService ?? Mock.Of(Strict), + releaseCacheService ?? Mock.Of(Strict), methodologyCacheService ?? Mock.Of(Strict), redirectsCacheService ?? Mock.Of(Strict)); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs index f3cb0a8689d..43b256454ac 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs @@ -40,6 +40,7 @@ public class PublicationService( IReleaseVersionRepository releaseVersionRepository, IMethodologyService methodologyService, IPublicationCacheService publicationCacheService, + IReleaseCacheService releaseCacheService, IMethodologyCacheService methodologyCacheService, IRedirectsCacheService redirectsCacheService) : IPublicationService @@ -523,6 +524,24 @@ public async Task>> publicationReleaseIds.JoinToString(",")); } + // Work out the publication's new latest published release version (if any). + // This is the latest published version of the first release which has a published version + Guid? latestPublishedReleaseVersionId = null; + foreach (var releaseId in updatedSeriesReleaseIds) + { + latestPublishedReleaseVersionId = (await context.ReleaseVersions + .LatestReleaseVersion(releaseId: releaseId, publishedOnly: true) + .SingleOrDefaultAsync())?.Id; + + if (latestPublishedReleaseVersionId != null) + { + break; + } + } + + var oldLatestPublishedReleaseVersionId = publication.LatestPublishedReleaseVersionId; + publication.LatestPublishedReleaseVersionId = latestPublishedReleaseVersionId; + publication.ReleaseSeries = updatedReleaseSeriesItems .Select(request => new ReleaseSeriesItem { @@ -534,8 +553,18 @@ public async Task>> await context.SaveChangesAsync(); + // Update the cached publication await publicationCacheService.UpdatePublication(publication.Slug); + // If the publication's latest published release version has changed, + // update the publication's cached latest release version + if (oldLatestPublishedReleaseVersionId != latestPublishedReleaseVersionId && + latestPublishedReleaseVersionId.HasValue) + { + await releaseCacheService.UpdateRelease(releaseVersionId: latestPublishedReleaseVersionId.Value, + publicationSlug: publication.Slug); + } + return await GetReleaseSeries(publication.Id); }); } From a12a20037f8ad81852eeb58a3126f7cd4c85eb5f Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 12 Dec 2024 09:46:04 +0000 Subject: [PATCH 12/40] EES-5656 Add UI tests to cover changing a publication's latest published release --- .../bau/release_reordering.robot | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot b/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot index 54fcece6839..9daf295d23d 100644 --- a/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot +++ b/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot @@ -136,8 +136,13 @@ Validate first release has latest release status in publication release order user checks table cell contains 3 4 Edit user checks table cell contains 3 4 Delete -Validate other releases section on public frontend +Navigate to first published release on public frontend user navigates to public frontend ${PUBLIC_RELEASE_1_URL} + +Validate first published release on public frontend is the latest data + user checks page contains This is the latest data + +Validate other releases section of first published release includes legacy releases user checks number of other releases is correct 2 ${view_releases}= user opens details dropdown View releases (2) @@ -230,8 +235,13 @@ Validate reordered publication releases user checks table cell contains 3 2 ${PUBLIC_RELEASE_1_URL} user checks table cell contains 3 3 Latest release -Validate other releases section on public frontend includes updated legacy release with expected order +Navigate to first published release on public frontend after reordering user navigates to public frontend ${PUBLIC_RELEASE_1_URL} + +Validate first published release is the latest data after reordering + user checks page contains This is the latest data + +Validate other releases section of first published release contains updated legacy release in expected order user checks number of other releases is correct 2 ${view_releases}= user opens details dropdown View releases (2) @@ -317,8 +327,13 @@ Validate second release has latest release status in publication release order user checks table cell contains 4 2 ${PUBLIC_RELEASE_1_URL} user checks table cell does not contain 4 3 Latest release -Validate other releases section on public frontend includes first release with expected order +Navigate to second published release on public frontend user navigates to public frontend ${PUBLIC_RELEASE_2_URL} + +Validate second published release is the latest data + user checks page contains This is the latest data + +Validate other releases section of second published release includes first release with expected order user checks number of other releases is correct 3 ${view_releases}= user opens details dropdown View releases (3) @@ -392,10 +407,62 @@ Validate first legacy release is deleted from publication release order user checks table cell contains 3 2 ${PUBLIC_RELEASE_1_URL} user checks table cell does not contain 3 3 Latest release -Validate other releases section on public frontend does not include first legacy release +Navigate to second published release on public frontend after deleting legacy release user navigates to public frontend ${PUBLIC_RELEASE_2_URL} + +Validate other releases section of second published release does not include first legacy release user checks number of other releases is correct 2 ${view_releases}= user opens details dropdown View releases (2) user checks other release is shown in position ${LEGACY_RELEASE_2_DESCRIPTION} 1 user checks other release is shown in position ${RELEASE_1_NAME} 2 + +Reorder the publication releases so the first release is the latest release + user navigates to publication page from dashboard ${PUBLICATION_NAME} + user clicks link Release order + user waits until h2 is visible Release order + + user clicks button Reorder releases + ${modal}= user waits until modal is visible Reorder releases + user clicks button OK ${modal} + user waits until modal is not visible Reorder releases + user waits until page contains button Confirm order + + click element xpath://div[text()="${RELEASE_1_NAME}"] CTRL + user presses keys ${SPACE} + user presses keys ARROW_UP + user presses keys ARROW_UP + user presses keys ${SPACE} + + user clicks button Confirm order + sleep 2 + +Validate first release has latest release status in publication release order after reordering + user waits until page contains button Reorder releases + user checks table body has x rows 3 testid:release-series + + user checks table cell contains 1 1 ${RELEASE_1_NAME} + user checks table cell contains 1 2 ${PUBLIC_RELEASE_1_URL} + user checks table cell contains 1 3 Latest release + + user checks table cell contains 2 1 ${RELEASE_2_NAME} + user checks table cell contains 2 2 ${PUBLIC_RELEASE_2_URL} + user checks table cell does not contain 2 3 Latest release + + user checks table cell contains 3 1 ${LEGACY_RELEASE_2_DESCRIPTION} + user checks table cell contains 3 2 ${LEGACY_RELEASE_2_URL} + user checks table cell contains 3 3 Legacy release + user checks table cell contains 3 4 Edit + user checks table cell contains 3 4 Delete + +Navigate to first published release on public frontend after changing the latest release + user navigates to public frontend ${PUBLIC_RELEASE_1_URL} + +Validate first published release is the latest data after changing the latest release + user checks page contains This is the latest data + +Navigate to second published release on public frontend after changing the latest release + user navigates to public frontend ${PUBLIC_RELEASE_2_URL} + +Validate second published release is not the latest data after changing the latest release + user checks page contains This is not the latest data From a15bf4fb93cd13cfe670ade6b8f874836898e70d Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 12 Dec 2024 15:43:10 +0000 Subject: [PATCH 13/40] EES-5656 Correct mistake in UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion --- .../Services/PublicationServiceTests.cs | 45 +++++-------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs index 768316817bd..7af28748d0e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs @@ -2990,18 +2990,9 @@ public async Task UpdateReleaseSeries() LegacyLinkDescription = "Legacy link new", LegacyLinkUrl = "https://test.com/new" }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2022.Id - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2020.Id - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2021.Id - } + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2022.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2020.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2021.Id } ]); VerifyAllMocks(publicationCacheService); @@ -3059,7 +3050,7 @@ public async Task UpdateReleaseSeries() // The publication's latest published release version should be unchanged as 2022 was positioned // as the first release after the legacy link - Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); + Assert.Equal(release2022.Versions[1].Id, actualPublication.LatestPublishedReleaseVersionId); } } @@ -3123,18 +3114,9 @@ public async Task UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion() publication.Id, updatedReleaseSeriesItems: [ - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2021.Id - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2020.Id - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2022.Id - } + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2021.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2020.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2022.Id } ]); VerifyAllMocks(publicationCacheService, releaseCacheService); @@ -3181,7 +3163,8 @@ public async Task UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion() // The latest published version of 2021 should now be the publication's latest published release // version since it was positioned as the first release - Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); + Assert.Equal(expectedLatestPublishedReleaseVersionId, + actualPublication.LatestPublishedReleaseVersionId); } } @@ -3288,14 +3271,8 @@ public async Task UpdateReleaseSeries_SetDuplicateRelease() publication.Id, updatedReleaseSeriesItems: [ - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release.Id - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release.Id - } + new ReleaseSeriesItemUpdateRequest { ReleaseId = release.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release.Id } ])); Assert.Equal($"Missing or duplicate release in new release series. Expected ReleaseIds: {release.Id}", From 9787a3dc726c343f9071fe06bfe41cee48dd6f00 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 12 Dec 2024 16:38:40 +0000 Subject: [PATCH 14/40] EES-5656 Add additional test UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion_SkipsUnpublishedReleases and reduce level of detail checked in UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion --- .../Services/PublicationServiceTests.cs | 114 ++++++++++++++++-- 1 file changed, 102 insertions(+), 12 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs index 7af28748d0e..de4e453532c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs @@ -3124,29 +3124,17 @@ public async Task UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion() var viewModels = result.AssertRight(); Assert.Equal(3, viewModels.Count); - Assert.False(viewModels[0].IsLegacyLink); - Assert.Equal(release2021.Title, viewModels[0].Description); Assert.Equal(release2021.Id, viewModels[0].ReleaseId); - Assert.Equal(release2021.Slug, viewModels[0].ReleaseSlug); Assert.True(viewModels[0].IsLatest); Assert.True(viewModels[0].IsPublished); - Assert.Null(viewModels[0].LegacyLinkUrl); - Assert.False(viewModels[1].IsLegacyLink); - Assert.Equal(release2020.Title, viewModels[1].Description); Assert.Equal(release2020.Id, viewModels[1].ReleaseId); - Assert.Equal(release2020.Slug, viewModels[1].ReleaseSlug); Assert.False(viewModels[1].IsLatest); Assert.True(viewModels[1].IsPublished); - Assert.Null(viewModels[1].LegacyLinkUrl); - Assert.False(viewModels[2].IsLegacyLink); - Assert.Equal(release2022.Title, viewModels[2].Description); Assert.Equal(release2022.Id, viewModels[2].ReleaseId); - Assert.Equal(release2022.Slug, viewModels[2].ReleaseSlug); Assert.False(viewModels[2].IsLatest); Assert.True(viewModels[2].IsPublished); - Assert.Null(viewModels[2].LegacyLinkUrl); } await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) @@ -3168,6 +3156,108 @@ public async Task UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion() } } + [Fact] + public async Task UpdateReleaseSeries_UpdatesLatestPublishedReleaseVersion_SkipsUnpublishedReleases() + { + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases([ + _dataFixture + .DefaultRelease(publishedVersions: 1, year: 2020), + _dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _dataFixture + .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022) + ]) + .WithTheme(_dataFixture.DefaultTheme()); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + var release2021 = publication.Releases.Single(r => r.Year == 2021); + var release2022 = publication.Releases.Single(r => r.Year == 2022); + + var expectedLatestPublishedReleaseVersionId = release2020.Versions[0].Id; + + // Check the publication's latest published release version in the generated test data setup + Assert.Equal(release2022.Versions[1].Id, publication.LatestPublishedReleaseVersionId); + + // Check the expected order of the release series items in the generated test data setup + Assert.Equal(3, publication.ReleaseSeries.Count); + Assert.Equal(release2022.Id, publication.ReleaseSeries[0].ReleaseId); + Assert.Equal(release2021.Id, publication.ReleaseSeries[1].ReleaseId); + Assert.Equal(release2020.Id, publication.ReleaseSeries[2].ReleaseId); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + { + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + { + var publicationCacheService = new Mock(Strict); + publicationCacheService.Setup(mock => + mock.UpdatePublication(publication.Slug)) + .ReturnsAsync(new PublicationCacheViewModel()); + + var releaseCacheService = new Mock(Strict); + releaseCacheService.Setup(mock => mock.UpdateRelease( + expectedLatestPublishedReleaseVersionId, + publication.Slug, + null)) + .ReturnsAsync(new ReleaseCacheViewModel(expectedLatestPublishedReleaseVersionId)); + + var publicationService = BuildPublicationService( + contentDbContext, + publicationCacheService: publicationCacheService.Object, + releaseCacheService: releaseCacheService.Object); + + var result = await publicationService.UpdateReleaseSeries( + publication.Id, + updatedReleaseSeriesItems: + [ + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2021.Id }, // Unpublished + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2020.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2022.Id } + ]); + + VerifyAllMocks(publicationCacheService, releaseCacheService); + + var viewModels = result.AssertRight(); + Assert.Equal(3, viewModels.Count); + + Assert.Equal(release2021.Id, viewModels[0].ReleaseId); + Assert.False(viewModels[0].IsLatest); + Assert.False(viewModels[0].IsPublished); + + Assert.Equal(release2020.Id, viewModels[1].ReleaseId); + Assert.True(viewModels[1].IsLatest); + Assert.True(viewModels[1].IsPublished); + + Assert.Equal(release2022.Id, viewModels[2].ReleaseId); + Assert.False(viewModels[2].IsLatest); + Assert.True(viewModels[2].IsPublished); + } + + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + { + var actualPublication = await contentDbContext.Publications + .SingleAsync(p => p.Id == publication.Id); + + var actualReleaseSeries = actualPublication.ReleaseSeries; + Assert.Equal(3, actualReleaseSeries.Count); + + Assert.Equal(release2021.Id, actualReleaseSeries[0].ReleaseId); + Assert.Equal(release2020.Id, actualReleaseSeries[1].ReleaseId); + Assert.Equal(release2022.Id, actualReleaseSeries[2].ReleaseId); + + // The latest published version of 2020 should now be the publication's latest published release + // version since it was positioned as the next release after 2021 which is unpublished + Assert.Equal(expectedLatestPublishedReleaseVersionId, + actualPublication.LatestPublishedReleaseVersionId); + } + } + [Fact] public async Task UpdateReleaseSeries_SetEmpty() { From 5fba23dee9d17b7590074b06b6b78d6826d8d41f Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 12 Dec 2024 17:05:07 +0000 Subject: [PATCH 15/40] EES-5656 Remove incorrect mention of ordering in method comments --- .../Repository/Interfaces/IReleaseVersionRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs index f17b7e734cb..7333c2e4be9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs @@ -65,7 +65,7 @@ Task IsLatestReleaseVersion( CancellationToken cancellationToken = default); /// - /// Retrieves the latest published release version id's associated with a publication in reverse chronological order. + /// Retrieves the latest published release version id's associated with a publication. /// /// The unique identifier of the publication. /// A to observe while waiting for the task to complete. @@ -85,7 +85,7 @@ Task> ListLatestPublishedReleaseVersions( CancellationToken cancellationToken = default); /// - /// Retrieves the latest version id's of all releases associated with a publication in reverse chronological order. + /// Retrieves the latest version id's of all releases associated with a publication. /// /// The unique identifier of the publication. /// A to observe while waiting for the task to complete. From 303e0a1203717b55a43c6dce2bc304746173bf52 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 12 Dec 2024 17:05:21 +0000 Subject: [PATCH 16/40] EES-5656 Remove mention of 'published' in method comments because it's dependent on the value of `publishedOnly` --- .../Predicates/ReleaseVersionPredicates.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs index 90fd93d88a5..57560809186 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs @@ -11,7 +11,7 @@ public static class ReleaseVersionPredicates { /// /// Filters a sequence of of type to only include the latest - /// published versions of each release. + /// versions of each release. /// /// The source of type to filter. /// Unique identifier of a publication to filter by. @@ -50,20 +50,20 @@ public static IQueryable LatestReleaseVersions(this IQueryable /// Filters a sequence of of type to only include the latest - /// published version of the release. + /// version of the release. /// - /// The source of type to filter. + /// The source of type to filter. /// Unique identifier of a release to filter by. /// Flag to only include published release versions. /// An of type that contains elements from the input /// sequence filtered to only include the latest version of the release. - public static IQueryable LatestReleaseVersion(this IQueryable releaseVersionsQueryable, + public static IQueryable LatestReleaseVersion(this IQueryable releaseVersions, Guid releaseId, bool publishedOnly = false) { - return releaseVersionsQueryable + return releaseVersions .Where(releaseVersion => releaseVersion.ReleaseId == releaseId) - .Where(releaseVersion => releaseVersion.Version == releaseVersionsQueryable + .Where(releaseVersion => releaseVersion.Version == releaseVersions .Where(latestVersion => latestVersion.ReleaseId == releaseId) .Where(latestVersion => !publishedOnly || latestVersion.Published.HasValue) .Select(latestVersion => (int?)latestVersion.Version) From 2bb26de3ae678b3952a2f44e1962014890749611 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 12 Dec 2024 17:08:19 +0000 Subject: [PATCH 17/40] EES-5656 Check latest release link has correct release name on non-latest release page --- .../tests/admin_and_public/bau/release_reordering.robot | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot b/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot index 9daf295d23d..2aaa4188822 100644 --- a/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot +++ b/tests/robot-tests/tests/admin_and_public/bau/release_reordering.robot @@ -466,3 +466,4 @@ Navigate to second published release on public frontend after changing the lates Validate second published release is not the latest data after changing the latest release user checks page contains This is not the latest data + user waits until page contains link View latest data: ${RELEASE_1_NAME} From 7ff9fe3deb4c1407bd09b78b54c77ee034f5f253 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Fri, 13 Dec 2024 17:54:08 +0000 Subject: [PATCH 18/40] EES-5656 Change ManageContentPageService.GetManageContentPageViewModel to not use method ListLatestPublishedReleaseVersions --- .../ManageContentPageServiceTests.cs | 9 +-- .../ManageContent/ManageContentPageService.cs | 74 ++++++++++--------- .../Startup.cs | 1 + .../Interfaces/IReleaseRepository.cs | 30 ++++++++ .../Repository/ReleaseRepository.cs | 40 ++++++++++ 5 files changed, 113 insertions(+), 41 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs index 7d7b01ece26..f4dea4b73af 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs @@ -17,6 +17,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using Microsoft.AspNetCore.Mvc; @@ -26,10 +27,6 @@ using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using HtmlBlockViewModel = GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.HtmlBlockViewModel; -using IReleaseVersionRepository = - GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces.IReleaseVersionRepository; -using ReleaseVersionRepository = - GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.ReleaseVersionRepository; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.ManageContent { @@ -678,7 +675,7 @@ private static ManageContentPageService SetupManageContentPageService( IDataBlockService? dataBlockService = null, IMethodologyVersionRepository? methodologyVersionRepository = null, IReleaseFileService? releaseFileService = null, - IReleaseVersionRepository? releaseVersionRepository = null, + IReleaseRepository? releaseRepository = null, IUserService? userService = null) { return new( @@ -688,7 +685,7 @@ private static ManageContentPageService SetupManageContentPageService( dataBlockService ?? new Mock().Object, methodologyVersionRepository ?? new Mock().Object, releaseFileService ?? new Mock().Object, - releaseVersionRepository ?? new ReleaseVersionRepository(contentDbContext), + releaseRepository ?? new ReleaseRepository(contentDbContext), userService ?? MockUtils.AlwaysTrueUserService().Object ); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ManageContent/ManageContentPageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ManageContent/ManageContentPageService.cs index f6c6c038a9c..84ba3798b60 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ManageContent/ManageContentPageService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ManageContent/ManageContentPageService.cs @@ -19,7 +19,6 @@ using System.Linq; using System.Threading.Tasks; using static GovUk.Education.ExploreEducationStatistics.Common.Model.FileType; -using IReleaseVersionRepository = GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces.IReleaseVersionRepository; namespace GovUk.Education.ExploreEducationStatistics.Admin.Services.ManageContent { @@ -31,7 +30,7 @@ public class ManageContentPageService : IManageContentPageService private readonly IDataBlockService _dataBlockService; private readonly IMethodologyVersionRepository _methodologyVersionRepository; private readonly IReleaseFileService _releaseFileService; - private readonly IReleaseVersionRepository _releaseVersionRepository; + private readonly IReleaseRepository _releaseRepository; private readonly IUserService _userService; public ManageContentPageService( @@ -41,7 +40,7 @@ public ManageContentPageService( IDataBlockService dataBlockService, IMethodologyVersionRepository methodologyVersionRepository, IReleaseFileService releaseFileService, - IReleaseVersionRepository releaseVersionRepository, + IReleaseRepository releaseRepository, IUserService userService) { _contentDbContext = contentDbContext; @@ -50,7 +49,7 @@ public ManageContentPageService( _dataBlockService = dataBlockService; _methodologyVersionRepository = methodologyVersionRepository; _releaseFileService = releaseFileService; - _releaseVersionRepository = releaseVersionRepository; + _releaseRepository = releaseRepository; _userService = userService; } @@ -101,38 +100,12 @@ public async Task> GetManageCon var releaseViewModel = _mapper.Map(releaseVersion); - // Hydrate Publication.ReleaseSeries - var publishedVersions = - await _releaseVersionRepository - .ListLatestPublishedReleaseVersions(releaseVersion.PublicationId); - var filteredReleaseSeries = releaseVersion.Publication.ReleaseSeries - .Where(rsi => // only show items for legacy links and published releases - rsi.IsLegacyLink - || publishedVersions - .Any(rv => rsi.ReleaseId == rv.ReleaseId) - ).ToList(); - releaseViewModel.Publication.ReleaseSeries = filteredReleaseSeries - .Select(rsi => - { - if (rsi.IsLegacyLink) - { - return new ReleaseSeriesItemViewModel - { - Description = rsi.LegacyLinkDescription!, - LegacyLinkUrl = rsi.LegacyLinkUrl, - }; - } - - var latestReleaseVersion = publishedVersions - .Single(rv => rv.ReleaseId == rsi.ReleaseId); + var publishedReleases = + await _releaseRepository.ListPublishedReleases(releaseVersion.PublicationId); - return new ReleaseSeriesItemViewModel - { - Description = latestReleaseVersion.Title, - ReleaseId = latestReleaseVersion.ReleaseId, - ReleaseSlug = latestReleaseVersion.Slug, - }; - }).ToList(); + // Hydrate Publication.ReleaseSeries + releaseViewModel.Publication.ReleaseSeries = + BuildReleaseSeriesItemViewModels(releaseVersion.Publication, publishedReleases); releaseViewModel.DownloadFiles = files.ToList(); releaseViewModel.Publication.Methodologies = @@ -146,6 +119,37 @@ await _releaseVersionRepository }); } + private static List BuildReleaseSeriesItemViewModels( + Publication publication, + List publishedReleases) + { + var publishedReleasesById = publishedReleases.ToDictionary(r => r.Id); + return publication.ReleaseSeries + // Only include release series items for legacy links and published releases + .Where(rsi => rsi.IsLegacyLink || publishedReleasesById.ContainsKey(rsi.ReleaseId!.Value)) + .Select(rsi => + { + if (rsi.IsLegacyLink) + { + return new ReleaseSeriesItemViewModel + { + Description = rsi.LegacyLinkDescription!, + LegacyLinkUrl = rsi.LegacyLinkUrl + }; + } + + var release = publishedReleasesById[rsi.ReleaseId!.Value]; + + return new ReleaseSeriesItemViewModel + { + Description = release.Title, + ReleaseId = release.Id, + ReleaseSlug = release.Slug + }; + }) + .ToList(); + } + private IQueryable HydrateReleaseQuery(IQueryable queryable) { // Using `AsSplitQuery` as the generated SQL without it is incredibly diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index b04002a6bbd..f41641b645a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -552,6 +552,7 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient + /// Retrieves the published releases that are associated with a publication. + /// + /// The unique identifier of the publication. + /// A to observe while waiting for the task to complete. + /// A collection of the published releases associated with the publication. + Task> ListPublishedReleases( + Guid publicationId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves the id's of all published releases that are associated with a publication. + /// + /// The unique identifier of the publication. + /// A to observe while waiting for the task to complete. + /// A collection of the id's of all published releases associated with the publication. + Task> ListPublishedReleaseIds( + Guid publicationId, + CancellationToken cancellationToken = default); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs new file mode 100644 index 00000000000..fc9bc10a6b1 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs @@ -0,0 +1,40 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; + +public class ReleaseRepository(ContentDbContext context) : IReleaseRepository +{ + public Task> ListPublishedReleases( + Guid publicationId, + CancellationToken cancellationToken = default) + { + return QueryPublishedReleases(publicationId) + .ToListAsync(cancellationToken); + } + + public Task> ListPublishedReleaseIds( + Guid publicationId, + CancellationToken cancellationToken = default) + { + return QueryPublishedReleases(publicationId) + .Select(r => r.Id) + .ToListAsync(cancellationToken); + } + + private IQueryable QueryPublishedReleases(Guid publicationId) + { + // For simplicity, we only query releases that have ANY version that has been published. + // In future this may need to change if release versions can be recalled/unpublished. + return context.Releases + .Where(r => r.PublicationId == publicationId) + .Where(r => r.Versions.Any(rv => rv.Published != null)); + } +} From da82791b157ba444bce12c384bb7c927de713833 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Fri, 13 Dec 2024 18:09:06 +0000 Subject: [PATCH 19/40] EES-5656 Change PublicationService.Get to not use method ListLatestPublishedReleaseVersions --- .../Startup.cs | 1 + .../Interfaces/IReleaseRepository.cs | 4 +- .../Repository/ReleaseRepository.cs | 26 +++-- .../Cache/PublicationCacheServiceTests.cs | 8 +- .../PublicationServiceTests.cs | 107 ++++++++++-------- .../PublicationService.cs | 104 ++++++++--------- .../PublicationCacheViewModel.cs | 2 +- .../PublicationViewModel.cs | 4 +- .../ReleaseTitleViewModel.cs | 10 ++ .../ReleaseVersionTitleViewModel.cs | 10 -- .../PublisherHostBuilderExtensions.cs | 1 + 11 files changed, 153 insertions(+), 124 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseTitleViewModel.cs delete mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseVersionTitleViewModel.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs index f926c6e179e..b75ee88371f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs @@ -162,6 +162,7 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs index ab46e1d668e..9a7bbbef143 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs @@ -9,11 +9,11 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.In public interface IReleaseRepository { /// - /// Retrieves the published releases that are associated with a publication. + /// Retrieves the published releases in release series order that are associated with a publication. /// /// The unique identifier of the publication. /// A to observe while waiting for the task to complete. - /// A collection of the published releases associated with the publication. + /// A collection of the published releases in release series order associated with the publication. Task> ListPublishedReleases( Guid publicationId, CancellationToken cancellationToken = default); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs index fc9bc10a6b1..ce4e1aa401e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs @@ -5,26 +5,38 @@ using System.Threading; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Microsoft.EntityFrameworkCore; namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; -public class ReleaseRepository(ContentDbContext context) : IReleaseRepository +public class ReleaseRepository(ContentDbContext contentDbContext) : IReleaseRepository { - public Task> ListPublishedReleases( + public async Task> ListPublishedReleases( Guid publicationId, CancellationToken cancellationToken = default) { - return QueryPublishedReleases(publicationId) - .ToListAsync(cancellationToken); + var publication = await contentDbContext.Publications + .SingleAsync(p => p.Id == publicationId, cancellationToken: cancellationToken); + + var publicationReleaseSeriesReleaseIds = publication.ReleaseSeries.ReleaseIds(); + + var releaseIdIndexMap = publicationReleaseSeriesReleaseIds + .Select((releaseId, index) => (releaseId, index)) + .ToDictionary(tuple => tuple.releaseId, tuple => tuple.index); + + return (await QueryPublishedReleases(publicationId) + .ToListAsync(cancellationToken)) + .OrderBy(r => releaseIdIndexMap[r.Id]) + .ToList(); } - public Task> ListPublishedReleaseIds( + public async Task> ListPublishedReleaseIds( Guid publicationId, CancellationToken cancellationToken = default) { - return QueryPublishedReleases(publicationId) + return await QueryPublishedReleases(publicationId) .Select(r => r.Id) .ToListAsync(cancellationToken); } @@ -33,7 +45,7 @@ private IQueryable QueryPublishedReleases(Guid publicationId) { // For simplicity, we only query releases that have ANY version that has been published. // In future this may need to change if release versions can be recalled/unpublished. - return context.Releases + return contentDbContext.Releases .Where(r => r.PublicationId == publicationId) .Where(r => r.Versions.Any(rv => rv.Published != null)); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/Cache/PublicationCacheServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/Cache/PublicationCacheServiceTests.cs index 55bd9df9411..ccf19513798 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/Cache/PublicationCacheServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/Cache/PublicationCacheServiceTests.cs @@ -43,15 +43,15 @@ public class PublicationCacheServiceTests : CacheServiceTestFixture Url = "" }, LatestReleaseId = Guid.NewGuid(), - Releases = new List - { - new() + Releases = + [ + new ReleaseTitleViewModel { Id = Guid.NewGuid(), Slug = "", Title = "" } - }, + ], ReleaseSeries = [ new ReleaseSeriesItemViewModel diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/PublicationServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/PublicationServiceTests.cs index 6821c31bde4..862a979d2ed 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/PublicationServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/PublicationServiceTests.cs @@ -122,32 +122,39 @@ public class GetTests : PublicationServiceTests Url = "https://external.methodology.com", }; - private readonly List _legacyLinks = new() - { - new ReleaseSeriesItem - { - Id = Guid.NewGuid(), - LegacyLinkDescription = "Legacy release description", - LegacyLinkUrl = "https://legacy.release.com", - }, - }; - [Fact] public async Task Success() { + ReleaseSeriesItem legacyLink = _dataFixture.DefaultLegacyReleaseSeriesItem(); + Publication publication = _dataFixture .DefaultPublication() - .WithReleases(ListOf( + .WithReleases([ _dataFixture - .DefaultRelease(publishedVersions: 1, year: 2020), + .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022), _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + .DefaultRelease(publishedVersions: 1, year: 2020), _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022))) + .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021) + ]) .WithContact(_contact) .WithExternalMethodology(_externalMethodology) - .WithLegacyLinks(_legacyLinks) - .WithTheme(_dataFixture.DefaultTheme()); + .WithLegacyLinks([legacyLink]) + .WithTheme(_dataFixture.DefaultTheme()) + .FinishWith(p => + { + // Adjust the generated LatestPublishedReleaseVersion to make 2020 the latest published release + var release2020Version0 = p.Releases.Single(r => r.Year == 2020).Versions[0]; + p.LatestPublishedReleaseVersion = release2020Version0; + p.LatestPublishedReleaseVersionId = release2020Version0.Id; + + // Apply a different release series order rather than using the default + p.ReleaseSeries = + [.. GenerateReleaseSeries(p.Releases, 2021, 2020, 2022), legacyLink]; + }); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + var release2022 = publication.Releases.Single(r => r.Year == 2022); var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) @@ -158,11 +165,6 @@ public async Task Success() await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - var expectedReleaseVersion1 = publication.ReleaseVersions - .Single(rv => rv is { Year: 2022, Version: 1 }); - var expectedReleaseVersion2 = publication.ReleaseVersions - .Single(rv => rv is { Year: 2020, Version: 0 }); - var service = SetupPublicationService(contentDbContext); var result = await service.Get(publication.Slug); @@ -175,41 +177,41 @@ public async Task Success() Assert.False(publicationViewModel.IsSuperseded); Assert.Equal(2, publicationViewModel.Releases.Count); - Assert.Equal(expectedReleaseVersion1.Id, publicationViewModel.LatestReleaseId); - Assert.Equal(expectedReleaseVersion1.Id, publicationViewModel.Releases[0].Id); - Assert.Equal(expectedReleaseVersion1.Slug, publicationViewModel.Releases[0].Slug); - Assert.Equal(expectedReleaseVersion1.Title, publicationViewModel.Releases[0].Title); + Assert.Equal(release2020.Versions[0].Id, publicationViewModel.LatestReleaseId); + + Assert.Equal(release2020.Id, publicationViewModel.Releases[0].Id); + Assert.Equal(release2020.Slug, publicationViewModel.Releases[0].Slug); + Assert.Equal(release2020.Title, publicationViewModel.Releases[0].Title); - Assert.Equal(expectedReleaseVersion2.Id, publicationViewModel.Releases[1].Id); - Assert.Equal(expectedReleaseVersion2.Slug, publicationViewModel.Releases[1].Slug); - Assert.Equal(expectedReleaseVersion2.Title, publicationViewModel.Releases[1].Title); + Assert.Equal(release2022.Id, publicationViewModel.Releases[1].Id); + Assert.Equal(release2022.Slug, publicationViewModel.Releases[1].Slug); + Assert.Equal(release2022.Title, publicationViewModel.Releases[1].Title); - Assert.Equal(3, publicationViewModel.ReleaseSeries.Count); + var releaseSeries = publicationViewModel.ReleaseSeries; - var releaseSeriesItem1 = publicationViewModel.ReleaseSeries[0]; - Assert.False(releaseSeriesItem1.IsLegacyLink); - Assert.Equal(expectedReleaseVersion1.ReleaseId, releaseSeriesItem1.ReleaseId); - Assert.Equal(expectedReleaseVersion1.Title, releaseSeriesItem1.Description); - Assert.Equal(expectedReleaseVersion1.Slug, releaseSeriesItem1.ReleaseSlug); - Assert.Null(releaseSeriesItem1.LegacyLinkUrl); + Assert.Equal(3, releaseSeries.Count); + + Assert.False(releaseSeries[0].IsLegacyLink); + Assert.Equal(release2020.Id, releaseSeries[0].ReleaseId); + Assert.Equal(release2020.Title, releaseSeries[0].Description); + Assert.Equal(release2020.Slug, releaseSeries[0].ReleaseSlug); + Assert.Null(releaseSeries[0].LegacyLinkUrl); // NOTE: 2021 release does exist in the database's publication.ReleaseSeries, but is filtered out // because it's unpublished - var releaseSeriesItem2 = publicationViewModel.ReleaseSeries[1]; - Assert.False(releaseSeriesItem2.IsLegacyLink); - Assert.Equal(expectedReleaseVersion2.ReleaseId, releaseSeriesItem2.ReleaseId); - Assert.Equal(expectedReleaseVersion2.Title, releaseSeriesItem2.Description); - Assert.Equal(expectedReleaseVersion2.Slug, releaseSeriesItem2.ReleaseSlug); - Assert.Null(releaseSeriesItem2.LegacyLinkUrl); + Assert.False(releaseSeries[1].IsLegacyLink); + Assert.Equal(release2022.Id, releaseSeries[1].ReleaseId); + Assert.Equal(release2022.Title, releaseSeries[1].Description); + Assert.Equal(release2022.Slug, releaseSeries[1].ReleaseSlug); + Assert.Null(releaseSeries[1].LegacyLinkUrl); - var releaseSeriesItem3 = publicationViewModel.ReleaseSeries[2]; - Assert.True(releaseSeriesItem3.IsLegacyLink); - Assert.Null(releaseSeriesItem3.ReleaseId); - Assert.Equal(_legacyLinks[0].LegacyLinkDescription, releaseSeriesItem3.Description); - Assert.Null(releaseSeriesItem3.ReleaseSlug); - Assert.Equal(_legacyLinks[0].LegacyLinkUrl, releaseSeriesItem3.LegacyLinkUrl); + Assert.True(releaseSeries[2].IsLegacyLink); + Assert.Null(releaseSeries[2].ReleaseId); + Assert.Equal(legacyLink.LegacyLinkDescription, releaseSeries[2].Description); + Assert.Null(releaseSeries[2].ReleaseSlug); + Assert.Equal(legacyLink.LegacyLinkUrl, releaseSeries[2].LegacyLinkUrl); Assert.Equal(publication.Theme.Id, publicationViewModel.Theme.Id); Assert.Equal(publication.Theme.Slug, publicationViewModel.Theme.Slug); @@ -1932,9 +1934,19 @@ public async Task ListSitemapItems() } } + private List GenerateReleaseSeries(IReadOnlyList releases, params int[] years) + { + return years.Select(year => + { + var release = releases.Single(r => r.Year == year); + return _dataFixture.DefaultReleaseSeriesItem().WithReleaseId(release.Id).Generate(); + }).ToList(); + } + private static PublicationService SetupPublicationService( ContentDbContext? contentDbContext = null, IPublicationRepository? publicationRepository = null, + IReleaseRepository? releaseRepository = null, IReleaseVersionRepository? releaseVersionRepository = null) { contentDbContext ??= InMemoryContentDbContext(); @@ -1943,6 +1955,7 @@ private static PublicationService SetupPublicationService( contentDbContext, new PersistenceHelper(contentDbContext), publicationRepository ?? new PublicationRepository(contentDbContext), + releaseRepository ?? new ReleaseRepository(contentDbContext), releaseVersionRepository ?? new ReleaseVersionRepository(contentDbContext) ); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs index 7792a5f2b93..25554190db8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs @@ -27,17 +27,20 @@ public class PublicationService : IPublicationService private readonly ContentDbContext _contentDbContext; private readonly IPersistenceHelper _contentPersistenceHelper; private readonly IPublicationRepository _publicationRepository; + private readonly IReleaseRepository _releaseRepository; private readonly IReleaseVersionRepository _releaseVersionRepository; public PublicationService( ContentDbContext contentDbContext, IPersistenceHelper contentPersistenceHelper, IPublicationRepository publicationRepository, + IReleaseRepository releaseRepository, IReleaseVersionRepository releaseVersionRepository) { _contentDbContext = contentDbContext; _contentPersistenceHelper = contentPersistenceHelper; _publicationRepository = publicationRepository; + _releaseRepository = releaseRepository; _releaseVersionRepository = releaseVersionRepository; } @@ -81,42 +84,9 @@ public async Task> Get(string pu return new Either(new NotFoundResult()); } - var publishedReleaseVersions = await _releaseVersionRepository.ListLatestPublishedReleaseVersions(publication.Id); - + var publishedReleases = await _releaseRepository.ListPublishedReleases(publication.Id); var isSuperseded = await _publicationRepository.IsSuperseded(publication.Id); - - // Only show legacy links and published releases in ReleaseSeries - var filteredReleaseSeries = publication.ReleaseSeries - .Where(rsi => - rsi.IsLegacyLink - || publishedReleaseVersions - .Any(rv => rsi.ReleaseId == rv.ReleaseId) - ).ToList(); - - var releaseSeriesItemViewModels = filteredReleaseSeries - .Select(rsi => - { - if (rsi.IsLegacyLink) - { - return new ReleaseSeriesItemViewModel - { - Description = rsi.LegacyLinkDescription!, - LegacyLinkUrl = rsi.LegacyLinkUrl, - }; - } - - var latestReleaseVersion = publishedReleaseVersions - .Single(rv => rv.ReleaseId == rsi.ReleaseId); - - return new ReleaseSeriesItemViewModel - { - Description = latestReleaseVersion.Title, - ReleaseId = latestReleaseVersion.ReleaseId, - ReleaseSlug = latestReleaseVersion.Slug, - }; - }).ToList(); - - return BuildPublicationViewModel(publication, publishedReleaseVersions, isSuperseded, releaseSeriesItemViewModels); + return BuildPublicationViewModel(publication, publishedReleases, isSuperseded); }); } @@ -222,23 +192,24 @@ public async Task releaseVersions, - bool isSuperseded, - List releaseSeries) + List releases, + bool isSuperseded) { - var theme = new ThemeViewModel( - publication.Theme.Id, - publication.Theme.Slug, - publication.Theme.Title, - publication.Theme.Summary - ); + var theme = publication.Theme; + + var releaseSeriesItemViewModels = BuildReleaseSeriesItemViewModels(publication, releases); return new PublicationCacheViewModel { Id = publication.Id, Title = publication.Title, Slug = publication.Slug, - Theme = theme, + Theme = new ThemeViewModel( + theme.Id, + theme.Slug, + theme.Title, + theme.Summary + ), Contact = new ContactViewModel(publication.Contact), ExternalMethodology = publication.ExternalMethodology != null ? new ExternalMethodologyViewModel(publication.ExternalMethodology) @@ -253,18 +224,49 @@ private static PublicationCacheViewModel BuildPublicationViewModel( Title = publication.SupersededBy.Title } : null, - Releases = releaseVersions - .Select(releaseVersion => new ReleaseVersionTitleViewModel + Releases = releases + .Select(r => new ReleaseTitleViewModel { - Id = releaseVersion.Id, - Slug = releaseVersion.Slug, - Title = releaseVersion.Title, + Id = r.Id, + Slug = r.Slug, + Title = r.Title }) .ToList(), - ReleaseSeries = releaseSeries, + ReleaseSeries = releaseSeriesItemViewModels }; } + private static List BuildReleaseSeriesItemViewModels( + Publication publication, + List releases) + { + var publishedReleasesById = releases.ToDictionary(r => r.Id); + return publication.ReleaseSeries + // Only include release series items for legacy links and published releases + .Where(rsi => rsi.IsLegacyLink || publishedReleasesById.ContainsKey(rsi.ReleaseId!.Value)) + .Select(rsi => + { + if (rsi.IsLegacyLink) + { + return new ReleaseSeriesItemViewModel + { + Description = rsi.LegacyLinkDescription!, + LegacyLinkUrl = rsi.LegacyLinkUrl + }; + } + + var release = publishedReleasesById[rsi.ReleaseId!.Value]; + + return new ReleaseSeriesItemViewModel + { + Description = release.Title, + ReleaseId = release.Id, + ReleaseSlug = release.Slug + }; + }) + .ToList(); + } + private async Task BuildPublicationTreeTheme(Theme theme) { var publications = await theme.Publications diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationCacheViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationCacheViewModel.cs index 56e581c8ad8..fd047c0df75 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationCacheViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationCacheViewModel.cs @@ -16,7 +16,7 @@ public record PublicationCacheViewModel public PublicationSupersededByViewModel? SupersededBy { get; init; } = new(); - public List Releases { get; init; } = []; + public List Releases { get; init; } = []; public List ReleaseSeries { get; init; } = []; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationViewModel.cs index 4a83b1455b5..100a88072fd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/PublicationViewModel.cs @@ -16,9 +16,9 @@ public record PublicationViewModel public PublicationSupersededByViewModel? SupersededBy { get; init; } = new(); - public List Releases { get; init; } = new(); + public List Releases { get; init; } = []; - public List ReleaseSeries { get; init; } = new(); + public List ReleaseSeries { get; init; } = []; public ThemeViewModel Theme { get; init; } = null!; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseTitleViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseTitleViewModel.cs new file mode 100644 index 00000000000..862370facf0 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseTitleViewModel.cs @@ -0,0 +1,10 @@ +namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; + +public record ReleaseTitleViewModel +{ + public required Guid Id { get; init; } + + public required string Slug { get; init; } + + public required string Title { get; init; } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseVersionTitleViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseVersionTitleViewModel.cs deleted file mode 100644 index 1a6017b82c8..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/ReleaseVersionTitleViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; - -public record ReleaseVersionTitleViewModel -{ - public Guid Id { get; set; } - - public string Slug { get; set; } = string.Empty; - - public string Title { get; set; } = string.Empty; -} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs index 3fc0b16ebdd..395fe578313 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs @@ -123,6 +123,7 @@ public static IHostBuilder ConfigurePublisherHostBuilder(this IHostBuilder hostB .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() From 0d82a73c57a8c6b443e96b64dcf2f4b587291ea3 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Mon, 16 Dec 2024 09:33:05 +0000 Subject: [PATCH 20/40] EES-5656 Consolidate methods ListLatestReleaseVersionIds/ListLatestPublishedReleaseVersionIds and ListLatestReleaseVersions/ListLatestPublishedReleaseVersions with new publishedOnly flag. --- .../ReleaseVersionRepositoryTests.cs | 540 ++++++++++-------- .../Interfaces/IReleaseVersionRepository.cs | 24 +- .../Repository/ReleaseVersionRepository.cs | 27 +- .../ReleaseServiceTests.cs | 2 +- .../PublicationService.cs | 3 +- .../ReleaseService.cs | 2 +- 6 files changed, 327 insertions(+), 271 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs index 8cf258824ce..d4446fa2cc7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs @@ -370,247 +370,333 @@ public async Task ReleaseVersionDoesNotExist_ReturnsFalse() } } - public class ListLatestPublishedReleaseVersionIdsTests : ReleaseVersionRepositoryTests + public class ListLatestReleaseVersionsTests : ReleaseVersionRepositoryTests { - [Fact] - public async Task Success() - { - var publications = _dataFixture - .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - var result = await repository.ListLatestPublishedReleaseVersionIds(publications[0].Id); - - // Expect the result to contain the highest published version of each release for the specified publication - AssertIdsAreEqualIgnoringOrder( - [ - publications[0].ReleaseVersions[2].Id, - publications[0].ReleaseVersions[5].Id - ], - result); - } - - [Fact] - public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() - { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has an unpublished release version - // Index 1 has a published release version - .ForIndex(0, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1))) - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestPublishedReleaseVersionIds(publications[0].Id)); - } - - [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + public class AnyPublishedStateTests : ListLatestReleaseVersionIdsTests { - var publications = _dataFixture - .DefaultPublication() - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestPublishedReleaseVersionIds(publications[0].Id)); + [Fact] + public async Task Success() + { + var publications = _dataFixture + .DefaultPublication() + .WithReleases(_ => ListOf( + _dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: false); + + // Expect the result to contain the highest version of each release for the specified publication + AssertIdsAreEqualIgnoringOrder( + [ + publications[0].ReleaseVersions[0].Id, + publications[0].ReleaseVersions[3].Id, + publications[0].ReleaseVersions[5].Id + ], + result); + } + + [Fact] + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + { + var publications = _dataFixture + .DefaultPublication() + // Index 0 has no release versions + // Index 1 has a published release version + .ForIndex(1, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: false)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsEmpty() + { + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1)); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersions(publicationId: Guid.NewGuid(), + publishedOnly: false)); + } } - [Fact] - public async Task PublicationDoesNotExist_ReturnsEmpty() + public class PublishedOnlyTests : ListLatestReleaseVersionsTests { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); - - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestPublishedReleaseVersionIds(Guid.NewGuid())); - } - } - - public class ListLatestPublishedReleaseVersionsTests : ReleaseVersionRepositoryTests - { - [Fact] - public async Task Success() - { - var publications = _dataFixture - .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - var result = await repository.ListLatestPublishedReleaseVersions(publications[0].Id); - - // Expect the result to contain the highest published version of each release for the specified publication - AssertIdsAreEqualIgnoringOrder( - [ - publications[0].ReleaseVersions[2].Id, - publications[0].ReleaseVersions[5].Id - ], - result); - } - - [Fact] - public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() - { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has an unpublished release version - // Index 1 has a published release version - .ForIndex(0, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1))) - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestPublishedReleaseVersions(publications[0].Id)); - } - - [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() - { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestPublishedReleaseVersions(publications[0].Id)); - } - - [Fact] - public async Task PublicationDoesNotExist_ReturnsEmpty() - { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); - - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestPublishedReleaseVersions(Guid.NewGuid())); + [Fact] + public async Task Success() + { + var publications = _dataFixture + .DefaultPublication() + .WithReleases(_ => ListOf( + _dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: true); + + // Expect the result to contain the highest published version of each release for the specified publication + AssertIdsAreEqualIgnoringOrder( + [ + publications[0].ReleaseVersions[2].Id, + publications[0].ReleaseVersions[5].Id + ], + result); + } + + [Fact] + public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() + { + var publications = _dataFixture + .DefaultPublication() + // Index 0 has an unpublished release version + // Index 1 has a published release version + .ForIndex(0, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true) + .Generate(1))) + .ForIndex(1, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: true)); + } + + [Fact] + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + { + var publications = _dataFixture + .DefaultPublication() + // Index 0 has no release versions + // Index 1 has a published release version + .ForIndex(1, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: true)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsEmpty() + { + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1)); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty( + await repository.ListLatestReleaseVersions(publicationId: Guid.NewGuid(), publishedOnly: true)); + } } } public class ListLatestReleaseVersionIdsTests : ReleaseVersionRepositoryTests { - [Fact] - public async Task Success() + public class AnyPublishedStateTests : ListLatestReleaseVersionIdsTests { - var publications = _dataFixture - .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - var result = await repository.ListLatestReleaseVersionIds(publications[0].Id); - - // Expect the result to contain the highest version of each release for the specified publication - AssertIdsAreEqualIgnoringOrder( - [ - publications[0].ReleaseVersions[0].Id, - publications[0].ReleaseVersions[3].Id, - publications[0].ReleaseVersions[5].Id - ], - result); + [Fact] + public async Task Success() + { + var publications = _dataFixture + .DefaultPublication() + .WithReleases(_ => ListOf( + _dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: false); + + // Expect the result to contain the highest version of each release for the specified publication + AssertIdsAreEqualIgnoringOrder( + [ + publications[0].ReleaseVersions[0].Id, + publications[0].ReleaseVersions[3].Id, + publications[0].ReleaseVersions[5].Id + ], + result); + } + + [Fact] + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + { + var publications = _dataFixture + .DefaultPublication() + // Index 0 has no release versions + // Index 1 has a published release version + .ForIndex(1, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: false)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsEmpty() + { + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1)); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersionIds(publicationId: Guid.NewGuid(), + publishedOnly: false)); + } } - [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + public class PublishedOnlyTrueTests : ListLatestReleaseVersionIdsTests { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id)); - } - - [Fact] - public async Task PublicationDoesNotExist_ReturnsEmpty() - { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); - - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Empty(await repository.ListLatestReleaseVersionIds(Guid.NewGuid())); + [Fact] + public async Task Success() + { + var publications = _dataFixture + .DefaultPublication() + .WithReleases(_ => ListOf( + _dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2, draftVersion: true), + _dataFixture + .DefaultRelease(publishedVersions: 2))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: true); + + // Expect the result to contain the highest published version of each release for the specified publication + AssertIdsAreEqualIgnoringOrder( + [ + publications[0].ReleaseVersions[2].Id, + publications[0].ReleaseVersions[5].Id + ], + result); + } + + [Fact] + public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() + { + var publications = _dataFixture + .DefaultPublication() + // Index 0 has an unpublished release version + // Index 1 has a published release version + .ForIndex(0, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 0, draftVersion: true) + .Generate(1))) + .ForIndex(1, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: true)); + } + + [Fact] + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + { + var publications = _dataFixture + .DefaultPublication() + .ForIndex(1, + p => p.SetReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1))) + .GenerateList(2); + + var contextId = await AddTestData(publications); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: true)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsEmpty() + { + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases(_dataFixture + .DefaultRelease(publishedVersions: 1) + .Generate(1)); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + Assert.Empty( + await repository.ListLatestReleaseVersionIds(publicationId: Guid.NewGuid(), publishedOnly: true)); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs index 7333c2e4be9..8cc5dd3d21e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs @@ -64,43 +64,27 @@ Task IsLatestReleaseVersion( Guid releaseVersionId, CancellationToken cancellationToken = default); - /// - /// Retrieves the latest published release version id's associated with a publication. - /// - /// The unique identifier of the publication. - /// A to observe while waiting for the task to complete. - /// A collection of the latest published version id's of all releases associated with the publication. - Task> ListLatestPublishedReleaseVersionIds( - Guid publicationId, - CancellationToken cancellationToken = default); - - /// - /// Retrieves the latest published versions of all releases associated with a publication in reverse chronological order. - /// - /// The unique identifier of the publication. - /// A to observe while waiting for the task to complete. - /// A collection of the latest published versions of all releases associated with the publication. - Task> ListLatestPublishedReleaseVersions( - Guid publicationId, - CancellationToken cancellationToken = default); - /// /// Retrieves the latest version id's of all releases associated with a publication. /// /// The unique identifier of the publication. + /// Flag to only include published release version id's. /// A to observe while waiting for the task to complete. /// A collection of the latest version id's of all releases associated with the publication. Task> ListLatestReleaseVersionIds( Guid publicationId, + bool publishedOnly = false, CancellationToken cancellationToken = default); /// /// Retrieves the latest versions of all releases associated with a given publication in reverse chronological order. /// /// The unique identifier of the publication. + /// Flag to only include published release versions. /// A to observe while waiting for the task to complete. /// A collection of the latest version id's of all releases associated with the publication. Task> ListLatestReleaseVersions( Guid publicationId, + bool publishedOnly = false, CancellationToken cancellationToken = default); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs index 7d7071ae153..6a167df5e37 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs @@ -103,39 +103,24 @@ public async Task IsLatestReleaseVersion( cancellationToken: cancellationToken); } - public async Task> ListLatestPublishedReleaseVersions( - Guid publicationId, - CancellationToken cancellationToken = default) - { - return (await _contentDbContext.ReleaseVersions.LatestReleaseVersions(publicationId, publishedOnly: true) - .ToListAsync(cancellationToken: cancellationToken)) - .OrderByReverseChronologicalOrder() - .ToList(); - } - - public async Task> ListLatestPublishedReleaseVersionIds( - Guid publicationId, - CancellationToken cancellationToken = default) - { - return await _contentDbContext.ReleaseVersions.LatestReleaseVersions(publicationId, publishedOnly: true) - .Select(rv => rv.Id) - .ToListAsync(cancellationToken: cancellationToken); - } - public async Task> ListLatestReleaseVersionIds( Guid publicationId, + bool publishedOnly = false, CancellationToken cancellationToken = default) { - return await _contentDbContext.ReleaseVersions.LatestReleaseVersions(publicationId) + return await _contentDbContext.ReleaseVersions + .LatestReleaseVersions(publicationId, publishedOnly: publishedOnly) .Select(rv => rv.Id) .ToListAsync(cancellationToken: cancellationToken); } public async Task> ListLatestReleaseVersions( Guid publicationId, + bool publishedOnly = false, CancellationToken cancellationToken = default) { - return (await _contentDbContext.ReleaseVersions.LatestReleaseVersions(publicationId) + return (await _contentDbContext.ReleaseVersions + .LatestReleaseVersions(publicationId, publishedOnly: publishedOnly) .ToListAsync(cancellationToken: cancellationToken)) .OrderByReverseChronologicalOrder() .ToList(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs index d54156bf6b2..2ddc439be26 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs @@ -745,7 +745,7 @@ public async Task List() Assert.Equal(2, releases.Count); - // Ordered from most newest to oldest + // Ordered from newest to oldest Assert.Equal(release2Version1.Id, releases[0].Id); Assert.Equal(release2Version1.Title, releases[0].Title); Assert.Equal(release2Version1.Slug, releases[0].Slug); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs index 25554190db8..8548db4a29c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs @@ -294,7 +294,8 @@ private async Task BuildPublicationTreePubl var latestReleaseHasData = latestPublishedReleaseVersionId.HasValue && await HasAnyDataFiles(latestPublishedReleaseVersionId.Value); - var publishedReleaseVersionIds = await _releaseVersionRepository.ListLatestPublishedReleaseVersionIds(publication.Id); + var publishedReleaseVersionIds = + await _releaseVersionRepository.ListLatestReleaseVersionIds(publication.Id, publishedOnly: true); var anyLiveReleaseHasData = await publishedReleaseVersionIds .ToAsyncEnumerable() .AnyAwaitAsync(async id => await HasAnyDataFiles(id)); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs index 97d0b74d267..076fa2592b6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs @@ -115,7 +115,7 @@ public async Task>> List(stri .OnSuccess(async publication => { var publishedReleaseVersions = - await _releaseVersionRepository.ListLatestPublishedReleaseVersions(publication.Id); + await _releaseVersionRepository.ListLatestReleaseVersions(publication.Id, publishedOnly: true); return publishedReleaseVersions .Select(releaseVersion => new ReleaseSummaryViewModel(releaseVersion, latestPublishedRelease: releaseVersion.Id == publication.LatestPublishedReleaseVersionId)) From cbbeb43a47b9b76ad0c0f2b1d7f8c063c0c4c54f Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Mon, 16 Dec 2024 11:14:51 +0000 Subject: [PATCH 21/40] EES-5656 Change ReleaseService to not use method GetLatestPublishedReleaseVersion(Guid publicationId) --- .../ReleaseServiceTests.cs | 2 - .../ReleaseService.cs | 47 +++++++++---------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs index 2ddc439be26..bc7f6e58e37 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs @@ -7,7 +7,6 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; -using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; @@ -851,7 +850,6 @@ private static ReleaseService SetupReleaseService( { return new( contentDbContext, - new PersistenceHelper(contentDbContext), releaseFileRepository ?? new ReleaseFileRepository(contentDbContext), releaseVersionRepository ?? new ReleaseVersionRepository(contentDbContext), userService ?? AlwaysTrueUserService().Object, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs index 076fa2592b6..68bc9328b6a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs @@ -8,7 +8,6 @@ using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; -using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; @@ -25,7 +24,6 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.Services public class ReleaseService : IReleaseService { private readonly ContentDbContext _contentDbContext; - private readonly IPersistenceHelper _persistenceHelper; private readonly IReleaseFileRepository _releaseFileRepository; private readonly IReleaseVersionRepository _releaseVersionRepository; private readonly IUserService _userService; @@ -36,42 +34,41 @@ public class ReleaseService : IReleaseService public ReleaseService( ContentDbContext contentDbContext, - IPersistenceHelper persistenceHelper, IReleaseFileRepository releaseFileRepository, IReleaseVersionRepository releaseVersionRepository, IUserService userService, IMapper mapper) { _contentDbContext = contentDbContext; - _persistenceHelper = persistenceHelper; _releaseFileRepository = releaseFileRepository; _releaseVersionRepository = releaseVersionRepository; _userService = userService; _mapper = mapper; } - public async Task> GetRelease(string publicationSlug, + public async Task> GetRelease( + string publicationSlug, string? releaseSlug = null) { - return await _persistenceHelper.CheckEntityExists(q => - q.Where(p => p.Slug == publicationSlug)) + return await _contentDbContext.Publications + .SingleOrNotFoundAsync(p => p.Slug == publicationSlug) .OnSuccess(async publication => { - // If no release is requested get the latest published release version - if (releaseSlug == null) - { - return await _releaseVersionRepository.GetLatestPublishedReleaseVersion(publication.Id) - .OrNotFound(); - } - - // Otherwise get the latest published version of the requested release - return await _releaseVersionRepository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug) - .OrNotFound(); - }) - .OnSuccess(releaseVersion => GetRelease(releaseVersion.Id)); + // If no release is requested use the publication's latest published release version, + // otherwise use the latest published version of the requested release + var latestReleaseVersionId = releaseSlug == null + ? publication.LatestPublishedReleaseVersionId + : (await _releaseVersionRepository.GetLatestPublishedReleaseVersion(publication.Id, + releaseSlug))?.Id; + + return latestReleaseVersionId.HasValue + ? await GetRelease(latestReleaseVersionId.Value) + : new NotFoundResult(); + }); } - public async Task> GetRelease(Guid releaseVersionId, + public async Task> GetRelease( + Guid releaseVersionId, DateTime? expectedPublishDate = null) { // Note this method is allowed to return a view model for an unpublished release version so that Publisher @@ -107,10 +104,8 @@ public async Task> GetRelease(Guid r public async Task>> List(string publicationSlug) { - return await _persistenceHelper.CheckEntityExists( - q => q - .Where(p => p.Slug == publicationSlug) - ) + return await _contentDbContext.Publications + .SingleOrNotFoundAsync(p => p.Slug == publicationSlug) .OnSuccess(_userService.CheckCanViewPublication) .OnSuccess(async publication => { @@ -140,9 +135,9 @@ private static void FilterContentBlock(IContentBlockViewModel block) private async Task> GetDownloadFiles(ReleaseVersion releaseVersion) { var files = await _releaseFileRepository.GetByFileType( - releaseVersion.Id, + releaseVersion.Id, types: [FileType.Ancillary, FileType.Data]); - + return files .Select(rf => rf.ToPublicFileInfo()) .OrderBy(file => file.Name) From 65a1bb8fab066accd152b7816258393cf5797595 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Mon, 16 Dec 2024 15:18:36 +0000 Subject: [PATCH 22/40] EES-5656 Change PermalinkService to not use method GetLatestPublishedReleaseVersion(Guid publicationId) --- .../Services/PermalinkServiceTests.cs | 479 ++++++------------ ...tePermalink_WithReleaseVersionId_csv.snap} | 0 ...ePermalink_WithReleaseVersionId_json.snap} | 0 ...ermalink_WithoutReleaseVersionId_csv.snap} | 0 ...rmalink_WithoutReleaseVersionId_json.snap} | 0 .../Services/PermalinkService.cs | 28 +- 6 files changed, 175 insertions(+), 332 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/{PermalinkServiceTests.CreatePermalink_WithReleaseId_csv.snap => PermalinkServiceTests.CreatePermalink_WithReleaseVersionId_csv.snap} (100%) rename src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/{PermalinkServiceTests.CreatePermalink_WithReleaseId_json.snap => PermalinkServiceTests.CreatePermalink_WithReleaseVersionId_json.snap} (100%) rename src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/{PermalinkServiceTests.CreatePermalink_WithoutReleaseId_csv.snap => PermalinkServiceTests.CreatePermalink_WithoutReleaseVersionId_csv.snap} (100%) rename src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/{PermalinkServiceTests.CreatePermalink_WithoutReleaseId_json.snap => PermalinkServiceTests.CreatePermalink_WithoutReleaseVersionId_json.snap} (100%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/PermalinkServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/PermalinkServiceTests.cs index b3ca52e070e..e55fa71c113 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/PermalinkServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/PermalinkServiceTests.cs @@ -1,4 +1,11 @@ #nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; @@ -12,11 +19,13 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Data.Api.Models; using GovUk.Education.ExploreEducationStatistics.Data.Api.Requests; using GovUk.Education.ExploreEducationStatistics.Data.Api.Services; using GovUk.Education.ExploreEducationStatistics.Data.Api.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Data.Api.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Data.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Data.Model.Utils; @@ -29,18 +38,12 @@ using Newtonsoft.Json.Linq; using Snapshooter; using Snapshooter.Xunit; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils.ContentDbUtils; using File = GovUk.Education.ExploreEducationStatistics.Content.Model.File; +using ReleaseVersion = GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion; namespace GovUk.Education.ExploreEducationStatistics.Data.Api.Tests.Services { @@ -55,8 +58,6 @@ public class PermalinkServiceTests } }; - private readonly Guid _publicationId = Guid.NewGuid(); - private readonly PermalinkTableViewModel _frontendTableResponse = new() { Caption = "Admission Numbers for 'Sample publication' in North East between 2022 and 2023", @@ -70,6 +71,10 @@ public class PermalinkServiceTests [Fact] public async Task CreatePermalink_LatestPublishedReleaseForSubjectNotFound() { + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 0, draftVersion: true)]) + .WithTheme(_fixture.DefaultTheme()); + var request = new PermalinkCreateRequest { Query = @@ -78,57 +83,64 @@ public async Task CreatePermalink_LatestPublishedReleaseForSubjectNotFound() } }; - var releaseVersionRepository = new Mock(MockBehavior.Strict); var subjectRepository = new Mock(MockBehavior.Strict); - releaseVersionRepository - .Setup(s => s.GetLatestPublishedReleaseVersion(_publicationId, default)) - .ReturnsAsync((ReleaseVersion?)null); - subjectRepository .Setup(s => s.FindPublicationIdForSubject(request.Query.SubjectId, default)) - .ReturnsAsync(_publicationId); + .ReturnsAsync(publication.Id); - var service = BuildService(releaseVersionRepository: releaseVersionRepository.Object, - subjectRepository: subjectRepository.Object); + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var service = BuildService( + contentDbContext: contentDbContext, + subjectRepository: subjectRepository.Object + ); - var result = await service.CreatePermalink(request); + var result = await service.CreatePermalink(request); - MockUtils.VerifyAllMocks( - releaseVersionRepository, - subjectRepository); + MockUtils.VerifyAllMocks(subjectRepository); - result.AssertNotFound(); + result.AssertNotFound(); + } } [Fact] - public async Task CreatePermalink_WithoutReleaseId() + public async Task CreatePermalink_WithoutReleaseVersionId() { - var subject = _fixture + Subject subject = _fixture .DefaultSubject() .WithFilters(_fixture.DefaultFilter() - .ForIndex(0, s => - s.SetGroupCsvColumn("filter_0_grouping") - .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) - .ForInstance(s => s.Set( - fg => fg.Label, - (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) - .Generate(2))) - .ForIndex(1, s => - s.SetGroupCsvColumn("filter_1_grouping") - .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) - .ForInstance(s => s.Set( - fg => fg.Label, - (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) - .Generate(2))) - .ForIndex(2, s => - s.SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 2) - .Generate(1))) + .ForIndex(0, + s => + s.SetGroupCsvColumn("filter_0_grouping") + .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) + .ForInstance(s => s.Set( + fg => fg.Label, + (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) + .Generate(2))) + .ForIndex(1, + s => + s.SetGroupCsvColumn("filter_1_grouping") + .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) + .ForInstance(s => s.Set( + fg => fg.Label, + (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) + .Generate(2))) + .ForIndex(2, + s => + s.SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 2) + .Generate(1))) .GenerateList()) .WithIndicatorGroups(_fixture .DefaultIndicatorGroup(indicatorCount: 1) - .Generate(3)) - .Generate(); + .Generate(3)); var indicators = subject .IndicatorGroups @@ -201,17 +213,11 @@ public async Task CreatePermalink_WithoutReleaseId() Filters = FiltersMetaViewModelBuilder.BuildFilters(subject.Filters), Indicators = IndicatorsMetaViewModelBuilder.BuildIndicators(indicators), Footnotes = footnoteViewModels, - TimePeriodRange = new List - { - new(2022, AcademicYear) - { - Label = "2022/23" - }, - new(2023, AcademicYear) - { - Label = "2023/24" - } - } + TimePeriodRange = + [ + new TimePeriodMetaViewModel(2022, AcademicYear) { Label = "2022/23" }, + new TimePeriodMetaViewModel(2023, AcademicYear) { Label = "2023/24" } + ] }, Results = observations .Select(o => @@ -219,20 +225,13 @@ public async Task CreatePermalink_WithoutReleaseId() .ToList() }; - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - }; + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]) + .WithTheme(_fixture.DefaultTheme()); - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = releaseVersion - }; + var releaseVersion = publication.Releases.Single().Versions.Single(); + + var releaseDataFile = ReleaseDataFile(releaseVersion, subject.Id); var csvMeta = new PermalinkCsvMetaViewModel { @@ -242,8 +241,8 @@ public async Task CreatePermalink_WithoutReleaseId() .Select(i => new IndicatorCsvMetaViewModel(i)) .ToDictionary(i => i.Name), Locations = locations.ToDictionary(l => l.Id, l => l.GetCsvValues()), - Headers = new List - { + Headers = + [ "time_period", "time_identifier", "geographic_level", @@ -262,7 +261,7 @@ public async Task CreatePermalink_WithoutReleaseId() indicators[0].Name, indicators[1].Name, indicators[2].Name - } + ] }; var request = new PermalinkCreateRequest @@ -349,17 +348,11 @@ public async Task CreatePermalink_WithoutReleaseId() It.IsAny())) .ReturnsAsync(csvMeta); - var releaseVersionRepository = new Mock(MockBehavior.Strict); - - releaseVersionRepository - .Setup(s => s.GetLatestPublishedReleaseVersion(_publicationId, default)) - .ReturnsAsync(releaseVersion); - var subjectRepository = new Mock(MockBehavior.Strict); subjectRepository .Setup(s => s.FindPublicationIdForSubject(subject.Id, default)) - .ReturnsAsync(_publicationId); + .ReturnsAsync(publication.Id); var tableBuilderService = new Mock(MockBehavior.Strict); @@ -375,8 +368,7 @@ public async Task CreatePermalink_WithoutReleaseId() await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.Add(releaseVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(releaseVersion, subject.Id)); + contentDbContext.ReleaseFiles.Add(releaseDataFile); await contentDbContext.SaveChangesAsync(); } @@ -388,7 +380,6 @@ public async Task CreatePermalink_WithoutReleaseId() publicBlobStorageService: publicBlobStorageService.Object, frontendService: frontendService.Object, permalinkCsvMetaService: permalinkCsvMetaService.Object, - releaseVersionRepository: releaseVersionRepository.Object, subjectRepository: subjectRepository.Object, tableBuilderService: tableBuilderService.Object); @@ -398,7 +389,6 @@ public async Task CreatePermalink_WithoutReleaseId() publicBlobStorageService, frontendService, permalinkCsvMetaService, - releaseVersionRepository, subjectRepository, tableBuilderService); @@ -439,33 +429,35 @@ public async Task CreatePermalink_WithoutReleaseId() } [Fact] - public async Task CreatePermalink_WithReleaseId() + public async Task CreatePermalink_WithReleaseVersionId() { - var subject = _fixture + Subject subject = _fixture .DefaultSubject() .WithFilters(_fixture.DefaultFilter() - .ForIndex(0, s => - s.SetGroupCsvColumn("filter_0_grouping") - .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) - .ForInstance(s => s.Set( - fg => fg.Label, - (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) - .Generate(2))) - .ForIndex(1, s => - s.SetGroupCsvColumn("filter_1_grouping") - .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) - .ForInstance(s => s.Set( - fg => fg.Label, - (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) - .Generate(2))) - .ForIndex(2, s => - s.SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 2) - .Generate(1))) + .ForIndex(0, + s => + s.SetGroupCsvColumn("filter_0_grouping") + .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) + .ForInstance(s => s.Set( + fg => fg.Label, + (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) + .Generate(2))) + .ForIndex(1, + s => + s.SetGroupCsvColumn("filter_1_grouping") + .SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 1) + .ForInstance(s => s.Set( + fg => fg.Label, + (_, _, context) => $"Filter group {context.FixtureTypeIndex}")) + .Generate(2))) + .ForIndex(2, + s => + s.SetFilterGroups(_fixture.DefaultFilterGroup(filterItemCount: 2) + .Generate(1))) .GenerateList()) .WithIndicatorGroups(_fixture .DefaultIndicatorGroup(indicatorCount: 1) - .Generate(3)) - .Generate(); + .Generate(3)); var indicators = subject .IndicatorGroups @@ -537,17 +529,11 @@ public async Task CreatePermalink_WithReleaseId() Filters = FiltersMetaViewModelBuilder.BuildFilters(subject.Filters), Indicators = IndicatorsMetaViewModelBuilder.BuildIndicators(indicators), Footnotes = footnoteViewModels, - TimePeriodRange = new List - { - new(2022, AcademicYear) - { - Label = "2022/23" - }, - new(2023, AcademicYear) - { - Label = "2023/24" - } - } + TimePeriodRange = + [ + new TimePeriodMetaViewModel(2022, AcademicYear) { Label = "2022/23" }, + new TimePeriodMetaViewModel(2023, AcademicYear) { Label = "2023/24" } + ] }, Results = observations .Select(o => @@ -555,20 +541,13 @@ public async Task CreatePermalink_WithReleaseId() .ToList() }; - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - }; + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]) + .WithTheme(_fixture.DefaultTheme()); - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = releaseVersion - }; + var releaseVersion = publication.Releases.Single().Versions.Single(); + + var releaseDataFile = ReleaseDataFile(releaseVersion, subject.Id); var csvMeta = new PermalinkCsvMetaViewModel { @@ -578,8 +557,8 @@ public async Task CreatePermalink_WithReleaseId() .Select(i => new IndicatorCsvMetaViewModel(i)) .ToDictionary(i => i.Name), Locations = locations.ToDictionary(l => l.Id, l => l.GetCsvValues()), - Headers = new List - { + Headers = + [ "time_period", "time_identifier", "geographic_level", @@ -598,7 +577,7 @@ public async Task CreatePermalink_WithReleaseId() indicators[0].Name, indicators[1].Name, indicators[2].Name - } + ] }; var request = new PermalinkCreateRequest @@ -699,8 +678,7 @@ public async Task CreatePermalink_WithReleaseId() await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.Add(releaseVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(releaseVersion, subject.Id)); + contentDbContext.ReleaseFiles.Add(releaseDataFile); await contentDbContext.SaveChangesAsync(); } @@ -761,20 +739,11 @@ public async Task CreatePermalink_WithReleaseId() [Fact] public async Task GetPermalink() { - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - ReleaseName = "2000", - PublicationId = _publicationId, - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - }; + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]) + .WithTheme(_fixture.DefaultTheme()); - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = releaseVersion - }; + var releaseVersion = publication.Releases.Single().Versions.Single(); var permalink = new Permalink { @@ -785,6 +754,8 @@ public async Task GetPermalink() SubjectId = Guid.NewGuid() }; + var releaseDataFile = ReleaseDataFile(releaseVersion, permalink.SubjectId); + var table = _frontendTableResponse with { Footnotes = FootnotesViewModelBuilder.BuildFootnotes(_fixture @@ -797,8 +768,7 @@ public async Task GetPermalink() { contentDbContext.Permalinks.Add(permalink); contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.Add(releaseVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(releaseVersion, permalink.SubjectId)); + contentDbContext.ReleaseFiles.Add(releaseDataFile); await contentDbContext.SaveChangesAsync(); } @@ -873,7 +843,7 @@ public async Task GetPermalink_BlobNotFound() } [Fact] - public async Task GetPermalink_ReleaseWithSubjectNotFound() + public async Task GetPermalink_ReleaseVersionWithSubjectNotFound() { var permalink = new Permalink { @@ -912,47 +882,29 @@ public async Task GetPermalink_ReleaseWithSubjectNotFound() [Fact] public async Task GetPermalink_SubjectIsForMultipleReleaseVersions() { - var previousVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - PreviousVersionId = null - }; - - var latestVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - PreviousVersionId = previousVersion.Id - }; + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 2)]) + .WithTheme(_fixture.DefaultTheme()); - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = latestVersion - }; + var (previousReleaseVersion, latestReleaseVersion) = publication.Releases.Single().Versions.ToTuple2(); var permalink = new Permalink { SubjectId = Guid.NewGuid() }; + ReleaseFile[] releaseDataFiles = + [ + ReleaseDataFile(previousReleaseVersion, permalink.SubjectId), + ReleaseDataFile(latestReleaseVersion, permalink.SubjectId) + ]; + var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Permalinks.Add(permalink); contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.AddRange(previousVersion, latestVersion); - contentDbContext.ReleaseFiles.AddRange( - ReleaseDataFile(previousVersion, permalink.SubjectId), - ReleaseDataFile(latestVersion, permalink.SubjectId) - ); + contentDbContext.ReleaseFiles.AddRange(releaseDataFiles); await contentDbContext.SaveChangesAsync(); } @@ -980,109 +932,37 @@ public async Task GetPermalink_SubjectIsForMultipleReleaseVersions() } [Fact] - public async Task GetPermalink_SubjectIsNotForLatestRelease_OlderByYear() + public async Task GetPermalink_SubjectIsNotForPublicationsLatestRelease() { - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - }; - - var latestReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2001", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - }; - - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = latestReleaseVersion - }; + Publication publication = _fixture + .DefaultPublication() + .WithReleases([ + _fixture + .DefaultRelease(publishedVersions: 1, year: 2020), + _fixture + .DefaultRelease(publishedVersions: 1, year: 2021) + ]) + .WithTheme(_fixture.DefaultTheme()); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + var release2021 = publication.Releases.Single(r => r.Year == 2021); + + // Check the publication's latest published release version in the generated test data setup + Assert.Equal(release2021.Versions[0].Id, publication.LatestPublishedReleaseVersionId); var permalink = new Permalink { SubjectId = Guid.NewGuid() }; - var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - contentDbContext.Permalinks.Add(permalink); - contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.AddRange(releaseVersion, latestReleaseVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(releaseVersion, permalink.SubjectId)); - - await contentDbContext.SaveChangesAsync(); - } - - var publicBlobStorageService = new Mock(MockBehavior.Strict); - - publicBlobStorageService.SetupGetDeserializedJson( - container: BlobContainers.PermalinkSnapshots, - path: $"{permalink.Id}.json.zst", - value: new PermalinkTableViewModel()); - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var service = BuildService( - contentDbContext: contentDbContext, - publicBlobStorageService: publicBlobStorageService.Object); - - var result = (await service.GetPermalink(permalink.Id)).AssertRight(); - - MockUtils.VerifyAllMocks(publicBlobStorageService); - - Assert.Equal(permalink.Id, result.Id); - Assert.Equal(PermalinkStatus.NotForLatestRelease, result.Status); - } - } - - [Fact] - public async Task GetPermalink_SubjectIsNotForLatestRelease_OlderByTimePeriod() - { - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = January, - Published = DateTime.UtcNow, - }; - - var latestReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = February, - Published = DateTime.UtcNow, - }; - - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = latestReleaseVersion - }; - - var permalink = new Permalink - { - SubjectId = Guid.NewGuid() - }; + var releaseDataFile = ReleaseDataFile(release2020.Versions[0], permalink.SubjectId); var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Permalinks.Add(permalink); contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.AddRange(releaseVersion, latestReleaseVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(releaseVersion, permalink.SubjectId)); + contentDbContext.ReleaseFiles.Add(releaseDataFile); await contentDbContext.SaveChangesAsync(); } @@ -1112,45 +992,24 @@ public async Task GetPermalink_SubjectIsNotForLatestRelease_OlderByTimePeriod() [Fact] public async Task GetPermalink_SubjectIsNotForLatestReleaseVersion() { - var previousVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - PreviousVersionId = null - }; + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 2)]); - var latestVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - PreviousVersionId = previousVersion.Id - }; - - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = latestVersion - }; + var previousReleaseVersion = publication.Releases.Single().Versions[0]; var permalink = new Permalink { SubjectId = Guid.NewGuid() }; + var releaseDataFile = ReleaseDataFile(previousReleaseVersion, permalink.SubjectId); + var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Permalinks.Add(permalink); contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.AddRange(previousVersion, latestVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(previousVersion, - permalink.SubjectId)); + contentDbContext.ReleaseFiles.Add(releaseDataFile); await contentDbContext.SaveChangesAsync(); } @@ -1180,37 +1039,27 @@ public async Task GetPermalink_SubjectIsNotForLatestReleaseVersion() [Fact] public async Task GetPermalink_SubjectIsFromSupersededPublication() { - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - PublicationId = _publicationId, - ReleaseName = "2000", - TimePeriodCoverage = AcademicYear, - Published = DateTime.UtcNow, - }; + Publication publication = _fixture.DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]) + .WithSupersededBy(_fixture + .DefaultPublication() + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)])); - var publication = new Publication - { - Id = _publicationId, - LatestPublishedReleaseVersion = releaseVersion, - SupersededBy = new Publication - { - LatestPublishedReleaseVersionId = Guid.NewGuid() - } - }; + var releaseVersion = publication.Releases.Single().Versions.Single(); var permalink = new Permalink { SubjectId = Guid.NewGuid() }; + var releaseDataFile = ReleaseDataFile(releaseVersion, permalink.SubjectId); + var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Permalinks.Add(permalink); - contentDbContext.Publications.Add(publication); - contentDbContext.ReleaseVersions.Add(releaseVersion); - contentDbContext.ReleaseFiles.AddRange(ReleaseDataFile(releaseVersion, permalink.SubjectId)); + contentDbContext.Publications.AddRange(publication); + contentDbContext.ReleaseFiles.Add(releaseDataFile); await contentDbContext.SaveChangesAsync(); } @@ -1337,7 +1186,6 @@ private static PermalinkService BuildService( IPermalinkCsvMetaService? permalinkCsvMetaService = null, IPublicBlobStorageService? publicBlobStorageService = null, IFrontendService? frontendService = null, - IReleaseVersionRepository? releaseVersionRepository = null, ISubjectRepository? subjectRepository = null, IPublicationRepository? publicationRepository = null) { @@ -1350,8 +1198,7 @@ private static PermalinkService BuildService( publicBlobStorageService ?? Mock.Of(MockBehavior.Strict), frontendService ?? Mock.Of(MockBehavior.Strict), subjectRepository ?? Mock.Of(MockBehavior.Strict), - publicationRepository ?? new PublicationRepository(contentDbContext), - releaseVersionRepository ?? Mock.Of(MockBehavior.Strict) + publicationRepository ?? new PublicationRepository(contentDbContext) ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseId_csv.snap b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseVersionId_csv.snap similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseId_csv.snap rename to src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseVersionId_csv.snap diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseId_json.snap b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseVersionId_json.snap similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseId_json.snap rename to src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithReleaseVersionId_json.snap diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseId_csv.snap b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseVersionId_csv.snap similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseId_csv.snap rename to src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseVersionId_csv.snap diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseId_json.snap b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseVersionId_json.snap similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseId_json.snap rename to src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Services/__snapshots__/PermalinkServiceTests.CreatePermalink_WithoutReleaseVersionId_json.snap diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Services/PermalinkService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Services/PermalinkService.cs index 5638c3496e0..e72b68fce1e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Services/PermalinkService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Services/PermalinkService.cs @@ -40,7 +40,6 @@ public class PermalinkService : IPermalinkService private readonly IFrontendService _frontendService; private readonly ISubjectRepository _subjectRepository; private readonly IPublicationRepository _publicationRepository; - private readonly IReleaseVersionRepository _releaseVersionRepository; public PermalinkService( ContentDbContext contentDbContext, @@ -49,8 +48,7 @@ public PermalinkService( IPublicBlobStorageService publicBlobStorageService, IFrontendService frontendService, ISubjectRepository subjectRepository, - IPublicationRepository publicationRepository, - IReleaseVersionRepository releaseVersionRepository) + IPublicationRepository publicationRepository) { _contentDbContext = contentDbContext; _tableBuilderService = tableBuilderService; @@ -59,7 +57,6 @@ public PermalinkService( _frontendService = frontendService; _subjectRepository = subjectRepository; _publicationRepository = publicationRepository; - _releaseVersionRepository = releaseVersionRepository; } public async Task> GetPermalink(Guid permalinkId, @@ -307,9 +304,11 @@ private async Task> FindLatestPublishedReleaseVersion { return await _subjectRepository.FindPublicationIdForSubject(subjectId) .OrNotFound() - .OnSuccess(publicationId => - _releaseVersionRepository.GetLatestPublishedReleaseVersion(publicationId).OrNotFound()) - .OnSuccess(releaseVersion => releaseVersion.Id); + .OnSuccess(async publicationId => await _contentDbContext.Publications + .Where(p => p.Id == publicationId) + .Select(p => p.LatestPublishedReleaseVersionId) + .SingleOrDefaultAsync() + .OrNotFound()); } private async Task GetPermalinkStatus(Guid subjectId) @@ -317,9 +316,7 @@ private async Task GetPermalinkStatus(Guid subjectId) // TODO EES-3339 This doesn't currently include a status to warn if the footnotes have been amended on a Release, // and will return 'Current' unless one of the other cases also applies. - var releasesWithSubject = await _contentDbContext.ReleaseFiles - .Include(rf => rf.File) - .Include(rf => rf.ReleaseVersion) + var releasesVersionsWithSubject = await _contentDbContext.ReleaseFiles .Where(rf => rf.File.SubjectId == subjectId && rf.File.Type == FileType.Data @@ -327,26 +324,25 @@ private async Task GetPermalinkStatus(Guid subjectId) .Select(rf => rf.ReleaseVersion) .ToListAsync(); - if (releasesWithSubject.Count == 0) + if (releasesVersionsWithSubject.Count == 0) { return PermalinkStatus.SubjectRemoved; } var publication = await _contentDbContext.Publications .Include(p => p.LatestPublishedReleaseVersion) - .SingleAsync(p => p.Id == releasesWithSubject.First().PublicationId); + .SingleAsync(p => p.Id == releasesVersionsWithSubject.First().PublicationId); var latestPublishedReleaseVersion = publication.LatestPublishedReleaseVersion; - if (latestPublishedReleaseVersion != null && releasesWithSubject.All(rv => - rv.Year != latestPublishedReleaseVersion.Year - || rv.TimePeriodCoverage != latestPublishedReleaseVersion.TimePeriodCoverage)) + if (latestPublishedReleaseVersion != null && releasesVersionsWithSubject.All(rv => + rv.ReleaseId != latestPublishedReleaseVersion.ReleaseId)) { return PermalinkStatus.NotForLatestRelease; } if (latestPublishedReleaseVersion != null - && releasesWithSubject.All(rv => rv.Id != latestPublishedReleaseVersion.Id)) + && releasesVersionsWithSubject.All(rv => rv.Id != latestPublishedReleaseVersion.Id)) { return PermalinkStatus.SubjectReplacedOrRemoved; } From 364dc0887710866afddbbe562c5414068e74e529 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Mon, 16 Dec 2024 16:24:16 +0000 Subject: [PATCH 23/40] EES-5656 Change TableBuilderService to not use method GetLatestPublishedReleaseVersion(Guid publicationId) --- .../TableBuilderServicePermissionTests.cs | 124 ++++++++--------- .../TableBuilderServiceTests.cs | 125 +++++++----------- ...ts.QueryToCsvStream_ReleaseVersionId.snap} | 0 ...CsvStream_ReleaseVersionId_NoFilters.snap} | 0 .../TableBuilderService.cs | 40 +++--- 5 files changed, 134 insertions(+), 155 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/{TableBuilderServiceTests.QueryToCsvStream_ReleaseId.snap => TableBuilderServiceTests.QueryToCsvStream_ReleaseVersionId.snap} (100%) rename src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/{TableBuilderServiceTests.QueryToCsvStream_ReleaseId_NoFilters.snap => TableBuilderServiceTests.QueryToCsvStream_ReleaseVersionId_NoFilters.snap} (100%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServicePermissionTests.cs index 9ff053349b9..94b5d24c312 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServicePermissionTests.cs @@ -1,110 +1,115 @@ #nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data.Query; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Utils; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Data.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Data.Services.Options; using GovUk.Education.ExploreEducationStatistics.Data.Services.Security; using Microsoft.Extensions.Options; using Moq; -using System; -using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; -using GovUk.Education.ExploreEducationStatistics.Data.Services.Options; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.PermissionTestUtils; +using static GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils.ContentDbUtils; using static Moq.MockBehavior; -using ReleaseVersion = GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseVersion; namespace GovUk.Education.ExploreEducationStatistics.Data.Services.Tests { public class TableBuilderServicePermissionTests { - private static readonly Guid PublicationId = Guid.NewGuid(); - private static readonly Guid ReleaseVersionId = Guid.NewGuid(); - private static readonly Guid SubjectId = Guid.NewGuid(); - - private readonly Subject _subject = new() - { - Id = Guid.NewGuid(), - }; - - private readonly ReleaseSubject _releaseSubject = new() - { - ReleaseVersionId = ReleaseVersionId, - SubjectId = SubjectId, - }; + private readonly DataFixture _dataFixture = new(); [Fact] public async Task Query_LatestRelease_CanViewSubjectData() { - await PolicyCheckBuilder() - .SetupResourceCheckToFail(_releaseSubject, DataSecurityPolicies.CanViewSubjectData) - .AssertForbidden( - async userService => - { - var statisticsPersistenceHelper = StatisticsPersistenceHelperMock(_subject); + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); - MockUtils.SetupCall(statisticsPersistenceHelper, _releaseSubject); + var releaseVersion = publication.Releases.Single().Versions.Single(); - var subjectRepository = new Mock(Strict); + var releaseSubject = new ReleaseSubject + { + ReleaseVersionId = releaseVersion.Id, + SubjectId = Guid.NewGuid(), + }; - subjectRepository - .Setup(s => s.FindPublicationIdForSubject(_subject.Id, default)) - .ReturnsAsync(PublicationId); + var statisticsPersistenceHelper = MockUtils.MockPersistenceHelper(); + MockUtils.SetupCall(statisticsPersistenceHelper, releaseSubject); - var releaseVersionRepository = new Mock(Strict); + await using var contextDbContext = InMemoryContentDbContext(); + contextDbContext.Publications.Add(publication); + await contextDbContext.SaveChangesAsync(); - releaseVersionRepository - .Setup(s => s.GetLatestPublishedReleaseVersion(PublicationId, default)) - .ReturnsAsync(new ReleaseVersion - { - Id = ReleaseVersionId - }); + var subjectRepository = new Mock(Strict); + subjectRepository + .Setup(s => s.FindPublicationIdForSubject(releaseSubject.SubjectId, default)) + .ReturnsAsync(publication.Id); + + await PolicyCheckBuilder() + .SetupResourceCheckToFail(releaseSubject, DataSecurityPolicies.CanViewSubjectData) + .AssertForbidden( + async userService => + { var service = BuildTableBuilderService( + contextDbContext, userService: userService.Object, subjectRepository: subjectRepository.Object, - releaseVersionRepository: releaseVersionRepository.Object, statisticsPersistenceHelper: statisticsPersistenceHelper.Object ); return await service.Query( - new FullTableQuery - { - SubjectId = _subject.Id - } + new FullTableQuery { SubjectId = releaseSubject.SubjectId } ); } ); } [Fact] - public async Task Query_ReleaseId_CanViewSubjectData() + public async Task Query_ReleaseVersionId_CanViewSubjectData() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var releaseVersion = publication.Releases.Single().Versions.Single(); + + var releaseSubject = new ReleaseSubject + { + ReleaseVersionId = releaseVersion.Id, + SubjectId = Guid.NewGuid(), + }; + + var statisticsPersistenceHelper = MockUtils.MockPersistenceHelper(); + MockUtils.SetupCall(statisticsPersistenceHelper, releaseSubject); + + await using var contextDbContext = InMemoryContentDbContext(); + contextDbContext.Publications.Add(publication); + await contextDbContext.SaveChangesAsync(); + await PolicyCheckBuilder() - .SetupResourceCheckToFail(_releaseSubject, DataSecurityPolicies.CanViewSubjectData) + .SetupResourceCheckToFail(releaseSubject, DataSecurityPolicies.CanViewSubjectData) .AssertForbidden( async userService => { - var statisticsPersistenceHelper = StatisticsPersistenceHelperMock(_subject); - - MockUtils.SetupCall(statisticsPersistenceHelper, _releaseSubject); - var service = BuildTableBuilderService( + contextDbContext, userService: userService.Object, statisticsPersistenceHelper: statisticsPersistenceHelper.Object ); return await service.Query( - ReleaseVersionId, - new FullTableQuery - { - SubjectId = _subject.Id - }, + releaseVersionId: releaseVersion.Id, + new FullTableQuery { SubjectId = releaseSubject.SubjectId }, boundaryLevelId: null ); } @@ -112,6 +117,7 @@ await PolicyCheckBuilder() } private TableBuilderService BuildTableBuilderService( + ContentDbContext contentDbContext, IFilterItemRepository? filterItemRepository = null, ILocationService? locationService = null, IObservationService? observationService = null, @@ -120,29 +126,23 @@ private TableBuilderService BuildTableBuilderService( ISubjectCsvMetaService? subjectCsvMetaService = null, ISubjectRepository? subjectRepository = null, IUserService? userService = null, - IReleaseVersionRepository? releaseVersionRepository = null, IOptions? tableBuilderOptions = null, IOptions? locationsOptions = null) { return new( Mock.Of(), + contentDbContext, filterItemRepository ?? Mock.Of(Strict), locationService ?? Mock.Of(Strict), observationService ?? Mock.Of(Strict), - statisticsPersistenceHelper ?? StatisticsPersistenceHelperMock(_subject).Object, + statisticsPersistenceHelper ?? MockUtils.MockPersistenceHelper().Object, subjectResultMetaService ?? Mock.Of(Strict), subjectCsvMetaService ?? Mock.Of(Strict), subjectRepository ?? Mock.Of(Strict), userService ?? Mock.Of(Strict), - releaseVersionRepository ?? Mock.Of(Strict), tableBuilderOptions ?? new TableBuilderOptions().ToOptionsWrapper(), locationsOptions ?? new LocationsOptions().ToOptionsWrapper() ); } - - private static Mock> StatisticsPersistenceHelperMock(Subject subject) - { - return MockUtils.MockPersistenceHelper(subject.Id, subject); - } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServiceTests.cs index 8ef7eba13c9..3b4c307a5e1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/TableBuilderServiceTests.cs @@ -1,4 +1,9 @@ #nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data.Query; @@ -8,8 +13,6 @@ using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; @@ -18,17 +21,12 @@ using GovUk.Education.ExploreEducationStatistics.Data.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Data.Model.Utils; using GovUk.Education.ExploreEducationStatistics.Data.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Data.Services.Options; using GovUk.Education.ExploreEducationStatistics.Data.Services.Utils; using GovUk.Education.ExploreEducationStatistics.Data.ViewModels.Meta; using Microsoft.Extensions.Options; using Moq; using Snapshooter.Xunit; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Data.Services.Options; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; @@ -48,9 +46,7 @@ public async Task Query_LatestRelease() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -244,9 +240,7 @@ public async Task Query_LatestRelease_SubjectNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -290,9 +284,7 @@ public async Task Query_LatestRelease_PublicationNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -334,7 +326,7 @@ public async Task Query_LatestRelease_PublicationNotFound() [Fact] public async Task Query_LatestRelease_ReleaseNotFound() { - // Set up a ReleaseSubject that references a non-existent release + // Set up a ReleaseSubject that references a non-existent release version ReleaseSubject releaseSubject = _fixture .DefaultReleaseSubject() .WithReleaseVersion(_fixture @@ -370,9 +362,7 @@ public async Task Query_LatestRelease_PredictedTableTooBig() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -453,9 +443,7 @@ public async Task Query_ReleaseId() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -640,13 +628,11 @@ public async Task Query_ReleaseId() } [Fact] - public async Task Query_ReleaseId_ReleaseNotFound() + public async Task Query_ReleaseVersionId_ReleaseVersionNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -677,21 +663,22 @@ public async Task Query_ReleaseId_ReleaseNotFound() SubjectId = releaseSubject.SubjectId }; - // Query using a non-existent release id - var result = await service.Query(Guid.NewGuid(), query, null); + // Query using a non-existent release version id + var result = await service.Query( + releaseVersionId: Guid.NewGuid(), + query, + boundaryLevelId: null); result.AssertNotFound(); } } [Fact] - public async Task Query_ReleaseId_SubjectNotFound() + public async Task Query_ReleaseVersionId_SubjectNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -723,20 +710,21 @@ public async Task Query_ReleaseId_SubjectNotFound() SubjectId = Guid.NewGuid(), }; - var result = await service.Query(releaseVersion.Id, query, null); + var result = await service.Query( + releaseVersionId: releaseVersion.Id, + query, + boundaryLevelId: null); result.AssertNotFound(); } } [Fact] - public async Task Query_ReleaseId_PredictedTableTooBig() + public async Task Query_ReleaseVersionId_PredictedTableTooBig() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -803,7 +791,10 @@ public async Task Query_ReleaseId_PredictedTableTooBig() tableBuilderOptions: options.ToOptionsWrapper() ); - var result = await service.Query(releaseSubject.ReleaseVersionId, query, null); + var result = await service.Query( + releaseVersionId: releaseSubject.ReleaseVersionId, + query, + boundaryLevelId: null); VerifyAllMocks(filterItemRepository); @@ -816,9 +807,7 @@ public async Task QueryToCsvStream_LatestRelease() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var filters = _fixture.DefaultFilter() .ForIndex(0, s => @@ -1011,7 +1000,7 @@ public async Task QueryToCsvStream_LatestRelease() } [Fact] - public async Task QueryToCsvStream_LatestRelease_ReleaseNotFound() + public async Task QueryToCsvStream_LatestRelease_ReleaseVersionNotFound() { // Set up a ReleaseSubject that references a non-existent release ReleaseSubject releaseSubject = _fixture @@ -1053,9 +1042,7 @@ public async Task QueryToCsvStream_LatestRelease_SubjectNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1101,9 +1088,7 @@ public async Task QueryToCsvStream_LatestRelease_PredictedTableTooBig() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1182,13 +1167,11 @@ public async Task QueryToCsvStream_LatestRelease_PredictedTableTooBig() } [Fact] - public async Task QueryToCsvStream_ReleaseId() + public async Task QueryToCsvStream_ReleaseVersionId() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1368,13 +1351,11 @@ public async Task QueryToCsvStream_ReleaseId() } [Fact] - public async Task QueryToCsvStream_ReleaseId_NoFilters() + public async Task QueryToCsvStream_ReleaseVersionId_NoFilters() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1504,13 +1485,11 @@ public async Task QueryToCsvStream_ReleaseId_NoFilters() } [Fact] - public async Task QueryToCsvStream_ReleaseId_ReleaseNotFound() + public async Task QueryToCsvStream_ReleaseVersionId_ReleaseVersionNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1544,21 +1523,22 @@ public async Task QueryToCsvStream_ReleaseId_ReleaseNotFound() using var stream = new MemoryStream(); - // Query using a non-existent release id - var result = await service.QueryToCsvStream(Guid.NewGuid(), query, stream); + // Query using a non-existent release version id + var result = await service.QueryToCsvStream( + releaseVersionId: Guid.NewGuid(), + query, + stream); result.AssertNotFound(); } } [Fact] - public async Task QueryToCsvStream_ReleaseId_SubjectNotFound() + public async Task QueryToCsvStream_ReleaseVersionId_SubjectNotFound() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1600,13 +1580,11 @@ public async Task QueryToCsvStream_ReleaseId_SubjectNotFound() } [Fact] - public async Task QueryToCsvStream_ReleaseId_PredictedTableTooBig() + public async Task QueryToCsvStream_ReleaseVersionId_PredictedTableTooBig() { Publication publication = _fixture .DefaultPublication() - .WithReleases(_fixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_fixture.DefaultRelease(publishedVersions: 1)]); var releaseVersion = publication.ReleaseVersions.Single(); @@ -1711,12 +1689,12 @@ private static TableBuilderService BuildTableBuilderService( ISubjectCsvMetaService? subjectCsvMetaService = null, ISubjectRepository? subjectRepository = null, IUserService? userService = null, - IReleaseVersionRepository? releaseVersionRepository = null, IOptions? tableBuilderOptions = null, IOptions? locationsOptions = null) { return new( statisticsDbContext, + contentDbContext ?? InMemoryContentDbContext(), filterItemRepository ?? Mock.Of(Strict), locationService ?? Mock.Of(Strict), observationService ?? Mock.Of(Strict), @@ -1725,7 +1703,6 @@ private static TableBuilderService BuildTableBuilderService( subjectCsvMetaService ?? Mock.Of(Strict), subjectRepository ?? new SubjectRepository(statisticsDbContext), userService ?? AlwaysTrueUserService().Object, - releaseVersionRepository ?? new ReleaseVersionRepository(contentDbContext ?? Mock.Of()), tableBuilderOptions ?? DefaultTableBuilderOptions(), locationsOptions ?? DefaultLocationOptions() ); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseId.snap b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseVersionId.snap similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseId.snap rename to src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseVersionId.snap diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseId_NoFilters.snap b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseVersionId_NoFilters.snap similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseId_NoFilters.snap rename to src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/__snapshots__/TableBuilderServiceTests.QueryToCsvStream_ReleaseVersionId_NoFilters.snap diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services/TableBuilderService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Services/TableBuilderService.cs index a3c0c22295a..ce7ae6d4af2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Services/TableBuilderService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Services/TableBuilderService.cs @@ -1,4 +1,12 @@ #nullable enable +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using CsvHelper; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; @@ -6,7 +14,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Validators; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Model.Repository.Interfaces; @@ -20,14 +28,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using static GovUk.Education.ExploreEducationStatistics.Data.Services.Utils.TableBuilderUtils; using static GovUk.Education.ExploreEducationStatistics.Data.Services.ValidationErrorMessages; using Unit = GovUk.Education.ExploreEducationStatistics.Common.Model.Unit; @@ -36,7 +36,8 @@ namespace GovUk.Education.ExploreEducationStatistics.Data.Services { public class TableBuilderService : ITableBuilderService { - private readonly StatisticsDbContext _context; + private readonly StatisticsDbContext _statisticsDbContext; + private readonly ContentDbContext _contentDbContext; private readonly IFilterItemRepository _filterItemRepository; private readonly ILocationService _locationService; private readonly IObservationService _observationService; @@ -45,12 +46,12 @@ public class TableBuilderService : ITableBuilderService private readonly ISubjectCsvMetaService _subjectCsvMetaService; private readonly ISubjectRepository _subjectRepository; private readonly IUserService _userService; - private readonly IReleaseVersionRepository _releaseVersionRepository; private readonly TableBuilderOptions _options; private readonly LocationsOptions _locationOptions; public TableBuilderService( - StatisticsDbContext context, + StatisticsDbContext statisticsDbContext, + ContentDbContext contentDbContext, IFilterItemRepository filterItemRepository, ILocationService locationService, IObservationService observationService, @@ -59,11 +60,11 @@ public TableBuilderService( ISubjectCsvMetaService subjectCsvMetaService, ISubjectRepository subjectRepository, IUserService userService, - IReleaseVersionRepository releaseVersionRepository, IOptions options, IOptions locationOptions) { - _context = context; + _statisticsDbContext = statisticsDbContext; + _contentDbContext = contentDbContext; _filterItemRepository = filterItemRepository; _locationService = locationService; _observationService = observationService; @@ -72,7 +73,6 @@ public TableBuilderService( _subjectCsvMetaService = subjectCsvMetaService; _subjectRepository = subjectRepository; _userService = userService; - _releaseVersionRepository = releaseVersionRepository; _options = options.Value; _locationOptions = locationOptions.Value; } @@ -189,7 +189,7 @@ private async Task>> ListQueryObservation (await _observationService.GetMatchedObservations(query, cancellationToken)) .Select(row => row.Id); - return await _context + return await _statisticsDbContext .Observation .AsNoTracking() .Include(o => o.Location) @@ -236,9 +236,11 @@ private async Task> FindLatestPublishedReleaseVersion { return await _subjectRepository.FindPublicationIdForSubject(subjectId) .OrNotFound() - .OnSuccess(publicationId => - _releaseVersionRepository.GetLatestPublishedReleaseVersion(publicationId).OrNotFound()) - .OnSuccess(releaseVersion => releaseVersion.Id); + .OnSuccess(async publicationId => await _contentDbContext.Publications + .Where(p => p.Id == publicationId) + .Select(p => p.LatestPublishedReleaseVersionId) + .SingleOrDefaultAsync() + .OrNotFound()); } private Task> CheckReleaseSubjectExists(Guid subjectId, From 2d81e797bbdd0620486d9453983354d62332f0cf Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Mon, 16 Dec 2024 17:36:09 +0000 Subject: [PATCH 24/40] EES-5656 Change TableBuilderController to not use method GetLatestPublishedReleaseVersion(Guid publicationId) --- .../TableBuilderControllerTests.cs | 457 +++++++++++------- .../Controllers/TableBuilderController.cs | 72 ++- 2 files changed, 318 insertions(+), 211 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Controllers/TableBuilderControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Controllers/TableBuilderControllerTests.cs index d182167b4b8..687c858841d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Controllers/TableBuilderControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Api.Tests/Controllers/TableBuilderControllerTests.cs @@ -1,4 +1,10 @@ #nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Model.Chart; @@ -9,7 +15,6 @@ using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Data.Api.Cache; using GovUk.Education.ExploreEducationStatistics.Data.Api.Controllers; @@ -23,15 +28,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Moq; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; -using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static Moq.MockBehavior; @@ -40,83 +38,50 @@ namespace GovUk.Education.ExploreEducationStatistics.Data.Api.Tests.Controllers public class TableBuilderControllerTests(TestApplicationFactory testApp) : IntegrationTestFixture(testApp) { - private static readonly DataFixture Fixture = new(); - - private static readonly Release Release = Fixture.DefaultRelease(publishedVersions: 1) - .WithPublication(Fixture.DefaultPublication()); - - private static readonly ReleaseVersion ReleaseVersion = Release.Versions.Single(); - - private static readonly DataBlockParent DataBlockParent = Fixture - .DefaultDataBlockParent() - .WithLatestPublishedVersion(Fixture - .DefaultDataBlockVersion() - .WithReleaseVersion(ReleaseVersion) - .WithDates(published: DateTime.UtcNow.AddDays(-1)) - .WithQuery(new FullTableQuery - { - SubjectId = Guid.NewGuid(), - LocationIds = [Guid.NewGuid(),], - TimePeriod = new TimePeriodQuery - { - StartYear = 2021, - StartCode = CalendarYear, - EndYear = 2022, - EndCode = CalendarYear - }, - Filters = new List(), - Indicators = new List // use collection expression -> test failures - { - Guid.NewGuid(), - }, - }) - .WithTable(new TableBuilderConfiguration - { - TableHeaders = new TableHeaders - { - Rows = new List { new("table header 1", TableHeaderType.Filter) } - } - }) - .WithCharts(ListOf(new LineChart - { - Title = "Test chart", - Height = 400, - Width = 500, - })) - .Generate()) - .Generate(); - - private static readonly DataBlockParent DataBlockParentWithNoPublishedVersion = Fixture - .DefaultDataBlockParent() - .WithLatestDraftVersion(Fixture - .DefaultDataBlockVersion() - .WithReleaseVersion(ReleaseVersion) - .Generate()) - .Generate(); - - private static readonly Guid PublicationId = ReleaseVersion.PublicationId; - - private static readonly Guid ReleaseVersionId = ReleaseVersion.Id; + private readonly DataFixture _dataFixture = new(); - private static readonly Guid DataBlockId = DataBlockParent.LatestPublishedVersion!.Id; - - private static readonly Guid DataBlockParentId = DataBlockParent.Id; + private static readonly List Charts = + [ + new LineChart + { + Title = "Test chart", + Height = 400, + Width = 500 + } + ]; - private static readonly FullTableQuery FullTableQuery = - DataBlockParent.LatestPublishedVersion!.Query; + private static readonly FullTableQuery FullTableQuery = new() + { + SubjectId = Guid.NewGuid(), + LocationIds = [Guid.NewGuid()], + TimePeriod = new TimePeriodQuery + { + StartYear = 2021, + StartCode = CalendarYear, + EndYear = 2022, + EndCode = CalendarYear + }, + Filters = new List(), + Indicators = new List // use collection expression -> test failures + { + Guid.NewGuid() + } + }; - private static readonly TableBuilderConfiguration TableConfiguration = - DataBlockParent.LatestPublishedVersion!.Table; + private static readonly TableBuilderConfiguration TableConfiguration = new() + { + TableHeaders = new TableHeaders { Rows = [new TableHeader("table header 1", TableHeaderType.Filter)] } + }; private readonly TableBuilderResultViewModel _tableBuilderResults = new() { SubjectMeta = new SubjectResultMetaViewModel { - TimePeriodRange = new List - { - new(2020, AcademicYear), - new(2021, AcademicYear), - } + TimePeriodRange = + [ + new TimePeriodMetaViewModel(2020, AcademicYear), + new TimePeriodMetaViewModel(2021, AcademicYear) + ] }, Results = new List { @@ -164,8 +129,7 @@ public async Task Query_Csv() ) .ReturnsAsync(Unit.Instance) .Callback( - (_, stream, _) => { stream.WriteText("Test csv"); } - ); + (_, stream, _) => stream.WriteText("Test csv")); var client = SetupApp(tableBuilderService: tableBuilderService.Object).CreateClient(); @@ -181,17 +145,22 @@ public async Task Query_Csv() } [Fact] - public async Task Query_ReleaseId() + public async Task Query_ReleaseVersionId() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var releaseVersion = publication.Releases.Single().Versions.Single(); + await TestApp.AddTestData(context => - context.ReleaseVersions.Add(ReleaseVersion)); + context.Publications.Add(publication)); var tableBuilderService = new Mock(Strict); tableBuilderService .Setup( s => s.Query( - ReleaseVersionId, + releaseVersion.Id, ItIs.DeepEqualTo(FullTableQuery), It.IsAny(), It.IsAny() @@ -203,7 +172,7 @@ await TestApp.AddTestData(context => .CreateClient(); var response = await client - .PostAsync($"/api/tablebuilder/release/{ReleaseVersionId}", + .PostAsync($"/api/tablebuilder/release/{releaseVersion.Id}", new JsonNetContent(FullTableQuery)); VerifyAllMocks(tableBuilderService); @@ -212,17 +181,23 @@ await TestApp.AddTestData(context => } [Fact] - public async Task Query_ReleaseId_Csv() + public async Task Query_ReleaseVersionId_Csv() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + await TestApp.AddTestData(context => - context.ReleaseVersions.Add(ReleaseVersion)); + context.Publications.Add(publication)); var tableBuilderService = new Mock(Strict); tableBuilderService .Setup( s => s.QueryToCsvStream( - ReleaseVersionId, + releaseVersion.Id, ItIs.DeepEqualTo(FullTableQuery), It.IsAny(), It.IsAny() @@ -230,14 +205,13 @@ await TestApp.AddTestData(context => ) .ReturnsAsync(Unit.Instance) .Callback( - (_, _, stream, _) => { stream.WriteText("Test csv"); } - ); + (_, _, stream, _) => stream.WriteText("Test csv")); var client = SetupApp(tableBuilderService: tableBuilderService.Object) .CreateClient(); var response = await client - .PostAsync($"/api/tablebuilder/release/{ReleaseVersionId}", + .PostAsync($"/api/tablebuilder/release/{releaseVersion.Id}", content: new JsonNetContent(FullTableQuery), headers: new Dictionary { { HeaderNames.Accept, ContentTypes.Csv } } ); @@ -251,12 +225,34 @@ await TestApp.AddTestData(context => [Fact] public async Task QueryForTableBuilderResult() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + + DataBlockParent dataBlockParent = _dataFixture + .DefaultDataBlockParent() + .WithLatestPublishedVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion) + .WithDates(published: DateTime.UtcNow.AddDays(-1)) + .WithQuery(FullTableQuery) + .WithTable(TableConfiguration) + .WithCharts(Charts)); + + var dataBlockId = dataBlockParent.LatestPublishedVersion!.Id; + await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParent)); + { + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParent); + }); - var cacheKey = new DataBlockTableResultCacheKey(publicationSlug: ReleaseVersion.Publication.Slug, - releaseSlug: ReleaseVersion.Slug, - DataBlockParentId); + var cacheKey = new DataBlockTableResultCacheKey( + publicationSlug: publication.Slug, + releaseSlug: release.Slug, + dataBlockParent.Id); BlobCacheService .Setup(s => s.GetItemAsync(cacheKey, typeof(TableBuilderResultViewModel))) @@ -269,7 +265,7 @@ await TestApp.AddTestData(context => var dataBlockService = new Mock(); dataBlockService - .Setup(s => s.GetDataBlockTableResult(ReleaseVersionId, DataBlockId, null)) + .Setup(s => s.GetDataBlockTableResult(releaseVersion.Id, dataBlockId, null)) .ReturnsAsync(_tableBuilderResults); var client = SetupApp(dataBlockService: dataBlockService.Object) @@ -277,7 +273,7 @@ await TestApp.AddTestData(context => var response = await client.GetAsync( - $"http://localhost/api/tablebuilder/release/{ReleaseVersionId}/data-block/{DataBlockParentId}"); + $"http://localhost/api/tablebuilder/release/{releaseVersion.Id}/data-block/{dataBlockParent.Id}"); VerifyAllMocks(BlobCacheService, dataBlockService); @@ -287,10 +283,18 @@ await client.GetAsync( [Fact] public async Task QueryForTableBuilderResult_NotFound() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + + await TestApp.AddTestData(context => context.Publications.Add(publication)); + var client = SetupApp().CreateClient(); var response = - await client.GetAsync($"/api/tablebuilder/release/{ReleaseVersionId}/data-block/{DataBlockParentId}"); + await client.GetAsync($"/api/tablebuilder/release/{releaseVersion.Id}/data-block/{Guid.NewGuid()}"); response.AssertNotFound(); } @@ -298,13 +302,32 @@ public async Task QueryForTableBuilderResult_NotFound() [Fact] public async Task QueryForTableBuilderResult_NotModified() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + + DataBlockParent dataBlockParent = _dataFixture + .DefaultDataBlockParent() + .WithLatestPublishedVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion) + .WithDates(published: DateTime.UtcNow.AddDays(-1)) + .WithQuery(FullTableQuery) + .WithTable(TableConfiguration) + .WithCharts(Charts)); + await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParent)); + { + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParent); + }); var client = SetupApp() .CreateClient(); - var publishedDate = DataBlockParent + var publishedDate = dataBlockParent .LatestPublishedVersion! .Published! .Value; @@ -314,7 +337,7 @@ await TestApp.AddTestData(context => var ifModifiedSinceDate = publishedDate.AddSeconds(1); var response = await client.GetAsync( - $"/api/tablebuilder/release/{ReleaseVersionId}/data-block/{DataBlockParentId}", + $"/api/tablebuilder/release/{releaseVersion.Id}/data-block/{dataBlockParent.Id}", new Dictionary { { HeaderNames.IfModifiedSince, ifModifiedSinceDate.ToUniversalTime().ToString("R") }, @@ -330,12 +353,34 @@ await TestApp.AddTestData(context => [Fact] public async Task QueryForTableBuilderResult_ETagChanged() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + + DataBlockParent dataBlockParent = _dataFixture + .DefaultDataBlockParent() + .WithLatestPublishedVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion) + .WithDates(published: DateTime.UtcNow.AddDays(-1)) + .WithQuery(FullTableQuery) + .WithTable(TableConfiguration) + .WithCharts(Charts)); + + var dataBlockId = dataBlockParent.LatestPublishedVersion!.Id; + await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParent)); + { + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParent); + }); - var cacheKey = new DataBlockTableResultCacheKey(publicationSlug: ReleaseVersion.Publication.Slug, - releaseSlug: ReleaseVersion.Slug, - DataBlockParentId); + var cacheKey = new DataBlockTableResultCacheKey( + publicationSlug: publication.Slug, + releaseSlug: release.Slug, + dataBlockParent.Id); BlobCacheService .Setup(s => s.GetItemAsync(cacheKey, typeof(TableBuilderResultViewModel))) @@ -348,13 +393,13 @@ await TestApp.AddTestData(context => var dataBlockService = new Mock(Strict); dataBlockService - .Setup(s => s.GetDataBlockTableResult(ReleaseVersionId, DataBlockId, null)) + .Setup(s => s.GetDataBlockTableResult(releaseVersion.Id, dataBlockId, null)) .ReturnsAsync(_tableBuilderResults); var client = SetupApp(dataBlockService: dataBlockService.Object) .CreateClient(); - var publishedDate = DataBlockParent + var publishedDate = dataBlockParent .LatestPublishedVersion! .Published! .Value; @@ -365,7 +410,7 @@ await TestApp.AddTestData(context => var ifModifiedSinceDate = publishedDate.AddSeconds(1); var response = await client.GetAsync( - $"/api/tablebuilder/release/{ReleaseVersionId}/data-block/{DataBlockParentId}", + $"/api/tablebuilder/release/{releaseVersion.Id}/data-block/{dataBlockParent.Id}", new Dictionary { { HeaderNames.IfModifiedSince, ifModifiedSinceDate.ToUniversalTime().ToString("R") }, @@ -381,12 +426,34 @@ await TestApp.AddTestData(context => [Fact] public async Task QueryForTableBuilderResult_LastModifiedChanged() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + + DataBlockParent dataBlockParent = _dataFixture + .DefaultDataBlockParent() + .WithLatestPublishedVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion) + .WithDates(published: DateTime.UtcNow.AddDays(-1)) + .WithQuery(FullTableQuery) + .WithTable(TableConfiguration) + .WithCharts(Charts)); + + var dataBlockId = dataBlockParent.LatestPublishedVersion!.Id; + await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParent)); + { + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParent); + }); - var cacheKey = new DataBlockTableResultCacheKey(publicationSlug: ReleaseVersion.Publication.Slug, - releaseSlug: ReleaseVersion.Slug, - DataBlockParentId); + var cacheKey = new DataBlockTableResultCacheKey( + publicationSlug: publication.Slug, + releaseSlug: release.Slug, + dataBlockParent.Id); BlobCacheService .Setup(s => s.GetItemAsync(cacheKey, typeof(TableBuilderResultViewModel))) @@ -399,7 +466,7 @@ await TestApp.AddTestData(context => var dataBlockService = new Mock(Strict); dataBlockService - .Setup(s => s.GetDataBlockTableResult(ReleaseVersionId, DataBlockId, null)) + .Setup(s => s.GetDataBlockTableResult(releaseVersion.Id, dataBlockId, null)) .ReturnsAsync(_tableBuilderResults); var client = SetupApp(dataBlockService: dataBlockService.Object) @@ -407,14 +474,14 @@ await TestApp.AddTestData(context => // The latest published DataBlockVersion has been published since the caller last requested it, so we // consider this "Modified" by the published date alone. - var yearBeforePublishedDate = DataBlockParent + var yearBeforePublishedDate = dataBlockParent .LatestPublishedVersion! .Published! .Value .AddYears(-1); var response = await client.GetAsync( - $"/api/tablebuilder/release/{ReleaseVersionId}/data-block/{DataBlockParentId}", + $"/api/tablebuilder/release/{releaseVersion.Id}/data-block/{dataBlockParent.Id}", new Dictionary { { HeaderNames.IfModifiedSince, yearBeforePublishedDate.ToUniversalTime().ToString("R") }, @@ -430,20 +497,40 @@ await TestApp.AddTestData(context => [Fact] public async Task QueryForFastTrack() { - await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParent)); + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases([ + _dataFixture + .DefaultRelease(publishedVersions: 1, year: 2020), + _dataFixture + .DefaultRelease(publishedVersions: 1, year: 2021) + ]); + + var release = publication.Releases.Single(r => r.Year == 2021); + var releaseVersion = release.Versions.Single(); + + DataBlockParent dataBlockParent = _dataFixture + .DefaultDataBlockParent() + .WithLatestPublishedVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion) + .WithDates(published: DateTime.UtcNow.AddDays(-1)) + .WithQuery(FullTableQuery) + .WithTable(TableConfiguration) + .WithCharts(Charts)); + + var dataBlockId = dataBlockParent.LatestPublishedVersion!.Id; - var latestReleaseVersion = new ReleaseVersion + await TestApp.AddTestData(context => { - Id = ReleaseVersionId, - ReleaseName = "2020", - TimePeriodCoverage = AcademicYear - }; + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParent); + }); var cacheKey = new DataBlockTableResultCacheKey( - ReleaseVersion.Publication.Slug, - ReleaseVersion.Slug, - DataBlockParentId); + publicationSlug: publication.Slug, + releaseSlug: release.Slug, + dataBlockParent.Id); BlobCacheService .Setup(s => s.GetItemAsync(cacheKey, typeof(TableBuilderResultViewModel))) @@ -456,38 +543,34 @@ await TestApp.AddTestData(context => var dataBlockService = new Mock(Strict); dataBlockService - .Setup(s => s.GetDataBlockTableResult(ReleaseVersionId, DataBlockId, null)) + .Setup(s => s.GetDataBlockTableResult(releaseVersion.Id, dataBlockId, null)) .ReturnsAsync(_tableBuilderResults); - var releaseVersionRepository = new Mock(Strict); - - releaseVersionRepository - .Setup(s => s.GetLatestPublishedReleaseVersion(PublicationId, default)) - .ReturnsAsync(latestReleaseVersion); - var client = SetupApp( - dataBlockService: dataBlockService.Object, - releaseVersionRepository: releaseVersionRepository.Object + dataBlockService: dataBlockService.Object ) .CreateClient(); - var response = await client.GetAsync($"/api/tablebuilder/fast-track/{DataBlockParentId}"); + var response = await client.GetAsync($"/api/tablebuilder/fast-track/{dataBlockParent.Id}"); - VerifyAllMocks(BlobCacheService, dataBlockService, releaseVersionRepository); + VerifyAllMocks( + BlobCacheService, + dataBlockService + ); var viewModel = response.AssertOk(); - Assert.Equal(DataBlockParentId, viewModel.DataBlockParentId); - Assert.Equal(ReleaseVersionId, viewModel.ReleaseId); - Assert.Equal(ReleaseVersion.Slug, viewModel.ReleaseSlug); - Assert.Equal(ReleaseVersion.Type, viewModel.ReleaseType); + Assert.Equal(dataBlockParent.Id, viewModel.DataBlockParentId); + Assert.Equal(releaseVersion.Id, viewModel.ReleaseId); + Assert.Equal(release.Slug, viewModel.ReleaseSlug); + Assert.Equal(releaseVersion.Type, viewModel.ReleaseType); viewModel.Configuration.AssertDeepEqualTo(TableConfiguration); viewModel.FullTable.AssertDeepEqualTo(_tableBuilderResults); Assert.True(viewModel.LatestData); - Assert.Equal("Academic year 2020/21", viewModel.LatestReleaseTitle); + Assert.Equal(release.Title, viewModel.LatestReleaseTitle); var queryViewModel = viewModel.Query; Assert.NotNull(queryViewModel); - Assert.Equal(PublicationId, queryViewModel.PublicationId); + Assert.Equal(publication.Id, queryViewModel.PublicationId); Assert.Equal(FullTableQuery.SubjectId, viewModel.Query.SubjectId); Assert.Equal(FullTableQuery.TimePeriod, viewModel.Query.TimePeriod); Assert.Equal(FullTableQuery.Filters, viewModel.Query.Filters); @@ -498,14 +581,29 @@ await TestApp.AddTestData(context => [Fact] public async Task QueryForFastTrack_DataBlockNotYetPublished() { + Publication publication = _dataFixture.DefaultPublication() + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1)]); + + var release = publication.Releases.Single(); + var releaseVersion = release.Versions.Single(); + + DataBlockParent dataBlockParentWithNoPublishedVersion = _dataFixture + .DefaultDataBlockParent() + .WithLatestDraftVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion)); + await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParentWithNoPublishedVersion)); + { + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParentWithNoPublishedVersion); + }); var client = SetupApp() .CreateClient(); var response = - await client.GetAsync($"/api/tablebuilder/fast-track/{DataBlockParentWithNoPublishedVersion.Id}"); + await client.GetAsync($"/api/tablebuilder/fast-track/{dataBlockParentWithNoPublishedVersion.Id}"); VerifyAllMocks(BlobCacheService); @@ -515,19 +613,43 @@ await TestApp.AddTestData(context => [Fact] public async Task QueryForFastTrack_NotLatestRelease() { - await TestApp.AddTestData(context => - context.DataBlockParents.Add(DataBlockParent)); + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases([ + _dataFixture + .DefaultRelease(publishedVersions: 1, year: 2020), + _dataFixture + .DefaultRelease(publishedVersions: 1, year: 2021) + ]); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + var release2021 = publication.Releases.Single(r => r.Year == 2021); + + // Release version is from the 2020 release which is not the latest release for the publication + var releaseVersion = release2020.Versions.Single(); + + DataBlockParent dataBlockParent = _dataFixture + .DefaultDataBlockParent() + .WithLatestPublishedVersion(_dataFixture + .DefaultDataBlockVersion() + .WithReleaseVersion(releaseVersion) + .WithDates(published: DateTime.UtcNow.AddDays(-1)) + .WithQuery(FullTableQuery) + .WithTable(TableConfiguration) + .WithCharts(Charts)); + + var dataBlockId = dataBlockParent.LatestPublishedVersion!.Id; - var latestReleaseVersion = new ReleaseVersion + await TestApp.AddTestData(context => { - Id = Guid.NewGuid(), - ReleaseName = "2021", - TimePeriodCoverage = AcademicYear - }; + context.Publications.Add(publication); + context.DataBlockParents.Add(dataBlockParent); + }); - var cacheKey = new DataBlockTableResultCacheKey(publicationSlug: ReleaseVersion.Publication.Slug, - releaseSlug: ReleaseVersion.Slug, - DataBlockParentId); + var cacheKey = new DataBlockTableResultCacheKey( + publicationSlug: publication.Slug, + releaseSlug: release2020.Slug, + dataBlockParent.Id); BlobCacheService .Setup(s => s.GetItemAsync(cacheKey, typeof(TableBuilderResultViewModel))) @@ -540,37 +662,32 @@ await TestApp.AddTestData(context => var dataBlockService = new Mock(Strict); dataBlockService - .Setup(s => s.GetDataBlockTableResult(ReleaseVersionId, DataBlockId, null)) + .Setup(s => s.GetDataBlockTableResult(releaseVersion.Id, dataBlockId, null)) .ReturnsAsync(_tableBuilderResults); - var releaseVersionRepository = new Mock(Strict); - - releaseVersionRepository - .Setup(s => s.GetLatestPublishedReleaseVersion(PublicationId, default)) - .ReturnsAsync(latestReleaseVersion); - var client = SetupApp( - dataBlockService: dataBlockService.Object, - releaseVersionRepository: releaseVersionRepository.Object + dataBlockService: dataBlockService.Object ) .CreateClient(); - var response = await client.GetAsync($"/api/tablebuilder/fast-track/{DataBlockParentId}"); + var response = await client.GetAsync($"/api/tablebuilder/fast-track/{dataBlockParent.Id}"); - VerifyAllMocks(BlobCacheService, dataBlockService, releaseVersionRepository); + VerifyAllMocks( + BlobCacheService, + dataBlockService + ); var viewModel = response.AssertOk(); - Assert.Equal(DataBlockParentId, viewModel.DataBlockParentId); - Assert.Equal(ReleaseVersionId, viewModel.ReleaseId); - Assert.Equal(ReleaseVersion.Slug, viewModel.ReleaseSlug); - Assert.Equal(ReleaseVersion.Type, viewModel.ReleaseType); + Assert.Equal(dataBlockParent.Id, viewModel.DataBlockParentId); + Assert.Equal(releaseVersion.Id, viewModel.ReleaseId); + Assert.Equal(release2020.Slug, viewModel.ReleaseSlug); + Assert.Equal(releaseVersion.Type, viewModel.ReleaseType); Assert.False(viewModel.LatestData); - Assert.Equal("Academic year 2021/22", viewModel.LatestReleaseTitle); + Assert.Equal(release2021.Title, viewModel.LatestReleaseTitle); } private WebApplicationFactory SetupApp( IDataBlockService? dataBlockService = null, - IReleaseVersionRepository? releaseVersionRepository = null, ITableBuilderService? tableBuilderService = null) { return TestApp @@ -580,8 +697,6 @@ private WebApplicationFactory SetupApp( services.ReplaceService(BlobCacheService); services.AddTransient(_ => dataBlockService ?? Mock.Of(Strict)); - services.AddTransient(_ => - releaseVersionRepository ?? Mock.Of(Strict)); services.AddTransient(_ => tableBuilderService ?? Mock.Of(Strict)); } ); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs index 6f76f1c2af1..6f6e13bac06 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Api/Controllers/TableBuilderController.cs @@ -1,12 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Cache; using GovUk.Education.ExploreEducationStatistics.Common.Cancellation; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Requests; -using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Data.Api.Cache; using GovUk.Education.ExploreEducationStatistics.Data.Api.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Data.Api.ViewModels; @@ -15,39 +18,22 @@ using GovUk.Education.ExploreEducationStatistics.Data.ViewModels.Meta; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using static GovUk.Education.ExploreEducationStatistics.Common.Cancellation.RequestTimeoutConfigurationKeys; namespace GovUk.Education.ExploreEducationStatistics.Data.Api.Controllers { [Route("api")] [ApiController] - public class TableBuilderController : ControllerBase + public class TableBuilderController( + ContentDbContext contextDbContext, + IDataBlockService dataBlockService, + ITableBuilderService tableBuilderService) + : ControllerBase { // Change this whenever there is a breaking change // that requires cache invalidation. public const string ApiVersion = "1"; - private readonly IPersistenceHelper _contentPersistenceHelper; - private readonly IDataBlockService _dataBlockService; - private readonly IReleaseVersionRepository _releaseVersionRepository; - private readonly ITableBuilderService _tableBuilderService; - - public TableBuilderController( - IPersistenceHelper contentPersistenceHelper, - IDataBlockService dataBlockService, - IReleaseVersionRepository releaseVersionRepository, - ITableBuilderService tableBuilderService) - { - _contentPersistenceHelper = contentPersistenceHelper; - _dataBlockService = dataBlockService; - _releaseVersionRepository = releaseVersionRepository; - _tableBuilderService = tableBuilderService; - } - [HttpPost("tablebuilder")] [Produces("application/json", "text/csv")] [CancellationTokenTimeout(TableBuilderQuery)] @@ -59,7 +45,7 @@ public async Task Query( { Response.ContentDispositionAttachment(ContentTypes.Csv); - return await _tableBuilderService.QueryToCsvStream( + return await tableBuilderService.QueryToCsvStream( query: request.AsFullTableQuery(), stream: Response.BodyWriter.AsStream(), cancellationToken: cancellationToken @@ -67,7 +53,7 @@ public async Task Query( .HandleFailuresOrNoOp(); } - return await _tableBuilderService + return await tableBuilderService .Query(request.AsFullTableQuery(), cancellationToken) .HandleFailuresOr(Ok); } @@ -86,7 +72,7 @@ public async Task Query( contentType: ContentTypes.Csv, filename: $"{releaseVersionId}.csv"); - return await _tableBuilderService.QueryToCsvStream( + return await tableBuilderService.QueryToCsvStream( releaseVersionId: releaseVersionId, query: request.AsFullTableQuery(), stream: Response.BodyWriter.AsStream(), @@ -95,7 +81,7 @@ public async Task Query( .HandleFailuresOrNoOp(); } - return await _tableBuilderService + return await tableBuilderService .Query(releaseVersionId, request.AsFullTableQuery(), boundaryLevelId: null, cancellationToken) .HandleFailuresOr(Ok); } @@ -147,9 +133,14 @@ public async Task> QueryForFastTrack(Guid dataB { return await GetLatestPublishedDataBlockVersion(dataBlockParentId) .OnSuccessCombineWith(dataBlockVersion => GetDataBlockTableResult(dataBlockVersion)) - .OnSuccessCombineWith(tuple => - _releaseVersionRepository.GetLatestPublishedReleaseVersion(tuple.Item1.ReleaseVersion.PublicationId) - .OrNotFound()) + .OnSuccessCombineWith(async tuple => + { + var (dataBlockVersion, _) = tuple; + return await contextDbContext.Publications + .Where(p => p.Id == dataBlockVersion.ReleaseVersion.PublicationId) + .Select(p => p.LatestPublishedReleaseVersion) + .SingleOrNotFoundAsync(); + }) .OnSuccess(tuple => { var (dataBlockVersion, tableResult, latestReleaseVersion) = tuple; @@ -163,7 +154,7 @@ private Task> GetDataBlockTabl DataBlockVersion dataBlockVersion, long? boundaryLevelId = null) { - return _dataBlockService.GetDataBlockTableResult( + return dataBlockService.GetDataBlockTableResult( releaseVersionId: dataBlockVersion.ReleaseVersionId, dataBlockVersionId: dataBlockVersion.Id, boundaryLevelId); @@ -174,7 +165,7 @@ private Task> GetLatestPublishedDataBlockVersion(Guid dataBlockParentId) + private async Task> GetLatestPublishedDataBlockVersion( + Guid dataBlockParentId) { - return _contentPersistenceHelper - .CheckEntityExists(dataBlockParentId, q => q - .Include(dataBlockParent => dataBlockParent.LatestPublishedVersion) - .ThenInclude(dataBlockVersion => dataBlockVersion.ReleaseVersion) - .ThenInclude(releaseVersion => releaseVersion.Publication)) - .OnSuccess(dataBlock => dataBlock.LatestPublishedVersion) + return await contextDbContext.DataBlockParents + .Include(dbp => dbp.LatestPublishedVersion) + .ThenInclude(dbv => dbv.ReleaseVersion) + .ThenInclude(rv => rv.Publication) + .SingleOrNotFoundAsync(dbp => dbp.Id == dataBlockParentId) + .OnSuccess(dbp => dbp.LatestPublishedVersion) .OrNotFound(); } } From 2d04ca5559609378f62fe659fe0424fa4f8bcc5f Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Mon, 16 Dec 2024 17:43:05 +0000 Subject: [PATCH 25/40] EES-5656 On completing publishing, set the latest published release version for publication based on the latest release with a published version --- .../ReleaseVersionRepositoryTests.cs | 133 +++--------------- .../Interfaces/IReleaseVersionRepository.cs | 10 -- .../Repository/ReleaseVersionRepository.cs | 10 -- .../Services/PublishingCompletionService.cs | 41 ++++-- 4 files changed, 49 insertions(+), 145 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs index d4446fa2cc7..3d410fc849a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs @@ -21,92 +21,6 @@ public class GetLatestPublishedReleaseVersionTests : ReleaseVersionRepositoryTes { [Fact] public async Task Success() - { - var publications = _dataFixture - .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021), - _dataFixture - .DefaultRelease(publishedVersions: 2, year: 2020))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - var result = await repository.GetLatestPublishedReleaseVersion(publications[0].Id); - - // Expect the result to be the latest published version taken from releases of the specified publication in - // reverse chronological order - var expectedReleaseVersion = publications[0].ReleaseVersions - .Single(rv => rv is { Published: not null, Year: 2021, Version: 1 }); - - Assert.NotNull(result); - Assert.Equal(expectedReleaseVersion.Id, result.Id); - } - - [Fact] - public async Task PublicationHasNoPublishedReleaseVersions_ReturnsNull() - { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has an unpublished release version - // Index 1 has a published release version - .ForIndex(0, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1))) - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id)); - } - - [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsNull() - { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id)); - } - - [Fact] - public async Task PublicationDoesNotExist_ReturnsNull() - { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); - - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.Null(await repository.GetLatestPublishedReleaseVersion(Guid.NewGuid())); - } - - [Fact] - public async Task SpecificReleaseSlug_Success() { var publications = _dataFixture .DefaultPublication() @@ -121,7 +35,7 @@ public async Task SpecificReleaseSlug_Success() await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.GetLatestPublishedReleaseVersion(publications[0].Id, "2021-22"); + var result = await repository.GetLatestPublishedReleaseVersion(publications[0].Id, releaseSlug: "2021-22"); // Expect the result to be the latest published version for the 2021-22 release of the specified publication var expectedReleaseVersion = publications[0].ReleaseVersions @@ -132,76 +46,70 @@ public async Task SpecificReleaseSlug_Success() } [Fact] - public async Task SpecificReleaseSlug_PublicationHasNoPublishedReleaseVersions_ReturnsNull() + public async Task PublicationHasNoPublishedReleaseVersions_ReturnsNull() { var publications = _dataFixture .DefaultPublication() // Index 0 has an unpublished release version // Index 1 has a published release version - .ForIndex(0, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021) - .Generate(1))) - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1, year: 2021) - .Generate(1))) + .ForIndex(0, p => p.SetReleases([ + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021) + ])) + .ForIndex(1, p => p.SetReleases([ + _dataFixture.DefaultRelease(publishedVersions: 1, year: 2021) + ])) .GenerateList(2); var contextId = await AddTestData(publications); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, releaseSlug: "2021-22")); } [Fact] - public async Task SpecificReleaseSlug_PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNull() + public async Task PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNull() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1, year: 2020) - .Generate(1)); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2020)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22")); } [Fact] - public async Task SpecificReleaseSlug_PublicationHasNoReleaseVersions_ReturnsNull() + public async Task PublicationHasNoReleaseVersions_ReturnsNull() { var publications = _dataFixture .DefaultPublication() // Index 0 has no release versions // Index 1 has a published release version - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1, year: 2021) - .Generate(1))) + .ForIndex(1, p => p.SetReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2021)])) .GenerateList(2); var contextId = await AddTestData(publications); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, releaseSlug: "2021-22")); } [Fact] - public async Task SpecificReleaseSlug_PublicationDoesNotExist_ReturnsNull() + public async Task PublicationDoesNotExist_ReturnsNull() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1, year: 2021) - .Generate(1)); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2021)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(Guid.NewGuid(), "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersion(Guid.NewGuid(), releaseSlug: "2021-22")); } } @@ -243,9 +151,8 @@ public async Task PublicationHasNoReleaseVersions_ReturnsNull() .DefaultPublication() // Index 0 has no release versions // Index 1 has a release version - .ForIndex(1, p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) + .ForIndex(1, p => p.SetReleases([ + _dataFixture.DefaultRelease(publishedVersions: 1)])) .GenerateList(2); var contextId = await AddTestData(publications); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs index 8cc5dd3d21e..8a01061242c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs @@ -12,16 +12,6 @@ Task GetPublishedDate( Guid releaseVersionId, DateTime actualPublishedDate); - /// - /// Retrieves the latest published version from all releases in reverse chronological order that are associated with a publication. - /// - /// The unique identifier of the publication. - /// A to observe while waiting for the task to complete. - /// The latest published version from all releases in reverse chronological order that are associated with a publication. - Task GetLatestPublishedReleaseVersion( - Guid publicationId, - CancellationToken cancellationToken = default); - /// /// Retrieves the latest published version of a release matching a given slug associated with a publication. /// diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs index 6a167df5e37..f07444f4340 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs @@ -54,16 +54,6 @@ await _contentDbContext.Entry(releaseVersion) return releaseVersion.PreviousVersion.Published.Value; } - public async Task GetLatestPublishedReleaseVersion( - Guid publicationId, - CancellationToken cancellationToken = default) - { - return (await _contentDbContext.ReleaseVersions.LatestReleaseVersions(publicationId, publishedOnly: true) - .ToListAsync(cancellationToken: cancellationToken)) - .OrderByReverseChronologicalOrder() - .FirstOrDefault(); - } - public async Task GetLatestReleaseVersion( Guid publicationId, CancellationToken cancellationToken = default) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs index 2269a2b9363..48728b8416b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Predicates; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces; @@ -18,7 +18,6 @@ public class PublishingCompletionService( INotificationsService notificationsService, IReleasePublishingStatusService releasePublishingStatusService, IPublicationCacheService publicationCacheService, - IReleaseVersionRepository releaseVersionRepository, IReleaseService releaseService, IRedirectsCacheService redirectsCacheService, IDataSetPublishingService dataSetPublishingService) @@ -29,10 +28,7 @@ public async Task CompletePublishingIfAllPriorStagesComplete( { var releaseStatuses = await releasePublishingKeys .ToAsyncEnumerable() - .SelectAwait(async key => - { - return await releasePublishingStatusService.Get(key); - }) + .SelectAwait(async key => await releasePublishingStatusService.Get(key)) .ToListAsync(); var prePublishingStagesComplete = releaseStatuses @@ -72,7 +68,6 @@ await releaseVersionIdsToUpdate foreach (var methodologyVersion in methodologyVersions) { - // WARN: This must be called before PublicationRepository#UpdateLatestPublishedRelease if (await methodologyService.IsBeingPublishedAlongsideRelease(methodologyVersion, releaseVersion)) { await methodologyService.Publish(methodologyVersion); @@ -89,7 +84,7 @@ await releaseVersionIdsToUpdate await directlyRelatedPublicationIds .ToAsyncEnumerable() - .ForEachAwaitAsync(UpdateLatestPublishedRelease); + .ForEachAwaitAsync(UpdateLatestPublishedReleaseVersionForPublication); // Update the cached publication and any cached superseded publications. // If this is the first live release of the publication, the superseding is now enforced @@ -124,14 +119,36 @@ await releasePublishingStatusService .UpdatePublishingStage(status.AsTableRowKey(), ReleasePublishingStatusPublishingStage.Complete)); } - private async Task UpdateLatestPublishedRelease(Guid publicationId) + private async Task UpdateLatestPublishedReleaseVersionForPublication(Guid publicationId) { var publication = await contentDbContext.Publications .SingleAsync(p => p.Id == publicationId); - var latestPublishedReleaseVersion = - await releaseVersionRepository.GetLatestPublishedReleaseVersion(publicationId); - publication.LatestPublishedReleaseVersionId = latestPublishedReleaseVersion!.Id; + // Get the publications release id's by the order they appear in the release series + var releaseSeriesReleaseIds = publication.ReleaseSeries + .Where(rsi => !rsi.IsLegacyLink) + .Select(rs => rs.ReleaseId!.Value) + .ToList(); + + // Work out the publication's new latest published release version. + // This is the latest published version of the first release which has a published version + Guid? latestPublishedReleaseVersionId = null; + foreach (var releaseId in releaseSeriesReleaseIds) + { + latestPublishedReleaseVersionId = (await contentDbContext.ReleaseVersions + .LatestReleaseVersion(releaseId: releaseId, publishedOnly: true) + .SingleOrDefaultAsync())?.Id; + + if (latestPublishedReleaseVersionId != null) + { + break; + } + } + + publication.LatestPublishedReleaseVersionId = + latestPublishedReleaseVersionId ?? + throw new InvalidOperationException( + $"No latest published release version found for publication {publicationId}"); contentDbContext.Update(publication); await contentDbContext.SaveChangesAsync(); From f3012b1a60743c5929bb03d5169a7e1a7baf7786 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Tue, 17 Dec 2024 16:05:04 +0000 Subject: [PATCH 26/40] EES-5656 Refactor logic to extract release id's from a release series to be reusable. --- .../Extensions/ReleaseSeriesItemExtensions.cs | 23 +++++++++++++++++++ .../Services/PublishingCompletionService.cs | 6 ++--- 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs new file mode 100644 index 00000000000..3581291c11a --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; + +public static class ReleaseSeriesItemExtensions +{ + /// + /// Retrieves the release id's by the order they appear in a of type , + /// ignoring any legacy links. + /// + /// + /// A of type containing the release id's in the order + /// they appear in the release series. + public static IReadOnlyList ReleaseIds(this List releaseSeriesItems) + { + return releaseSeriesItems + .Where(rsi => !rsi.IsLegacyLink) + .Select(rs => rs.ReleaseId!.Value) + .ToList(); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs index 48728b8416b..9bd2031ff20 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; using GovUk.Education.ExploreEducationStatistics.Content.Model.Predicates; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Publisher.Model; @@ -125,10 +126,7 @@ private async Task UpdateLatestPublishedReleaseVersionForPublication(Guid public .SingleAsync(p => p.Id == publicationId); // Get the publications release id's by the order they appear in the release series - var releaseSeriesReleaseIds = publication.ReleaseSeries - .Where(rsi => !rsi.IsLegacyLink) - .Select(rs => rs.ReleaseId!.Value) - .ToList(); + var releaseSeriesReleaseIds = publication.ReleaseSeries.ReleaseIds(); // Work out the publication's new latest published release version. // This is the latest published version of the first release which has a published version From 9c873761b94c160a4eccf1151cd9b54d477536bc Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Wed, 18 Dec 2024 08:26:29 +0000 Subject: [PATCH 27/40] EES-5656 Update methods of ReleaseVersionRepository to use release series --- .../ReleaseVersionRepositoryTests.cs | 618 +++++++++--------- .../Extensions/ReleaseSeriesItemExtensions.cs | 26 +- .../Interfaces/IReleaseVersionRepository.cs | 8 +- .../Repository/ReleaseVersionRepository.cs | 41 +- 4 files changed, 375 insertions(+), 318 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs index 3d410fc849a..88ecdddf474 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs @@ -7,7 +7,6 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using Xunit; -using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Utils.ComparerUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils.ContentDbUtils; @@ -22,57 +21,54 @@ public class GetLatestPublishedReleaseVersionTests : ReleaseVersionRepositoryTes [Fact] public async Task Success() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 1, year: 2022), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases( + [ + _dataFixture.DefaultRelease(publishedVersions: 1, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021) + ]); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.GetLatestPublishedReleaseVersion(publications[0].Id, releaseSlug: "2021-22"); + var result = await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22"); - // Expect the result to be the latest published version for the 2021-22 release of the specified publication - var expectedReleaseVersion = publications[0].ReleaseVersions - .Single(rv => rv is { Published: not null, Year: 2021, Version: 1 }); + // Expect the result to be the latest published version for the 2021-22 release + var expectedReleaseVersion = publication.Releases.Single(r => r is { Year: 2021 }).Versions[1]; Assert.NotNull(result); Assert.Equal(expectedReleaseVersion.Id, result.Id); } [Fact] - public async Task PublicationHasNoPublishedReleaseVersions_ReturnsNull() + public async Task MultiplePublications_ReturnsReleaseVersionAssociatedWithPublication() { - var publications = _dataFixture + var (publication1, publication2) = _dataFixture .DefaultPublication() - // Index 0 has an unpublished release version - // Index 1 has a published release version - .ForIndex(0, p => p.SetReleases([ - _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021) - ])) - .ForIndex(1, p => p.SetReleases([ - _dataFixture.DefaultRelease(publishedVersions: 1, year: 2021) - ])) - .GenerateList(2); + .WithReleases(_ => [_dataFixture.DefaultRelease(publishedVersions: 1, year: 2021)]) + .GenerateTuple2(); - var contextId = await AddTestData(publications); + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, releaseSlug: "2021-22")); + var result = await repository.GetLatestPublishedReleaseVersion(publication1.Id, releaseSlug: "2021-22"); + + // Expect the result to be from the specified publication + var expectedReleaseVersion = publication1.Releases.Single().Versions.Single(); + + Assert.NotNull(result); + Assert.Equal(expectedReleaseVersion.Id, result.Id); } [Fact] - public async Task PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNull() + public async Task PublicationHasNoPublishedReleaseVersions_ReturnsNull() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2020)]); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); @@ -82,34 +78,38 @@ public async Task PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNu } [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsNull() + public async Task PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNull() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, p => p.SetReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2021)])) - .GenerateList(2); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2020)]); - var contextId = await AddTestData(publications); + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, releaseSlug: "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22")); } [Fact] - public async Task PublicationDoesNotExist_ReturnsNull() + public async Task PublicationHasNoReleaseVersions_ReturnsNull() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2021)]); + Publication publication = _dataFixture.DefaultPublication(); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(Guid.NewGuid(), releaseSlug: "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22")); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsNull() + { + var repository = BuildRepository(); + + Assert.Null(await repository.GetLatestPublishedReleaseVersion(publicationId: Guid.NewGuid(), + releaseSlug: "2021-22")); } } @@ -118,64 +118,68 @@ public class GetLatestReleaseVersionTests : ReleaseVersionRepositoryTests [Fact] public async Task Success() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021), - _dataFixture - .DefaultRelease(publishedVersions: 2, year: 2020))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases( + [ + _dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _dataFixture.DefaultRelease(publishedVersions: 2, year: 2020) + ]) + .FinishWith(p => p.ReleaseSeries = GenerateReleaseSeries(p.Releases, 2021, 2020, 2022)); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.GetLatestReleaseVersion(publications[0].Id); + var result = await repository.GetLatestReleaseVersion(publication.Id); - // Expect the result to be the latest version taken from releases of the specified publication in - // reverse chronological order - var expectedReleaseVersion = publications[0].ReleaseVersions - .Single(rv => rv is { Year: 2022, Version: 0 }); + // Expect the result to be the latest version of the latest release in the release series + var expectedReleaseVersion = publication.Releases.Single(r => r is { Year: 2021 }).Versions[0]; Assert.NotNull(result); Assert.Equal(expectedReleaseVersion.Id, result.Id); } [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsNull() + public async Task MultiplePublications_ReturnsReleaseVersionAssociatedWithPublication() { - var publications = _dataFixture + var (publication1, publication2) = _dataFixture .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a release version - .ForIndex(1, p => p.SetReleases([ - _dataFixture.DefaultRelease(publishedVersions: 1)])) - .GenerateList(2); + .WithReleases(_ => [_dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true)]) + .GenerateTuple2(); - var contextId = await AddTestData(publications); + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestReleaseVersion(publications[0].Id)); + var result = await repository.GetLatestReleaseVersion(publication1.Id); + + // Expect the result to be from the specified publication + var expectedReleaseVersion = publication1.Releases.Single().Versions.Single(); + + Assert.NotNull(result); + Assert.Equal(expectedReleaseVersion.Id, result.Id); } [Fact] - public async Task PublicationDoesNotExist_ReturnsNull() + public async Task PublicationHasNoReleaseVersions_ReturnsNull() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + Publication publication = _dataFixture.DefaultPublication(); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestReleaseVersion(Guid.NewGuid())); + Assert.Null(await repository.GetLatestReleaseVersion(publication.Id)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsNull() + { + var repository = BuildRepository(); + + Assert.Null(await repository.GetLatestReleaseVersion(publicationId: Guid.NewGuid())); } } @@ -186,20 +190,18 @@ public async Task Success() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true) - .Generate(1)); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var releaseVersions = publication.ReleaseVersions; + var release = publication.Releases.Single(); - // Expect only the highest published version of the release to be the latest - Assert.False(await repository.IsLatestPublishedReleaseVersion(releaseVersions[0].Id)); - Assert.True(await repository.IsLatestPublishedReleaseVersion(releaseVersions[1].Id)); - Assert.False(await repository.IsLatestPublishedReleaseVersion(releaseVersions[2].Id)); + // Expect only the latest published version of the release to be returned as the latest + Assert.False(await repository.IsLatestPublishedReleaseVersion(release.Versions[0].Id)); + Assert.True(await repository.IsLatestPublishedReleaseVersion(release.Versions[1].Id)); + Assert.False(await repository.IsLatestPublishedReleaseVersion(release.Versions[2].Id)); } [Fact] @@ -207,15 +209,13 @@ public async Task ReleaseHasNoPublishedReleaseVersions_ReturnsFalse() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1)); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var releaseVersion = publication.ReleaseVersions.Single(); + var releaseVersion = publication.Releases.Single().Versions.Single(); Assert.False(await repository.IsLatestPublishedReleaseVersion(releaseVersion.Id)); } @@ -223,17 +223,9 @@ public async Task ReleaseHasNoPublishedReleaseVersions_ReturnsFalse() [Fact] public async Task ReleaseVersionDoesNotExist_ReturnsFalse() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + var repository = BuildRepository(); - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.False(await repository.IsLatestPublishedReleaseVersion(Guid.NewGuid())); + Assert.False(await repository.IsLatestPublishedReleaseVersion(releaseVersionId: Guid.NewGuid())); } } @@ -244,36 +236,26 @@ public async Task Success() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true) - .Generate(1)); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var releaseVersions = publication.ReleaseVersions; + var release = publication.Releases.Single(); - // Expect only the highest version of the release to be the latest - Assert.False(await repository.IsLatestReleaseVersion(releaseVersions[0].Id)); - Assert.False(await repository.IsLatestReleaseVersion(releaseVersions[1].Id)); - Assert.True(await repository.IsLatestReleaseVersion(releaseVersions[2].Id)); + // Expect only the latest draft version of the release to be returned as the latest + Assert.False(await repository.IsLatestReleaseVersion(release.Versions[0].Id)); + Assert.False(await repository.IsLatestReleaseVersion(release.Versions[1].Id)); + Assert.True(await repository.IsLatestReleaseVersion(release.Versions[2].Id)); } [Fact] public async Task ReleaseVersionDoesNotExist_ReturnsFalse() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); - - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); + var repository = BuildRepository(); - Assert.False(await repository.IsLatestReleaseVersion(Guid.NewGuid())); + Assert.False(await repository.IsLatestReleaseVersion(releaseVersionId: Guid.NewGuid())); } } @@ -284,29 +266,56 @@ public class AnyPublishedStateTests : ListLatestReleaseVersionIdsTests [Fact] public async Task Success() { - var publications = _dataFixture + Publication publication = _dataFixture + .DefaultPublication() + .WithReleases( + [ + _dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _dataFixture.DefaultRelease(publishedVersions: 2, year: 2020) + ]) + .FinishWith(p => p.ReleaseSeries = GenerateReleaseSeries(p.Releases, 2021, 2020, 2022)); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await ListLatestReleaseVersions(repository, publication.Id); + + // Expect the latest versions of each release, ordered by release series + Guid[] expectedReleaseVersionIds = + [ + publication.Releases.Single(r => r is { Year: 2021 }).Versions[0].Id, + publication.Releases.Single(r => r is { Year: 2020 }).Versions[1].Id, + publication.Releases.Single(r => r is { Year: 2022 }).Versions[2].Id + ]; + + Assert.Equal(expectedReleaseVersionIds, result.Select(rv => rv.Id)); + } + + [Fact] + public async Task MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() + { + var (publication1, publication2) = _dataFixture .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases(_ => + [ + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021) + ]) + .GenerateTuple2(); + + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: false); + var result = await ListLatestReleaseVersions(repository, publication1.Id); - // Expect the result to contain the highest version of each release for the specified publication + // Expect the results to be from the specified publication AssertIdsAreEqualIgnoringOrder( [ - publications[0].ReleaseVersions[0].Id, - publications[0].ReleaseVersions[3].Id, - publications[0].ReleaseVersions[5].Id + publication1.Releases.Single(r => r is { Year: 2022 }).Versions[0].Id, + publication1.Releases.Single(r => r is { Year: 2021 }).Versions[0].Id ], result); } @@ -314,68 +323,85 @@ public async Task Success() [Fact] public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + Publication publication = _dataFixture.DefaultPublication(); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: false)); + Assert.Empty(await ListLatestReleaseVersions(repository, publication.Id)); } [Fact] public async Task PublicationDoesNotExist_ReturnsEmpty() + { + var repository = BuildRepository(); + + Assert.Empty(await ListLatestReleaseVersions(repository, publicationId: Guid.NewGuid())); + } + + private static async Task> ListLatestReleaseVersions( + ReleaseVersionRepository repository, + Guid publicationId) + { + return await repository.ListLatestReleaseVersions(publicationId, publishedOnly: false); + } + } + + public class PublishedOnlyTests : ListLatestReleaseVersionsTests + { + [Fact] + public async Task Success() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases( + [ + _dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _dataFixture.DefaultRelease(publishedVersions: 2, year: 2020) + ]) + .FinishWith(p => p.ReleaseSeries = GenerateReleaseSeries(p.Releases, 2021, 2020, 2022)); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersions(publicationId: Guid.NewGuid(), - publishedOnly: false)); + var result = await ListLatestReleaseVersions(repository, publication.Id); + + // Expect the latest published version of each release, ordered by release series + Guid[] expectedReleaseVersionIds = + [ + publication.Releases.Single(r => r is { Year: 2020 }).Versions[1].Id, + publication.Releases.Single(r => r is { Year: 2022 }).Versions[1].Id + ]; + + Assert.Equal(expectedReleaseVersionIds, result.Select(rv => rv.Id)); } - } - public class PublishedOnlyTests : ListLatestReleaseVersionsTests - { [Fact] - public async Task Success() + public async Task MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() { - var publications = _dataFixture + var (publication1, publication2) = _dataFixture .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases(_ => + [ + _dataFixture.DefaultRelease(publishedVersions: 1, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 1, year: 2021) + ]) + .GenerateTuple2(); + + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: true); + var result = await ListLatestReleaseVersions(repository, publication1.Id); - // Expect the result to contain the highest published version of each release for the specified publication + // Expect the results to be from the specified publication AssertIdsAreEqualIgnoringOrder( [ - publications[0].ReleaseVersions[2].Id, - publications[0].ReleaseVersions[5].Id + publication1.Releases.Single(r => r is { Year: 2022 }).Versions[0].Id, + publication1.Releases.Single(r => r is { Year: 2021 }).Versions[0].Id ], result); } @@ -383,62 +409,42 @@ public async Task Success() [Fact] public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - // Index 0 has an unpublished release version - // Index 1 has a published release version - .ForIndex(0, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1))) - .ForIndex(1, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true)]); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: true)); + Assert.Empty(await ListLatestReleaseVersions(repository, publication.Id)); } [Fact] public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + Publication publication = _dataFixture.DefaultPublication(); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersions(publications[0].Id, publishedOnly: true)); + Assert.Empty(await ListLatestReleaseVersions(repository, publication.Id)); } [Fact] public async Task PublicationDoesNotExist_ReturnsEmpty() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + var repository = BuildRepository(); - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); + Assert.Empty(await ListLatestReleaseVersions(repository, publicationId: Guid.NewGuid())); + } - Assert.Empty( - await repository.ListLatestReleaseVersions(publicationId: Guid.NewGuid(), publishedOnly: true)); + private static async Task> ListLatestReleaseVersions( + ReleaseVersionRepository repository, + Guid publicationId) + { + return await repository.ListLatestReleaseVersions(publicationId, publishedOnly: true); } } } @@ -450,163 +456,194 @@ public class AnyPublishedStateTests : ListLatestReleaseVersionIdsTests [Fact] public async Task Success() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases( + [ + _dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _dataFixture.DefaultRelease(publishedVersions: 2, year: 2020) + ]); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: false); + var result = await ListLatestReleaseVersionIds(repository, publication.Id); - // Expect the result to contain the highest version of each release for the specified publication + // Expect the latest version id's of each release AssertIdsAreEqualIgnoringOrder( [ - publications[0].ReleaseVersions[0].Id, - publications[0].ReleaseVersions[3].Id, - publications[0].ReleaseVersions[5].Id + publication.Releases.Single(r => r is { Year: 2022 }).Versions[2].Id, + publication.Releases.Single(r => r is { Year: 2021 }).Versions[0].Id, + publication.Releases.Single(r => r is { Year: 2020 }).Versions[1].Id ], result); } [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + public async Task MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() { - var publications = _dataFixture + var (publication1, publication2) = _dataFixture .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a published release version - .ForIndex(1, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases(_ => + [ + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021) + ]) + .GenerateTuple2(); + + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: false)); + var result = await ListLatestReleaseVersionIds(repository, publication1.Id); + + // Expect the results to be from the specified publication + AssertIdsAreEqualIgnoringOrder( + [ + publication1.Releases.Single(r => r is { Year: 2022 }).Versions[0].Id, + publication1.Releases.Single(r => r is { Year: 2021 }).Versions[0].Id + ], + result); } [Fact] - public async Task PublicationDoesNotExist_ReturnsEmpty() + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + Publication publication = _dataFixture.DefaultPublication(); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersionIds(publicationId: Guid.NewGuid(), - publishedOnly: false)); + Assert.Empty(await ListLatestReleaseVersionIds(repository, publication.Id)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsEmpty() + { + var repository = BuildRepository(); + + Assert.Empty(await ListLatestReleaseVersionIds(repository, publicationId: Guid.NewGuid())); + } + + private static async Task> ListLatestReleaseVersionIds( + ReleaseVersionRepository repository, + Guid publicationId) + { + return await repository.ListLatestReleaseVersionIds(publicationId, publishedOnly: false); } } - public class PublishedOnlyTrueTests : ListLatestReleaseVersionIdsTests + public class PublishedOnlyTests : ListLatestReleaseVersionIdsTests { [Fact] public async Task Success() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_ => ListOf( - _dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2, draftVersion: true), - _dataFixture - .DefaultRelease(publishedVersions: 2))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases( + [ + _dataFixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _dataFixture.DefaultRelease(publishedVersions: 2, year: 2020) + ]); + + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: true); + var result = await ListLatestReleaseVersionIds(repository, publication.Id); - // Expect the result to contain the highest published version of each release for the specified publication + // Expect the latest published version id's of each release AssertIdsAreEqualIgnoringOrder( [ - publications[0].ReleaseVersions[2].Id, - publications[0].ReleaseVersions[5].Id + publication.Releases.Single(r => r is { Year: 2022 }).Versions[1].Id, + publication.Releases.Single(r => r is { Year: 2020 }).Versions[1].Id ], result); } [Fact] - public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() + public async Task MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() { - var publications = _dataFixture + var (publication1, publication2) = _dataFixture .DefaultPublication() - // Index 0 has an unpublished release version - // Index 1 has a published release version - .ForIndex(0, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 0, draftVersion: true) - .Generate(1))) - .ForIndex(1, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); - - var contextId = await AddTestData(publications); + .WithReleases(_ => + [ + _dataFixture.DefaultRelease(publishedVersions: 1, year: 2022), + _dataFixture.DefaultRelease(publishedVersions: 1, year: 2021) + ]) + .GenerateTuple2(); + + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: true)); + var result = await ListLatestReleaseVersionIds(repository, publication1.Id); + + // Expect the results to be from the specified publication + AssertIdsAreEqualIgnoringOrder( + [ + publication1.Releases.Single(r => r is { Year: 2022 }).Versions[0].Id, + publication1.Releases.Single(r => r is { Year: 2021 }).Versions[0].Id + ], + result); } [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() { - var publications = _dataFixture + Publication publication = _dataFixture .DefaultPublication() - .ForIndex(1, - p => p.SetReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1))) - .GenerateList(2); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true)]); - var contextId = await AddTestData(publications); + var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty(await repository.ListLatestReleaseVersionIds(publications[0].Id, publishedOnly: true)); + Assert.Empty(await ListLatestReleaseVersionIds(repository, publication.Id)); } [Fact] - public async Task PublicationDoesNotExist_ReturnsEmpty() + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + Publication publication = _dataFixture.DefaultPublication(); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Empty( - await repository.ListLatestReleaseVersionIds(publicationId: Guid.NewGuid(), publishedOnly: true)); + Assert.Empty(await ListLatestReleaseVersionIds(repository, publication.Id)); + } + + [Fact] + public async Task PublicationDoesNotExist_ReturnsEmpty() + { + var repository = BuildRepository(); + + Assert.Empty(await ListLatestReleaseVersionIds(repository, publicationId: Guid.NewGuid())); + } + + private static async Task> ListLatestReleaseVersionIds( + ReleaseVersionRepository repository, + Guid publicationId) + { + return await repository.ListLatestReleaseVersionIds(publicationId, publishedOnly: true); } } } + private List GenerateReleaseSeries(IReadOnlyList releases, params int[] years) + { + return years.Select(year => + { + var release = releases.Single(r => r.Year == year); + return _dataFixture.DefaultReleaseSeriesItem().WithReleaseId(release.Id).Generate(); + }).ToList(); + } + private static void AssertIdsAreEqualIgnoringOrder( IReadOnlyCollection expectedIds, IReadOnlyCollection actualReleaseVersions) @@ -639,11 +676,10 @@ private static async Task AddTestData(IReadOnlyCollection p return contextId; } - private static ReleaseVersionRepository BuildRepository( - ContentDbContext contentDbContext) + private static ReleaseVersionRepository BuildRepository(ContentDbContext? contentDbContext = null) { return new ReleaseVersionRepository( - contentDbContext: contentDbContext + contentDbContext: contentDbContext ?? InMemoryContentDbContext() ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs index 3581291c11a..a7a09eec584 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; @@ -7,17 +8,26 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; public static class ReleaseSeriesItemExtensions { /// - /// Retrieves the release id's by the order they appear in a of type , + /// Retrieves the first release id in a of type , + /// ignoring any legacy links. + /// + /// + /// The first release id in the release series. + public static Guid? LatestReleaseId(this List releaseSeriesItems) => + SelectReleaseIds(releaseSeriesItems).FirstOrDefault(); + + /// + /// Retrieves the release id's in a of type , /// ignoring any legacy links. /// /// /// A of type containing the release id's in the order /// they appear in the release series. - public static IReadOnlyList ReleaseIds(this List releaseSeriesItems) - { - return releaseSeriesItems + public static IReadOnlyList ReleaseIds(this List releaseSeriesItems) => + SelectReleaseIds(releaseSeriesItems).ToList(); + + private static IEnumerable SelectReleaseIds(List releaseSeriesItems) => + releaseSeriesItems .Where(rsi => !rsi.IsLegacyLink) - .Select(rs => rs.ReleaseId!.Value) - .ToList(); - } + .Select(rs => rs.ReleaseId!.Value); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs index 8a01061242c..fb99630f1f0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs @@ -25,11 +25,11 @@ Task GetPublishedDate( CancellationToken cancellationToken = default); /// - /// Retrieves the latest version from all releases in reverse chronological order that are associated with a publication. + /// Retrieves the latest version of the latest release in release series order associated with a publication. /// /// The unique identifier of the publication. /// A to observe while waiting for the task to complete. - /// The latest version from all releases in reverse chronological order that are associated with a publication. + /// The latest version of the latest release in release series order associated with the publication. Task GetLatestReleaseVersion( Guid publicationId, CancellationToken cancellationToken = default); @@ -67,12 +67,12 @@ Task> ListLatestReleaseVersionIds( CancellationToken cancellationToken = default); /// - /// Retrieves the latest versions of all releases associated with a given publication in reverse chronological order. + /// Retrieves the latest versions of all releases in release series order associated with a given publication. /// /// The unique identifier of the publication. /// Flag to only include published release versions. /// A to observe while waiting for the task to complete. - /// A collection of the latest version id's of all releases associated with the publication. + /// A collection of the latest versions of all releases in release series order associated with the publication. Task> ListLatestReleaseVersions( Guid publicationId, bool publishedOnly = false, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs index f07444f4340..18ba9720d88 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; using GovUk.Education.ExploreEducationStatistics.Content.Model.Predicates; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Microsoft.EntityFrameworkCore; @@ -58,10 +59,16 @@ await _contentDbContext.Entry(releaseVersion) Guid publicationId, CancellationToken cancellationToken = default) { - return (await _contentDbContext.ReleaseVersions.LatestReleaseVersions(publicationId) - .ToListAsync(cancellationToken: cancellationToken)) - .OrderByReverseChronologicalOrder() - .FirstOrDefault(); + var latestReleaseId = await _contentDbContext.Publications + .Where(p => p.Id == publicationId) + .Select(p => p.ReleaseSeries.LatestReleaseId()) + .SingleOrDefaultAsync(cancellationToken: cancellationToken); + + return latestReleaseId.HasValue + ? await _contentDbContext.ReleaseVersions + .LatestReleaseVersion(releaseId: latestReleaseId.Value) + .SingleOrDefaultAsync(cancellationToken: cancellationToken) + : null; } public async Task GetLatestPublishedReleaseVersion( @@ -109,10 +116,24 @@ public async Task> ListLatestReleaseVersions( bool publishedOnly = false, CancellationToken cancellationToken = default) { + var publication = await _contentDbContext.Publications + .SingleOrDefaultAsync(p => p.Id == publicationId, cancellationToken: cancellationToken); + + if (publication == null) + { + return []; + } + + var publicationReleaseSeriesReleaseIds = publication.ReleaseSeries.ReleaseIds(); + + var releaseIdIndexMap = publicationReleaseSeriesReleaseIds + .Select((releaseId, index) => (releaseId, index)) + .ToDictionary(tuple => tuple.releaseId, tuple => tuple.index); + return (await _contentDbContext.ReleaseVersions .LatestReleaseVersions(publicationId, publishedOnly: publishedOnly) .ToListAsync(cancellationToken: cancellationToken)) - .OrderByReverseChronologicalOrder() + .OrderBy(rv => releaseIdIndexMap[rv.ReleaseId]) .ToList(); } @@ -156,13 +177,3 @@ private async Task IsLatestReleaseVersion( private record ReleaseIdVersion(Guid ReleaseId, int Version); } - -internal static class ReleaseVersionIEnumerableExtensions -{ - internal static IOrderedEnumerable OrderByReverseChronologicalOrder( - this IEnumerable query) - { - return query.OrderByDescending(releaseVersion => releaseVersion.Year) - .ThenByDescending(releaseVersion => releaseVersion.TimePeriodCoverage); - } -} From 2e744b2770a77b01b9a1fb2278f6a7fc74588c88 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 19 Dec 2024 12:15:27 +0000 Subject: [PATCH 28/40] EES-5656 Rename method GetLatestPublishedReleaseVersion to GetLatestPublishedReleaseVersionByReleaseSlug --- .../ReleaseVersionRepositoryTests.cs | 19 ++++++++++++------- .../Interfaces/IReleaseVersionRepository.cs | 2 +- .../Repository/ReleaseVersionRepository.cs | 4 ++-- .../ReleaseService.cs | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs index 88ecdddf474..54a95d1034a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs @@ -16,7 +16,7 @@ public class ReleaseVersionRepositoryTests { private readonly DataFixture _dataFixture = new(); - public class GetLatestPublishedReleaseVersionTests : ReleaseVersionRepositoryTests + public class GetLatestPublishedReleaseVersionByReleaseSlugTests : ReleaseVersionRepositoryTests { [Fact] public async Task Success() @@ -33,7 +33,8 @@ public async Task Success() await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22"); + var result = + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22"); // Expect the result to be the latest published version for the 2021-22 release var expectedReleaseVersion = publication.Releases.Single(r => r is { Year: 2021 }).Versions[1]; @@ -54,7 +55,8 @@ public async Task MultiplePublications_ReturnsReleaseVersionAssociatedWithPublic await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.GetLatestPublishedReleaseVersion(publication1.Id, releaseSlug: "2021-22"); + var result = + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication1.Id, releaseSlug: "2021-22"); // Expect the result to be from the specified publication var expectedReleaseVersion = publication1.Releases.Single().Versions.Single(); @@ -74,7 +76,8 @@ public async Task PublicationHasNoPublishedReleaseVersions_ReturnsNull() await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22")); + Assert.Null( + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22")); } [Fact] @@ -88,7 +91,8 @@ public async Task PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNu await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22")); + Assert.Null( + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22")); } [Fact] @@ -100,7 +104,8 @@ public async Task PublicationHasNoReleaseVersions_ReturnsNull() await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publication.Id, releaseSlug: "2021-22")); + Assert.Null( + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22")); } [Fact] @@ -108,7 +113,7 @@ public async Task PublicationDoesNotExist_ReturnsNull() { var repository = BuildRepository(); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publicationId: Guid.NewGuid(), + Assert.Null(await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publicationId: Guid.NewGuid(), releaseSlug: "2021-22")); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs index fb99630f1f0..fb1e7400467 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs @@ -19,7 +19,7 @@ Task GetPublishedDate( /// The slug of the release. /// A to observe while waiting for the task to complete. /// The latest published version of the release associated with the publication. - Task GetLatestPublishedReleaseVersion( + Task GetLatestPublishedReleaseVersionByReleaseSlug( Guid publicationId, string releaseSlug, CancellationToken cancellationToken = default); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs index 18ba9720d88..685151bbb79 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs @@ -71,7 +71,7 @@ await _contentDbContext.Entry(releaseVersion) : null; } - public async Task GetLatestPublishedReleaseVersion( + public async Task GetLatestPublishedReleaseVersionByReleaseSlug( Guid publicationId, string releaseSlug, CancellationToken cancellationToken = default) @@ -79,7 +79,7 @@ await _contentDbContext.Entry(releaseVersion) // There should only ever be one latest published release version with a given slug return await _contentDbContext.ReleaseVersions .LatestReleaseVersions(publicationId, releaseSlug, publishedOnly: true) - .FirstOrDefaultAsync(cancellationToken: cancellationToken); + .SingleOrDefaultAsync(cancellationToken: cancellationToken); } public async Task IsLatestPublishedReleaseVersion( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs index 68bc9328b6a..49bb95807e8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ReleaseService.cs @@ -58,7 +58,7 @@ public async Task> GetRelease( // otherwise use the latest published version of the requested release var latestReleaseVersionId = releaseSlug == null ? publication.LatestPublishedReleaseVersionId - : (await _releaseVersionRepository.GetLatestPublishedReleaseVersion(publication.Id, + : (await _releaseVersionRepository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug))?.Id; return latestReleaseVersionId.HasValue From 23523e222dd4c28018d6d81586e87efc30eb4d6d Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Thu, 19 Dec 2024 17:38:58 +0000 Subject: [PATCH 29/40] EES-5656 Remove ManageContentPageViewModel.PublicationViewModel.Releases --- .../ManageContentPageServiceTests.cs | 6 ------ .../Mappings/MappingProfiles.cs | 17 ----------------- .../ManageContent/ManageContentPageViewModel.cs | 11 ----------- .../__tests__/ReleaseContentPage.test.tsx | 1 - .../__tests__/PreReleaseContentPage.test.tsx | 1 - .../src/prototypes/data/releaseContentData.ts | 7 ------- .../test/generators/releaseContentGenerators.ts | 1 - .../src/services/publicationService.ts | 5 ----- .../find-statistics/PublicationReleasePage.tsx | 6 +++++- .../__tests__/PublicationReleasePage.test.tsx | 10 ---------- .../__tests__/__data__/testReleaseData.ts | 12 ------------ .../components/__tests__/__data__/tableData.ts | 1 - 12 files changed, 5 insertions(+), 73 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs index f4dea4b73af..f5906b91e79 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ManageContent/ManageContentPageServiceTests.cs @@ -324,12 +324,6 @@ public async Task GetManageContentPageViewModel() Assert.Equal(publication.ReleaseSeries[2].LegacyLinkUrl, contentPublicationReleaseSeries[2].LegacyLinkUrl); - var contentPublicationReleases = contentPublication.Releases; - Assert.Single(contentPublicationReleases); - Assert.Equal(otherReleaseVersion.Id, contentPublicationReleases[0].Id); - Assert.Equal(otherReleaseVersion.Slug, contentPublicationReleases[0].Slug); - Assert.Equal(otherReleaseVersion.Title, contentPublicationReleases[0].Title); - Assert.Equal(2, contentPublication.Methodologies.Count); Assert.Equal(methodology.Versions[0].Id, contentPublication.Methodologies[0].Id); Assert.Equal(methodology.Versions[0].Title, contentPublication.Methodologies[0].Title); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs index 3f802dd7f08..affee167e36 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs @@ -128,18 +128,6 @@ public MappingProfiles() Title = rv.Publication.Title, Slug = rv.Publication.Slug, Contact = rv.Publication.Contact, - Releases = rv.Publication.ReleaseVersions - .FindAll(otherReleaseVersion => rv.Id != otherReleaseVersion.Id && - IsLatestVersionOfRelease(rv.Publication.ReleaseVersions, otherReleaseVersion.Id)) - .OrderByDescending(otherReleaseVersion => otherReleaseVersion.Year) - .ThenByDescending(otherReleaseVersion => otherReleaseVersion.TimePeriodCoverage) - .Select(otherReleaseVersion => new PreviousReleaseViewModel - { - Id = otherReleaseVersion.Id, - Slug = otherReleaseVersion.Slug, - Title = otherReleaseVersion.Title, - }) - .ToList(), ReleaseSeries = new List(), // Must be hydrated after mapping ExternalMethodology = rv.Publication.ExternalMethodology != null ? new ExternalMethodology @@ -225,10 +213,5 @@ private void CreateContentBlockMap() CreateMap(); } - - private static bool IsLatestVersionOfRelease(IEnumerable releaseVersions, Guid releaseVersionId) - { - return !releaseVersions.Any(rv => rv.PreviousVersionId == releaseVersionId && rv.Id != releaseVersionId); - } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ManageContent/ManageContentPageViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ManageContent/ManageContentPageViewModel.cs index f91481a35d6..be3e5d8802d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ManageContent/ManageContentPageViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ManageContent/ManageContentPageViewModel.cs @@ -80,8 +80,6 @@ public class PublicationViewModel public string Slug { get; set; } - public List Releases { get; set; } - public List ReleaseSeries { get; set; } public Contact Contact { get; set; } @@ -100,13 +98,4 @@ public class ReleaseNoteViewModel public DateTime On { get; set; } } - - public class PreviousReleaseViewModel - { - public Guid Id { get; set; } - - public string Slug { get; set; } - - public string Title { get; set; } - } } diff --git a/src/explore-education-statistics-admin/src/pages/release/content/__tests__/ReleaseContentPage.test.tsx b/src/explore-education-statistics-admin/src/pages/release/content/__tests__/ReleaseContentPage.test.tsx index bf84627fec8..cd8fcd19559 100644 --- a/src/explore-education-statistics-admin/src/pages/release/content/__tests__/ReleaseContentPage.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/content/__tests__/ReleaseContentPage.test.tsx @@ -129,7 +129,6 @@ describe('ReleaseContentPage', () => { id: 'publication-id', title: 'Publication 1', slug: 'publication-1', - releases: [], releaseSeries: [], theme: { id: 'theme-1', title: 'Theme 1' }, contact: { diff --git a/src/explore-education-statistics-admin/src/pages/release/pre-release/__tests__/PreReleaseContentPage.test.tsx b/src/explore-education-statistics-admin/src/pages/release/pre-release/__tests__/PreReleaseContentPage.test.tsx index c8c4e512df1..892b34e7b1c 100644 --- a/src/explore-education-statistics-admin/src/pages/release/pre-release/__tests__/PreReleaseContentPage.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/pre-release/__tests__/PreReleaseContentPage.test.tsx @@ -91,7 +91,6 @@ describe('PreReleaseContentPage', () => { id: 'publication-id', title: 'Publication 1', slug: 'publication-1', - releases: [], releaseSeries: [], theme: { id: 'theme-1', title: 'Theme 1' }, contact: { diff --git a/src/explore-education-statistics-admin/src/prototypes/data/releaseContentData.ts b/src/explore-education-statistics-admin/src/prototypes/data/releaseContentData.ts index 6b5c0352ddc..abc3df8550b 100644 --- a/src/explore-education-statistics-admin/src/prototypes/data/releaseContentData.ts +++ b/src/explore-education-statistics-admin/src/prototypes/data/releaseContentData.ts @@ -128,13 +128,6 @@ const prototypeReleaseContent: ReleaseContent = { slug: 'methodology-slug', }, ], - releases: [ - { - id: 'previous-release-id', - slug: 'previous-release-slug', - title: 'Previous release title', - }, - ], releaseSeries: [], slug: 'publication-slug', title: 'Initial Teacher Training Census', diff --git a/src/explore-education-statistics-admin/test/generators/releaseContentGenerators.ts b/src/explore-education-statistics-admin/test/generators/releaseContentGenerators.ts index ab2c4eb9faa..5e71a6747bc 100644 --- a/src/explore-education-statistics-admin/test/generators/releaseContentGenerators.ts +++ b/src/explore-education-statistics-admin/test/generators/releaseContentGenerators.ts @@ -92,7 +92,6 @@ const defaultPublication: Publication = { slug: 'methodology-slug', }, ], - releases: [], releaseSeries: [ { isLegacyLink: true, diff --git a/src/explore-education-statistics-common/src/services/publicationService.ts b/src/explore-education-statistics-common/src/services/publicationService.ts index d32c4c8898d..6a919192418 100644 --- a/src/explore-education-statistics-common/src/services/publicationService.ts +++ b/src/explore-education-statistics-common/src/services/publicationService.ts @@ -20,11 +20,6 @@ export interface Publication { id: string; slug: string; title: string; - releases: { - id: string; - slug: string; - title: string; - }[]; releaseSeries: ReleaseSeriesItem[]; theme: { id: string; diff --git a/src/explore-education-statistics-frontend/src/modules/find-statistics/PublicationReleasePage.tsx b/src/explore-education-statistics-frontend/src/modules/find-statistics/PublicationReleasePage.tsx index ac3a4d19f6b..fb89cf32d87 100644 --- a/src/explore-education-statistics-frontend/src/modules/find-statistics/PublicationReleasePage.tsx +++ b/src/explore-education-statistics-frontend/src/modules/find-statistics/PublicationReleasePage.tsx @@ -130,7 +130,11 @@ const PublicationReleasePage: NextPage = ({ release }) => { > View latest data:{' '} - {release.publication.releases[0].title} + { + release.publication.releaseSeries.find( + rsi => !rsi.isLegacyLink, + )?.description + } )} diff --git a/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/PublicationReleasePage.test.tsx b/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/PublicationReleasePage.test.tsx index 4dbfc470915..1522c2a7a7d 100644 --- a/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/PublicationReleasePage.test.tsx +++ b/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/PublicationReleasePage.test.tsx @@ -342,16 +342,6 @@ describe('PublicationReleasePage', () => { month: 2, year: 2022, }, - publication: { - ...testRelease.publication, - releases: [ - { - id: 'latest-release', - title: 'Latest Release Title', - slug: 'latest-release-slug', - }, - ], - }, }} />, ); diff --git a/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/__data__/testReleaseData.ts b/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/__data__/testReleaseData.ts index c7c697c1db3..b36cccdaa90 100644 --- a/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/__data__/testReleaseData.ts +++ b/src/explore-education-statistics-frontend/src/modules/find-statistics/__tests__/__data__/testReleaseData.ts @@ -4,18 +4,6 @@ export const testPublication: Publication = { id: 'publication-1', title: 'Pupil absence in schools in England', slug: 'pupil-absence-in-schools-in-england', - releases: [ - { - id: 'release-2', - slug: '2018-19', - title: 'Academic year 2018/19', - }, - { - id: 'release-1', - slug: '2017-18', - title: 'Academic year 2017/18', - }, - ], releaseSeries: [ { isLegacyLink: false, diff --git a/src/explore-education-statistics-frontend/src/modules/table-tool/components/__tests__/__data__/tableData.ts b/src/explore-education-statistics-frontend/src/modules/table-tool/components/__tests__/__data__/tableData.ts index eaa670625a7..9f7823af6ae 100644 --- a/src/explore-education-statistics-frontend/src/modules/table-tool/components/__tests__/__data__/tableData.ts +++ b/src/explore-education-statistics-frontend/src/modules/table-tool/components/__tests__/__data__/tableData.ts @@ -273,7 +273,6 @@ export const testPublicationRelease: Release = { id: '', slug: '', title: '', - releases: [], releaseSeries: [], theme: { title: '', From 9412715a81c7dd64b0fd38ec21496222fe2e8fe0 Mon Sep 17 00:00:00 2001 From: Ben Outram Date: Fri, 20 Dec 2024 10:07:13 +0000 Subject: [PATCH 30/40] EES-5656 Change ContentService UpdateContent/UpdateContentStaged methods to use release series when working out the latest published release version of publication --- .../Extensions/EnumerableExtensionsTests.cs | 17 -- .../Extensions/EnumerableExtensions.cs | 26 -- .../Predicates/ReleaseVersionPredicates.cs | 16 +- .../Extensions/PublisherExtensionTests.cs | 175 ----------- .../Services/ReleaseServiceTests.cs | 287 ++++++++++++------ .../Extensions/PublisherExtensions.cs | 42 --- .../PublisherHostBuilderExtensions.cs | 1 + .../Services/ContentService.cs | 77 ++--- .../Services/Interfaces/IContentService.cs | 2 +- .../Services/Interfaces/IReleaseService.cs | 4 +- .../Services/PublishingCompletionService.cs | 26 +- .../Services/ReleaseService.cs | 86 +++--- 12 files changed, 301 insertions(+), 458 deletions(-) delete mode 100644 src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Extensions/PublisherExtensionTests.cs delete mode 100644 src/GovUk.Education.ExploreEducationStatistics.Publisher/Extensions/PublisherExtensions.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/EnumerableExtensionsTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/EnumerableExtensionsTests.cs index d70fa90b556..acdb9ab102d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/EnumerableExtensionsTests.cs @@ -218,23 +218,6 @@ public void ToDictionaryIndexed() Assert.Equal(expected, result); } - [Fact] - public void DistinctByProperty() - { - var list = new List - { - new(1), - new(1), - new(2), - }; - - var distinct = list.DistinctByProperty(x => x.Value).ToList(); - - Assert.Equal(2, distinct.Count); - Assert.Equal(1, distinct[0].Value); - Assert.Equal(2, distinct[1].Value); - } - [Fact] public void IndexOfFirst() { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/EnumerableExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/EnumerableExtensions.cs index 5ff99b81aff..48d1210fcc7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/EnumerableExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/EnumerableExtensions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Model; using NaturalSort.Extension; @@ -229,31 +228,6 @@ public static IAsyncEnumerable WhereNotNull(this IAsyncEnumerable sour public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable self) => self.Select((item, index) => (item, index)); - /// - /// Filter a list down to distinct elements based on a property of the type. - /// - /// - /// - /// As IEqualityComparers (as used in Linq's Distinct() method) compare with GetHashCode() rather than with - /// Equals(), the property being used to compare distinctions against needs to produce a reliable hash code - /// that we can use for equality. A good property type then could be a Guid Id field, as two identical Guid Ids - /// can then represent that 2 or more entities in the list are duplicates as they will have the same hash code. - /// - /// - /// Sequence of elements to filter on a distinct property - /// A supplier of a property from each entity to check for equality. The property - /// chosen must produce the same hash code for any two elements in the source list that are considered - /// duplicates. A good example would be a Guid Id. - /// - /// - public static IEnumerable DistinctByProperty( - this IEnumerable source, - Func propertyGetter) - where T : class - { - return source.Distinct(ComparerUtils.CreateComparerByProperty(propertyGetter)); - } - public static bool IsSameAsIgnoringOrder(this IEnumerable first, IEnumerable second) { var firstList = first.ToList(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs index 57560809186..30d7023a26c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; using System.Linq; namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Predicates; @@ -55,17 +56,26 @@ public static IQueryable LatestReleaseVersions(this IQueryableThe source of type to filter. /// Unique identifier of a release to filter by. /// Flag to only include published release versions. + /// Optional list of unpublished release version ids to also consider. + /// Applicable when is true. /// An of type that contains elements from the input /// sequence filtered to only include the latest version of the release. - public static IQueryable LatestReleaseVersion(this IQueryable releaseVersions, + public static IQueryable LatestReleaseVersion( + this IQueryable releaseVersions, Guid releaseId, - bool publishedOnly = false) + bool publishedOnly = false, + IReadOnlyList? includeUnpublishedVersionIds = null) { return releaseVersions .Where(releaseVersion => releaseVersion.ReleaseId == releaseId) .Where(releaseVersion => releaseVersion.Version == releaseVersions .Where(latestVersion => latestVersion.ReleaseId == releaseId) - .Where(latestVersion => !publishedOnly || latestVersion.Published.HasValue) + .Where(latestVersion => !publishedOnly || + latestVersion.Published.HasValue || + ( + includeUnpublishedVersionIds != null && + includeUnpublishedVersionIds.Contains(latestVersion.Id) + )) .Select(latestVersion => (int?)latestVersion.Version) .Max()); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Extensions/PublisherExtensionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Extensions/PublisherExtensionTests.cs deleted file mode 100644 index 6fd5e204a10..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Extensions/PublisherExtensionTests.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Collections.Generic; -using GovUk.Education.ExploreEducationStatistics.Content.Model; -using GovUk.Education.ExploreEducationStatistics.Publisher.Extensions; -using Xunit; - -namespace GovUk.Education.ExploreEducationStatistics.Publisher.Tests.Extensions -{ - public class PublisherExtensionTests - { - [Fact] - public void IsReleasePublished_ReleasePublished() - { - var publication = new Publication(); - - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - Published = DateTime.UtcNow.AddSeconds(-1) - }; - - publication.ReleaseVersions = new List - { - releaseVersion - }; - - Assert.True(releaseVersion.IsReleasePublished()); - } - - [Fact] - public void IsReleasePublished_ReleaseNotPublished() - { - var publication = new Publication(); - - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - Published = null - }; - - publication.ReleaseVersions = new List - { - releaseVersion - }; - - Assert.False(releaseVersion.IsReleasePublished()); - } - - [Fact] - public void IsReleasePublished_ReleaseNotPublishedButIncluded() - { - var publication = new Publication(); - - var releaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - Published = null - }; - - publication.ReleaseVersions = new List - { - releaseVersion - }; - - Assert.True(releaseVersion.IsReleasePublished(new List - { - releaseVersion.Id - })); - } - - [Fact] - public void IsReleasePublished_AmendmentReleaseNotPublished() - { - var publication = new Publication(); - - var originalReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - Version = 0, - Published = DateTime.UtcNow.AddSeconds(-1) - }; - - var amendmentReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - PreviousVersionId = originalReleaseVersion.Id, - Version = 1 - }; - - publication.ReleaseVersions = new List - { - originalReleaseVersion, - amendmentReleaseVersion - }; - - Assert.True(originalReleaseVersion.IsReleasePublished()); - Assert.False(amendmentReleaseVersion.IsReleasePublished()); - } - - [Fact] - public void IsReleasePublished_AmendmentReleasePublished() - { - var publication = new Publication(); - - var originalReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - Version = 0, - Published = DateTime.UtcNow.AddSeconds(-1) - }; - - var amendmentReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - PreviousVersionId = originalReleaseVersion.Id, - Version = 1, - Published = DateTime.UtcNow.AddSeconds(-1) - }; - - publication.ReleaseVersions = new List - { - originalReleaseVersion, - amendmentReleaseVersion - }; - - Assert.False(originalReleaseVersion.IsReleasePublished()); - Assert.True(amendmentReleaseVersion.IsReleasePublished()); - } - - [Fact] - public void IsReleasePublished_AmendmentReleaseNotPublishedButIncluded() - { - var publication = new Publication(); - - var originalReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - Version = 0, - Published = DateTime.UtcNow.AddSeconds(-1) - }; - - var amendmentReleaseVersion = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - PreviousVersionId = originalReleaseVersion.Id, - Version = 1 - }; - - publication.ReleaseVersions = new List - { - originalReleaseVersion, - amendmentReleaseVersion - }; - - Assert.False(originalReleaseVersion.IsReleasePublished(new List - { - amendmentReleaseVersion.Id - })); - - Assert.True(amendmentReleaseVersion.IsReleasePublished(new List - { - amendmentReleaseVersion.Id - })); - } - } -} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Services/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Services/ReleaseServiceTests.cs index b8b48213731..9524492242a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Services/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher.Tests/Services/ReleaseServiceTests.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; @@ -7,14 +11,8 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Publisher.Services; using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Common.Model.FileType; -using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; using static GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils.ContentDbUtils; namespace GovUk.Education.ExploreEducationStatistics.Publisher.Tests.Services @@ -95,108 +93,136 @@ public async Task GetFiles() } [Fact] - public async Task GetLatestRelease() + public async Task GetLatestPublishedReleaseVersion_Success() { - var publication = new Publication(); + Publication publication = _fixture.DefaultPublication() + .WithReleases(_ => + [ + _fixture.DefaultRelease(publishedVersions: 1, year: 2020), + _fixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021), + _fixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022) + ]); - var release1V0 = new ReleaseVersion - { - Id = Guid.NewGuid(), - Publication = publication, - ReleaseName = "2017", - TimePeriodCoverage = AcademicYearQ4, - Slug = "2017-18-q4", - Published = new DateTime(2019, 4, 1), - ApprovalStatus = Approved - }; + var release2021 = publication.Releases.Single(r => r.Year == 2021); - var release2V0 = new ReleaseVersion + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - Id = new Guid("e7e1aae3-a0a1-44b7-bdf3-3df4a363ce20"), - Publication = publication, - ReleaseName = "2018", - TimePeriodCoverage = AcademicYearQ1, - Slug = "2018-19-q1", - Published = new DateTime(2019, 3, 1), - ApprovalStatus = Approved, - }; + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } - var release3V0 = new ReleaseVersion + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - Id = Guid.NewGuid(), - Publication = publication, - ReleaseName = "2018", - TimePeriodCoverage = AcademicYearQ2, - Slug = "2018-19-q2", - Published = new DateTime(2019, 1, 1), - ApprovalStatus = Approved, - Version = 0, - PreviousVersionId = null - }; + var service = BuildReleaseService(contentDbContext); + + var result = await service.GetLatestPublishedReleaseVersion( + publication.Id, + includeUnpublishedVersionIds: []); + + Assert.Equal(release2021.Versions[1].Id, result.Id); + } + } - var release3V1 = new ReleaseVersion + [Fact] + public async Task GetLatestPublishedReleaseVersion_NonDefaultReleaseOrder() + { + Publication publication = _fixture.DefaultPublication() + .WithReleases(_ => + [ + _fixture.DefaultRelease(publishedVersions: 1, year: 2020), + _fixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021), + _fixture.DefaultRelease(publishedVersions: 1, year: 2022) + ]) + .FinishWith(p => + { + // Adjust the generated LatestPublishedReleaseVersion to make 2020 the latest published release + var release2020Version0 = p.Releases.Single(r => r.Year == 2020).Versions[0]; + p.LatestPublishedReleaseVersion = release2020Version0; + p.LatestPublishedReleaseVersionId = release2020Version0.Id; + + // Apply a different release series order rather than using the default + p.ReleaseSeries = + [.. GenerateReleaseSeries(p.Releases, 2021, 2020, 2022)]; + }); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - Id = Guid.NewGuid(), - Publication = publication, - ReleaseName = "2018", - TimePeriodCoverage = AcademicYearQ2, - Slug = "2018-19-q2", - Published = new DateTime(2019, 2, 1), - ApprovalStatus = Approved, - Version = 1, - PreviousVersionId = release3V0.Id - }; + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } - var release3V2Deleted = new ReleaseVersion + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - Id = Guid.NewGuid(), - Publication = publication, - ReleaseName = "2018", - TimePeriodCoverage = AcademicYearQ2, - Slug = "2018-19-q2", - Published = null, - ApprovalStatus = Approved, - Version = 2, - PreviousVersionId = release3V1.Id, - SoftDeleted = true - }; + var service = BuildReleaseService(contentDbContext); - var release3V3NotPublished = new ReleaseVersion + var result = await service.GetLatestPublishedReleaseVersion( + publication.Id, + includeUnpublishedVersionIds: []); + + // Check the 2020 release version is considered to be the latest published release version, + // since 2020 is the first release in the release series with a published version + Assert.Equal(release2020.Versions[0].Id, result.Id); + } + } + + [Fact] + public async Task GetLatestPublishedReleaseVersion_IncludeUnpublishedReleaseVersion() + { + Publication publication = _fixture.DefaultPublication() + .WithReleases(_ => + [ + _fixture.DefaultRelease(publishedVersions: 1, year: 2020), + _fixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021), + _fixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022) + ]); + + var release2021 = publication.Releases.Single(r => r.Year == 2021); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - Id = Guid.NewGuid(), - Publication = publication, - ReleaseName = "2018", - TimePeriodCoverage = AcademicYearQ2, - Slug = "2018-19-q2", - Published = null, - ApprovalStatus = Approved, - Version = 3, - PreviousVersionId = release3V1.Id - }; + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } - var release4V0 = new ReleaseVersion + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - Id = Guid.NewGuid(), - Publication = publication, - ReleaseName = "2018", - TimePeriodCoverage = AcademicYearQ3, - Slug = "2018-19-q3", - Published = null, - ApprovalStatus = Approved - }; + var service = BuildReleaseService(contentDbContext); - var contentDbContextId = Guid.NewGuid().ToString(); + // Include the unpublished 2021 release version id in the call + // to test the scenario where this version is about to be published + var result = await service.GetLatestPublishedReleaseVersion( + publication.Id, + includeUnpublishedVersionIds: [release2021.Versions.Single(rv => rv.Published == null).Id]); + + // Check the unpublished 2021 release version is considered to be the latest published release version, + // despite the fact that it is not published yet + Assert.Equal(release2021.Versions.Single(rv => rv.Published == null).Id, result.Id); + } + } + + [Fact] + public async Task GetLatestPublishedReleaseVersion_IncludeUnpublishedReleaseVersions() + { + Publication publication = _fixture.DefaultPublication() + .WithReleases(_ => + [ + _fixture.DefaultRelease(publishedVersions: 1, year: 2020), + _fixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021), + _fixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022) + ]); + + var release2021 = publication.Releases.Single(r => r.Year == 2021); + var release2022 = publication.Releases.Single(r => r.Year == 2022); + var contentDbContextId = Guid.NewGuid().ToString(); await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - await contentDbContext.AddRangeAsync(publication); - await contentDbContext.AddRangeAsync(release1V0, - release2V0, - release3V0, - release3V1, - release3V2Deleted, - release3V3NotPublished, - release4V0); + contentDbContext.Publications.Add(publication); await contentDbContext.SaveChangesAsync(); } @@ -204,10 +230,73 @@ await contentDbContext.AddRangeAsync(release1V0, { var service = BuildReleaseService(contentDbContext); - var result = await service.GetLatestReleaseVersion(publication.Id, Enumerable.Empty()); + // Include the unpublished 2021 and 2022 release version id's in the call + // to test the scenario where both versions are about to be published together + var result = await service.GetLatestPublishedReleaseVersion( + publication.Id, + includeUnpublishedVersionIds: + [ + release2021.Versions.Single(rv => rv.Published == null).Id, + release2022.Versions.Single(rv => rv.Published == null).Id + ]); + + // Check the unpublished 2022 release version is considered to be the latest published release version, + // despite the fact that it is not published yet + Assert.Equal(release2022.Versions.Single(rv => rv.Published == null).Id, result.Id); + } + } + + [Fact] + public async Task GetLatestPublishedReleaseVersion_IgnoresIncludedUnpublishedReleaseVersions() + { + Publication publication = _fixture.DefaultPublication() + .WithReleases(_ => + [ + _fixture.DefaultRelease(publishedVersions: 1, year: 2020), + _fixture.DefaultRelease(publishedVersions: 2, draftVersion: true, year: 2021), + _fixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2022) + ]) + .FinishWith(p => + { + // Adjust the generated LatestPublishedReleaseVersion to make 2020 the latest published release + var release2020Version0 = p.Releases.Single(r => r.Year == 2020).Versions[0]; + p.LatestPublishedReleaseVersion = release2020Version0; + p.LatestPublishedReleaseVersionId = release2020Version0.Id; + + // Apply a different release series order rather than using the default + p.ReleaseSeries = + [.. GenerateReleaseSeries(p.Releases, 2020, 2021, 2022)]; + }); + + var release2020 = publication.Releases.Single(r => r.Year == 2020); + var release2021 = publication.Releases.Single(r => r.Year == 2021); + var release2022 = publication.Releases.Single(r => r.Year == 2022); - Assert.Equal(release3V1.Id, result.Id); - Assert.Equal("Academic year Q2 2018/19", result.Title); + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + contentDbContext.Publications.Add(publication); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var service = BuildReleaseService(contentDbContext); + + // Include the unpublished 2021 and 2022 release version id's in the call + // to test the scenario where both versions are about to be published together + var result = await service.GetLatestPublishedReleaseVersion( + publication.Id, + includeUnpublishedVersionIds: + [ + release2021.Versions.Single(rv => rv.Published == null).Id, + release2022.Versions.Single(rv => rv.Published == null).Id + ]); + + // Check the 2020 release version is considered to be the latest published release version, + // despite the fact that versions for 2021 and 2022 are about to be published, + // since 2020 is the first release in the release series and has a published version + Assert.Equal(release2020.Versions[0].Id, result.Id); } } @@ -535,8 +624,16 @@ public async Task CompletePublishing_AmendedReleaseAndUpdatePublishedDateIsTrue( } } - private static ReleaseService BuildReleaseService( - ContentDbContext? contentDbContext = null) + private List GenerateReleaseSeries(IReadOnlyList releases, params int[] years) + { + return years.Select(year => + { + var release = releases.Single(r => r.Year == year); + return _fixture.DefaultReleaseSeriesItem().WithReleaseId(release.Id).Generate(); + }).ToList(); + } + + private static ReleaseService BuildReleaseService(ContentDbContext? contentDbContext = null) { contentDbContext ??= InMemoryContentDbContext(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Extensions/PublisherExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Extensions/PublisherExtensions.cs deleted file mode 100644 index cc405bd6ea2..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Extensions/PublisherExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using GovUk.Education.ExploreEducationStatistics.Content.Model; - -namespace GovUk.Education.ExploreEducationStatistics.Publisher.Extensions; - -public static class PublisherExtensions -{ - /// - /// Determines whether a release version should be published or not. - /// - /// The to test - /// Release version id's which are not published yet but are in the process of being published - /// True if the release version is the latest published version of a release or is one of the included releases - public static bool IsReleasePublished(this ReleaseVersion releaseVersion, - IEnumerable includedReleaseVersionIds = null) - { - return includedReleaseVersionIds != null && - includedReleaseVersionIds.Contains(releaseVersion.Id) || - releaseVersion.IsLatestPublishedVersionOfRelease(includedReleaseVersionIds); - } - - private static bool IsLatestPublishedVersionOfRelease(this ReleaseVersion releaseVersion, - IEnumerable includedReleaseIds) - { - if (releaseVersion.Publication?.ReleaseVersions == null || !releaseVersion.Publication.ReleaseVersions.Any()) - { - throw new ArgumentException( - "All release versions of the publication must be hydrated to test the latest published version"); - } - - return - // Release version itself must be live - releaseVersion.Live - // It must also be the latest version unless the later version is a draft not included for publishing - && !releaseVersion.Publication.ReleaseVersions.Any(rv => - (rv.Live || includedReleaseIds != null && includedReleaseIds.Contains(rv.Id)) - && rv.PreviousVersionId == releaseVersion.Id - && rv.Id != releaseVersion.Id); - } -} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs index 395fe578313..bb952026124 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/PublisherHostBuilderExtensions.cs @@ -96,6 +96,7 @@ public static IHostBuilder ConfigurePublisherHostBuilder(this IHostBuilder hostB provider.GetRequiredService>())) .AddScoped(provider => new ContentService( + contentDbContext: provider.GetRequiredService(), publicBlobStorageService: provider.GetRequiredService(), privateBlobCacheService: new BlobCacheService( provider.GetRequiredService(), diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ContentService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ContentService.cs index 72987829a04..8cddbea8b29 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ContentService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ContentService.cs @@ -4,10 +4,11 @@ using GovUk.Education.ExploreEducationStatistics.Common; using GovUk.Education.ExploreEducationStatistics.Common.Cache; using GovUk.Education.ExploreEducationStatistics.Common.Cache.Interfaces; -using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces; +using Microsoft.EntityFrameworkCore; using static GovUk.Education.ExploreEducationStatistics.Common.BlobContainers; using static GovUk.Education.ExploreEducationStatistics.Common.Services.FileStoragePathUtils; @@ -15,6 +16,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Publisher.Services { public class ContentService : IContentService { + private readonly ContentDbContext _contentDbContext; private readonly IBlobCacheService _privateBlobCacheService; private readonly IBlobCacheService _publicBlobCacheService; private readonly IBlobStorageService _publicBlobStorageService; @@ -24,6 +26,7 @@ public class ContentService : IContentService private readonly IPublicationCacheService _publicationCacheService; public ContentService( + ContentDbContext contentDbContext, IBlobCacheService privateBlobCacheService, IBlobCacheService publicBlobCacheService, IBlobStorageService publicBlobStorageService, @@ -32,6 +35,7 @@ public ContentService( IReleaseCacheService releaseCacheService, IPublicationCacheService publicationCacheService) { + _contentDbContext = contentDbContext; _privateBlobCacheService = privateBlobCacheService; _publicBlobCacheService = publicBlobCacheService; _publicBlobStorageService = publicBlobStorageService; @@ -94,60 +98,61 @@ await _publicBlobStorageService.DeleteBlobs( } } - public async Task UpdateContent(params Guid[] releaseVersionIds) + public async Task UpdateContent(Guid releaseVersionId) { - var releaseVersions = (await _releaseService - .List(releaseVersionIds)) - .ToList(); - - foreach (var releaseVersion in releaseVersions) - { - await _releaseCacheService.UpdateRelease( - releaseVersion.Id, - publicationSlug: releaseVersion.Publication.Slug, - releaseSlug: releaseVersion.Slug); - } - - var publications = releaseVersions - .Select(rv => rv.Publication) - .DistinctByProperty(publication => publication.Id) - .ToList(); - - foreach (var publication in publications) - { - // Cache the latest release version for the publication as a separate cache entry - var latestReleaseVersion = await _releaseService.GetLatestReleaseVersion(publication.Id, releaseVersionIds); - await _releaseCacheService.UpdateRelease( - latestReleaseVersion.Id, - publicationSlug: publication.Slug); - } + var releaseVersion = await _contentDbContext.ReleaseVersions + .Include(rv => rv.Release) + .ThenInclude(r => r.Publication) + .SingleAsync(rv => rv.Id == releaseVersionId); + + await _releaseCacheService.UpdateRelease( + releaseVersion.Id, + publicationSlug: releaseVersion.Release.Publication.Slug, + releaseSlug: releaseVersion.Release.Slug); + + var publication = releaseVersion.Release.Publication; + + // Cache the latest release version for the publication as a separate cache entry + var latestReleaseVersion = await _releaseService.GetLatestPublishedReleaseVersion( + publicationId: publication.Id, + includeUnpublishedVersionIds: [releaseVersion.Id]); + + await _releaseCacheService.UpdateRelease( + releaseVersionId: latestReleaseVersion.Id, + publicationSlug: publication.Slug); } - public async Task UpdateContentStaged(DateTime expectedPublishDate, + public async Task UpdateContentStaged( + DateTime expectedPublishDate, params Guid[] releaseVersionIds) { - var releaseVersions = (await _releaseService - .List(releaseVersionIds)) - .ToList(); + var releaseVersions = await _contentDbContext.ReleaseVersions + .Where(rv => releaseVersionIds.Contains(rv.Id)) + .Include(rv => rv.Release) + .ThenInclude(r => r.Publication) + .ToListAsync(); foreach (var releaseVersion in releaseVersions) { await _releaseCacheService.UpdateReleaseStaged( releaseVersion.Id, expectedPublishDate, - publicationSlug: releaseVersion.Publication.Slug, - releaseSlug: releaseVersion.Slug); + publicationSlug: releaseVersion.Release.Publication.Slug, + releaseSlug: releaseVersion.Release.Slug); } var publications = releaseVersions - .Select(rv => rv.Publication) - .DistinctByProperty(publication => publication.Id) + .Select(rv => rv.Release.Publication) + .DistinctBy(p => p.Id) .ToList(); foreach (var publication in publications) { // Cache the latest release version for the publication as a separate cache entry - var latestReleaseVersion = await _releaseService.GetLatestReleaseVersion(publication.Id, releaseVersionIds); + var latestReleaseVersion = await _releaseService.GetLatestPublishedReleaseVersion( + publicationId: publication.Id, + includeUnpublishedVersionIds: releaseVersionIds); + await _releaseCacheService.UpdateReleaseStaged( latestReleaseVersion.Id, expectedPublishDate, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IContentService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IContentService.cs index 9e80eb61220..28792d63f3a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IContentService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IContentService.cs @@ -9,7 +9,7 @@ public interface IContentService Task DeletePreviousVersionsContent(params Guid[] releaseVersionIds); - Task UpdateContent(params Guid[] releaseVersionIds); + Task UpdateContent(Guid releaseVersionId); Task UpdateContentStaged(DateTime expectedPublishDate, params Guid[] releaseVersionIds); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IReleaseService.cs index 83fc632e269..51a60318f13 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/Interfaces/IReleaseService.cs @@ -16,7 +16,9 @@ public interface IReleaseService Task> GetFiles(Guid releaseVersionId, params FileType[] types); - Task GetLatestReleaseVersion(Guid publicationId, IEnumerable includedReleaseVersionIds); + Task GetLatestPublishedReleaseVersion( + Guid publicationId, + IReadOnlyList? includeUnpublishedVersionIds = null); Task CompletePublishing(Guid releaseVersionId, DateTime actualPublishedDate); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs index 9bd2031ff20..bc469d819da 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs @@ -3,8 +3,6 @@ using System.Linq; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Predicates; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces; @@ -125,30 +123,10 @@ private async Task UpdateLatestPublishedReleaseVersionForPublication(Guid public var publication = await contentDbContext.Publications .SingleAsync(p => p.Id == publicationId); - // Get the publications release id's by the order they appear in the release series - var releaseSeriesReleaseIds = publication.ReleaseSeries.ReleaseIds(); + var latestPublishedReleaseVersion = await releaseService.GetLatestPublishedReleaseVersion(publicationId); - // Work out the publication's new latest published release version. - // This is the latest published version of the first release which has a published version - Guid? latestPublishedReleaseVersionId = null; - foreach (var releaseId in releaseSeriesReleaseIds) - { - latestPublishedReleaseVersionId = (await contentDbContext.ReleaseVersions - .LatestReleaseVersion(releaseId: releaseId, publishedOnly: true) - .SingleOrDefaultAsync())?.Id; - - if (latestPublishedReleaseVersionId != null) - { - break; - } - } - - publication.LatestPublishedReleaseVersionId = - latestPublishedReleaseVersionId ?? - throw new InvalidOperationException( - $"No latest published release version found for publication {publicationId}"); + publication.LatestPublishedReleaseVersionId = latestPublishedReleaseVersion.Id; - contentDbContext.Update(publication); await contentDbContext.SaveChangesAsync(); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleaseService.cs index dcf55d60cef..52b3465b012 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleaseService.cs @@ -1,39 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Predicates; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces; using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using static GovUk.Education.ExploreEducationStatistics.Publisher.Extensions.PublisherExtensions; namespace GovUk.Education.ExploreEducationStatistics.Publisher.Services { - public class ReleaseService : IReleaseService + public class ReleaseService( + ContentDbContext contentDbContext, + IReleaseVersionRepository releaseVersionRepository + ) : IReleaseService { - private readonly ContentDbContext _contentDbContext; - private readonly IReleaseVersionRepository _releaseVersionRepository; - - public ReleaseService( - ContentDbContext contentDbContext, - IReleaseVersionRepository releaseVersionRepository) - { - _contentDbContext = contentDbContext; - _releaseVersionRepository = releaseVersionRepository; - } - public async Task Get(Guid releaseVersionId) { - return await _contentDbContext.ReleaseVersions + return await contentDbContext.ReleaseVersions .SingleAsync(releaseVersion => releaseVersion.Id == releaseVersionId); } public async Task> List(IEnumerable releaseVersionIds) { - return await _contentDbContext.ReleaseVersions + return await contentDbContext.ReleaseVersions .Where(rv => releaseVersionIds.Contains(rv.Id)) .Include(rv => rv.Publication) .Include(rv => rv.PreviousVersion) @@ -42,31 +35,48 @@ public async Task> List(IEnumerable releaseVer public async Task> GetAmendedReleases(IEnumerable releaseVersionIds) { - return await _contentDbContext.ReleaseVersions + return await contentDbContext.ReleaseVersions .Include(rv => rv.PreviousVersion) .Include(rv => rv.Publication) .Where(rv => releaseVersionIds.Contains(rv.Id) && rv.PreviousVersionId != null) .ToListAsync(); } - public async Task GetLatestReleaseVersion(Guid publicationId, - IEnumerable includedReleaseVersionIds) + public async Task GetLatestPublishedReleaseVersion( + Guid publicationId, + IReadOnlyList? includeUnpublishedVersionIds = null) { - var releases = await _contentDbContext.ReleaseVersions - .Include(rv => rv.Publication) - .Where(rv => rv.PublicationId == publicationId) - .ToListAsync(); + var publication = await contentDbContext.Publications + .SingleAsync(p => p.Id == publicationId); + + // Get the publications release id's by the order they appear in the release series + var releaseSeriesReleaseIds = publication.ReleaseSeries.ReleaseIds(); + + // Work out the publication's latest published release version. + // This is the latest published version of the first release which has either a published version + // or one of the included (about to be published) release version ids + ReleaseVersion? latestPublishedReleaseVersion = null; + foreach (var releaseId in releaseSeriesReleaseIds) + { + latestPublishedReleaseVersion = await contentDbContext.ReleaseVersions + .LatestReleaseVersion(releaseId: releaseId, + publishedOnly: true, + includeUnpublishedVersionIds: includeUnpublishedVersionIds) + .SingleOrDefaultAsync(); + + if (latestPublishedReleaseVersion != null) + { + break; + } + } - return releases - .Where(rv => rv.IsReleasePublished(includedReleaseVersionIds)) - .OrderBy(rv => rv.Year) - .ThenBy(rv => rv.TimePeriodCoverage) - .Last(); + return latestPublishedReleaseVersion ?? throw new InvalidOperationException( + $"No latest published release version found for publication {publicationId}"); } public async Task> GetFiles(Guid releaseVersionId, params FileType[] types) { - return await _contentDbContext + return await contentDbContext .ReleaseFiles .Include(rf => rf.File) .Where(rf => rf.ReleaseVersionId == releaseVersionId) @@ -77,15 +87,15 @@ public async Task> GetFiles(Guid releaseVersionId, params FileType[] public async Task CompletePublishing(Guid releaseVersionId, DateTime actualPublishedDate) { - var releaseVersion = await _contentDbContext + var releaseVersion = await contentDbContext .ReleaseVersions .Include(rv => rv.DataBlockVersions) .ThenInclude(dataBlockVersion => dataBlockVersion.DataBlockParent) .SingleAsync(rv => rv.Id == releaseVersionId); - _contentDbContext.ReleaseVersions.Update(releaseVersion); + contentDbContext.ReleaseVersions.Update(releaseVersion); - var publishedDate = await _releaseVersionRepository.GetPublishedDate(releaseVersion.Id, actualPublishedDate); + var publishedDate = await releaseVersionRepository.GetPublishedDate(releaseVersion.Id, actualPublishedDate); releaseVersion.Published = publishedDate; @@ -93,14 +103,14 @@ public async Task CompletePublishing(Guid releaseVersionId, DateTime actualPubli await UpdatePublishedDataBlockVersions(releaseVersion); - await _contentDbContext.SaveChangesAsync(); + await contentDbContext.SaveChangesAsync(); } private async Task UpdateReleaseFilePublishedDate( ReleaseVersion releaseVersion, DateTime publishedDate) { - var dataReleaseFiles = _contentDbContext.ReleaseFiles + var dataReleaseFiles = contentDbContext.ReleaseFiles .Where(releaseFile => releaseFile.ReleaseVersionId == releaseVersion.Id) .Include(rf => rf.File); @@ -143,7 +153,7 @@ private async Task UpdatePublishedDataBlockVersions(ReleaseVersion releaseVersio { var latestDataBlockParentIds = latestDataBlockParents.Select(dataBlockParent => dataBlockParent.Id); - var removedDataBlockVersions = await _contentDbContext + var removedDataBlockVersions = await contentDbContext .DataBlockVersions .Where(dataBlockVersion => dataBlockVersion.ReleaseVersionId == releaseVersion.PreviousVersionId && !latestDataBlockParentIds.Contains(dataBlockVersion.DataBlockParentId)) From 3968fb43a929712109bbf93ac464552f9b51e1c3 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Fri, 20 Dec 2024 16:20:19 +0000 Subject: [PATCH 31/40] EES-5738 More changes in response to PR comments --- .../tests/admin_and_public/bau/data_catalogue.robot | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot b/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot index ceff983c154..659af1efdbc 100644 --- a/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot +++ b/tests/robot-tests/tests/admin_and_public/bau/data_catalogue.robot @@ -219,6 +219,9 @@ Filter by geographic level user checks page contains button Local Authority District user checks testid element contains total-results 1 data set + user clicks button Show more details + user checks testid element contains Geographic levels-value Local authority district + Remove geographic level filter user clicks button Local Authority District From 7648f19910641d5b15c15cf353ee028b3bf7422c Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Mon, 16 Dec 2024 11:19:23 +0000 Subject: [PATCH 32/40] EES-5740 Add FilterHierarchies column to Files table --- ...rHierarchiesColumnToFilesTable.Designer.cs | 2246 +++++++++++++++++ ..._AddFilterHierarchiesColumnToFilesTable.cs | 33 + .../ContentDbContextModelSnapshot.cs | 3 + .../DataSetFileFilterHierarchy.cs | 12 + .../Database/ContentDbContext.cs | 4 + .../File.cs | 2 + .../DataSetFileFilterHierarchyViewModel.cs | 13 + 7 files changed, 2313 insertions(+) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.Designer.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.Designer.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.Designer.cs new file mode 100644 index 00000000000..5cbb8fd53c8 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.Designer.cs @@ -0,0 +1,2246 @@ +// +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("20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable")] + partial class EES5740_AddFilterHierarchiesColumnToFilesTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .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"); + + 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("FilterHierarchies") + .HasColumnType("nvarchar(max)"); + + 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.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("ThemeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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("ThemeId"); + + 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("Label") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(81) + .HasColumnType("nvarchar(81)"); + + b.Property("TimePeriodCoverage") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PublicationId", "Year", "TimePeriodCoverage", "Label") + .IsUnique(); + + 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("PublicApiDataSetId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicApiDataSetVersion") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("ReleaseVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ReleaseVersionId", "FileId") + .IsUnique(); + + b.HasIndex("ReleaseVersionId", "PublicApiDataSetId", "PublicApiDataSetVersion") + .IsUnique() + .HasFilter("[PublicApiDataSetId] IS NOT NULL AND [PublicApiDataSetVersion] IS NOT NULL"); + + b.ToTable("ReleaseFiles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseRedirect", b => + { + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.HasKey("ReleaseId", "Slug"); + + b.ToTable("ReleaseRedirects"); + }); + + 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.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("DeletedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeletedById"); + + 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") + .WithMany() + .HasForeignKey("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("MethodologyRedirects") + .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.Theme", "Theme") + .WithMany("Publications") + .HasForeignKey("ThemeId") + .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("Theme"); + }); + + 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("PublicationRedirects") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("Releases") + .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.ReleaseRedirect", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("ReleaseRedirects") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Release"); + }); + + 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.NoAction) + .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.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.User", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById"); + + b.Navigation("DeletedBy"); + }); + + 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("MethodologyRedirects"); + + b.Navigation("Notes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.Navigation("Methodologies"); + + b.Navigation("PublicationRedirects"); + + b.Navigation("ReleaseVersions"); + + b.Navigation("Releases"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.Navigation("ReleaseRedirects"); + + 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("Publications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.cs new file mode 100644 index 00000000000..891d3b8a8f2 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20241216111431_EES5740_AddFilterHierarchiesColumnToFilesTable.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations +{ + /// + public partial class EES5740_AddFilterHierarchiesColumnToFilesTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FilterHierarchies", + table: "Files", + type: "nvarchar(max)", + nullable: true, + defaultValue: null); + + migrationBuilder.Sql(""" + UPDATE [dbo].[Files] + SET FilterHierarchies = '[]' + WHERE Type = 'Data'; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FilterHierarchies", + table: "Files"); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs index 292daec9d7f..0cb277fbbbf 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs @@ -453,6 +453,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("FilterHierarchies") + .HasColumnType("nvarchar(max)"); + b.Property("ReplacedById") .HasColumnType("uniqueidentifier"); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs new file mode 100644 index 00000000000..8beda1511a8 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs @@ -0,0 +1,12 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model; + +public record DataSetFileFilterHierarchy( + Guid RootFilterId, + List ChildFilterIds, + List RootOptionIds, + List>> Tiers +); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs index 8521a3ede07..8546a5da54e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs @@ -458,6 +458,10 @@ private static void ConfigureFile(ModelBuilder modelBuilder) .HasConversion( v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject(v)); + entity.Property(p => p.FilterHierarchies) + .HasConversion( + v => JsonConvert.SerializeObject(v), + v => JsonConvert.DeserializeObject>(v)); }); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs index 7ada31a8934..2cece595e8d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/File.cs @@ -27,6 +27,8 @@ public class File : ICreatedTimestamp public DataSetFileMeta? DataSetFileMeta { get; set; } + public List? FilterHierarchies { get; set; } + public Guid? ReplacedById { get; set; } public File? ReplacedBy { get; set; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs new file mode 100644 index 00000000000..374b0707817 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs @@ -0,0 +1,13 @@ +namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; + +public record DataSetFileFilterHierarchyViewModel( + Guid RootFilterId, + List ChildFilterIds, + List RootOptionIds, + List Tiers +); + +public record DataSetFileFilterHierarchyTierViewModel( + int Level, + Dictionary> Hierarchy +); From e02f46f13bcacd12dcb1f22cf93563aa32de225e Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Mon, 16 Dec 2024 14:56:02 +0000 Subject: [PATCH 33/40] EES-5740 Generate FilterHierarchies on import --- .../DataSetFileFilterHierarchyViewModel.cs | 5 +- .../DataSetFileFilterHierarchy.cs | 4 +- .../DataSetFileMeta.cs | 3 + .../Models/FilterAndIndicatorValuesReader.cs | 2 +- .../Models/MetaDataFileReader.cs | 2 +- .../Services/DataImportService.cs | 109 ++++++++++++++++++ .../SubjectMetaServiceTests.cs | 1 + .../SubjectMetaService.cs | 15 +++ .../Meta/SubjectMetaViewModel.cs | 4 + 9 files changed, 140 insertions(+), 5 deletions(-) rename src/{GovUk.Education.ExploreEducationStatistics.Content.ViewModels => GovUk.Education.ExploreEducationStatistics.Common/ViewModels}/DataSetFileFilterHierarchyViewModel.cs (72%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/ViewModels/DataSetFileFilterHierarchyViewModel.cs similarity index 72% rename from src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs rename to src/GovUk.Education.ExploreEducationStatistics.Common/ViewModels/DataSetFileFilterHierarchyViewModel.cs index 374b0707817..664eaf53f91 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.ViewModels/DataSetFileFilterHierarchyViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/ViewModels/DataSetFileFilterHierarchyViewModel.cs @@ -1,4 +1,7 @@ -namespace GovUk.Education.ExploreEducationStatistics.Content.ViewModels; +using System; +using System.Collections.Generic; + +namespace GovUk.Education.ExploreEducationStatistics.Common.ViewModels; public record DataSetFileFilterHierarchyViewModel( Guid RootFilterId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs index 8beda1511a8..10fbe3db72c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileFilterHierarchy.cs @@ -6,7 +6,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Content.Model; public record DataSetFileFilterHierarchy( Guid RootFilterId, - List ChildFilterIds, + List ChildFilterIds, // in order of the tiers List RootOptionIds, - List>> Tiers + List>> Tiers // also in order i.e. Tier[0] is root -> childFilterIds[0], Tier[1] is childFilterIds[0] -> childFilterIds[1], etc. ); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileMeta.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileMeta.cs index aefd4941516..f5f5e6b6a86 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileMeta.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/DataSetFileMeta.cs @@ -44,6 +44,9 @@ public class FilterMeta public string? Hint { get; set; } public required string ColumnName { get; set; } + + [JsonIgnore] + public string? GroupCsvColumn { get; set; } } public class IndicatorMeta diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs index b97817cbce8..db0957b5475 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs @@ -77,7 +77,7 @@ public string GetFilterItemLabel( return rowValues[columnIndex].Trim().NullIfWhiteSpace() ?? DefaultFilterItemLabel; } - public string GetFilterGroupLabel( + public string GetFilterGroupLabel( // @MarkFix IReadOnlyList rowValues, Guid filterId) { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs index 1ab5d82993a..655711916b3 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs @@ -42,7 +42,7 @@ public MetaRow GetMetaRow(IReadOnlyList rowValues) ColumnName = ReadMetaColumnValue(MetaColumns.col_name, rowValues), ColumnType = Enum.Parse(columnType!), Label = ReadMetaColumnValue(MetaColumns.label, rowValues), - FilterGroupingColumn = ReadMetaColumnValue(MetaColumns.filter_grouping_column, rowValues), + FilterGroupingColumn = ReadMetaColumnValue(MetaColumns.filter_grouping_column, rowValues), // @MarkFix FilterHint = ReadMetaColumnValue(MetaColumns.filter_hint, rowValues), IndicatorGrouping = ReadMetaColumnValue(MetaColumns.indicator_grouping, rowValues), IndicatorUnit = EnumUtil.GetFromEnumValue(!indicatorUnit.IsNullOrEmpty() ? indicatorUnit : ""), diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs index 5b29fd83184..adabc4a28d5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs @@ -1,13 +1,17 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using AngleSharp.Dom; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Processor.Services.Interfaces; using Microsoft.EntityFrameworkCore; @@ -189,6 +193,7 @@ public async Task WriteDataSetFileMeta(Guid fileId, Guid subjectId) Label = f.Label, Hint = f.Hint, ColumnName = f.Name, + GroupCsvColumn = f.GroupCsvColumn, }) .ToListAsync(); @@ -230,7 +235,111 @@ public async Task WriteDataSetFileMeta(Guid fileId, Guid subjectId) contentDbContext.DataSetFileVersionGeographicLevels.AddRange( dataSetFileVersionGeographicLevels); + file.FilterHierarchies = await GenerateFilterHierarchies(statisticsDbContext, filters); + await contentDbContext.SaveChangesAsync(); } + + private static async Task> GenerateFilterHierarchies( + StatisticsDbContext statisticsDbContext, + List filters) + { + var rootFilters = filters + .Where(parentFilter => parentFilter.GroupCsvColumn == null + && filters.Any(childFilter => + parentFilter.ColumnName == childFilter.GroupCsvColumn)); + + var hierarchies = new List(); + + foreach (var rootFilter in rootFilters) + { + var hierarchy = await GenerateFilterHierarchy(statisticsDbContext, rootFilter, filters); + hierarchies.Add(hierarchy); + } + + return hierarchies; + } + + private static async Task GenerateFilterHierarchy( + StatisticsDbContext statisticsDbContext, + FilterMeta rootFilter, + List filters) + { + var rootFilterItemIds = statisticsDbContext.FilterItem + .AsNoTracking() + .Where(fi => fi.FilterGroup.FilterId == rootFilter.Id) + .Select(fi => fi.Id) + .ToHashSet(); + + var childFilterIds = new List(); + var tiers = new List>>(); + + var parentFilter = rootFilter; + var parentFilterItemIds = rootFilterItemIds; + var childFilter = filters + .Single(f => f.GroupCsvColumn == parentFilter.ColumnName); + + while (true) // one iteration of loop per tier + { + var currentParentFilterId = parentFilter.Id; // avoid closure madness + var currentChildFilterId = childFilter.Id; + + childFilterIds.Add(currentChildFilterId); + + var filterItemRelationships = await statisticsDbContext.FilterItem + .AsNoTracking() + .Where(fi => fi.FilterGroup.FilterId == currentParentFilterId) + .SelectMany(parentFilterItem => + statisticsDbContext.ObservationFilterItem + .AsNoTracking() + .Where(childOfi => + childOfi.FilterId == currentChildFilterId + && statisticsDbContext.ObservationFilterItem.Any(parentOfi => + childOfi.ObservationId == parentOfi.ObservationId + && parentOfi.FilterItemId == parentFilterItem.Id)) + .Select(childOfi => new + { + FilterItemId = childOfi.FilterItem.Id, + ParentItemId = parentFilterItem.Id, + }) + .ToList()) + .Distinct() + .ToListAsync(); + + var tier = new Dictionary>(); + foreach (var parentFilterItemId in parentFilterItemIds) + { + var childFilterItemIdsForParentItem = filterItemRelationships + .Where(childFilterItem => childFilterItem.ParentItemId == parentFilterItemId) + .Select(childFilterItem => childFilterItem.FilterItemId) + .ToList(); + + tier.Add(parentFilterItemId, childFilterItemIdsForParentItem); + } + + tiers.Add(tier); + + // check whether we're finished + var newChildFilter = filters + .SingleOrDefault(newChildFilter => newChildFilter.GroupCsvColumn == childFilter.ColumnName); + if (newChildFilter == null) + { + break; + } + + // if not finished, prepare for next iteration of loop + parentFilter = childFilter; + childFilter = newChildFilter; + parentFilterItemIds = filterItemRelationships + .Select(childFilterItem => childFilterItem.FilterItemId) + .ToHashSet(); + } + + return new DataSetFileFilterHierarchy( + RootFilterId: rootFilter.Id, + ChildFilterIds: childFilterIds, + RootOptionIds: [.. rootFilterItemIds], + Tiers: tiers); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs index 90d7b753b8a..8500bbc7cf7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs @@ -662,6 +662,7 @@ public async Task FilterSubjectMeta_FiltersAndIndicators() { SubjectId = releaseSubject.SubjectId, Type = FileType.Data, + FilterHierarchies = [], // @MarkFix }, }; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services/SubjectMetaService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Services/SubjectMetaService.cs index e4297c34567..b65097f1a53 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Services/SubjectMetaService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Services/SubjectMetaService.cs @@ -11,6 +11,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; 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; using GovUk.Education.ExploreEducationStatistics.Data.Model; @@ -24,6 +25,7 @@ using GovUk.Education.ExploreEducationStatistics.Data.ViewModels.Meta; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using static GovUk.Education.ExploreEducationStatistics.Common.Validators.ValidationUtils; @@ -198,6 +200,7 @@ private async Task GetSubjectMetaViewModelFromRequest( var stopwatch = Stopwatch.StartNew(); var releaseFile = await contentDbContext.ReleaseFiles + .Include(rf => rf.File) .Where(rf => rf.ReleaseVersionId == releaseSubject.ReleaseVersionId && rf.File.SubjectId == releaseSubject.SubjectId && rf.File.Type == FileType.Data) @@ -222,10 +225,22 @@ await observationService.GetMatchedObservations( releaseSubject.SubjectId, releaseFile.IndicatorSequence); logger.LogTrace("Got Indicators in {Time} ms", stopwatch.Elapsed.TotalMilliseconds); + var filterHierarchies = releaseFile.File.FilterHierarchies! + .Select(fh => new DataSetFileFilterHierarchyViewModel( + RootFilterId: fh.RootFilterId, + ChildFilterIds: fh.ChildFilterIds, + RootOptionIds: fh.RootOptionIds, + Tiers: fh.Tiers + .Select((tier, index) => + new DataSetFileFilterHierarchyTierViewModel(index, tier)) + .ToList() + )).ToList(); + return new SubjectMetaViewModel { Filters = filters, Indicators = indicators, + FilterHierarchies = filterHierarchies, }; } default: diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs index 058f296e945..7b4bbbcec7d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs @@ -1,3 +1,5 @@ +using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; + namespace GovUk.Education.ExploreEducationStatistics.Data.ViewModels.Meta; public record SubjectMetaViewModel @@ -9,4 +11,6 @@ public record SubjectMetaViewModel public Dictionary Locations { get; set; } = new(); public TimePeriodsMetaViewModel TimePeriod { get; set; } = new(); + + public List? FilterHierarchies { get; set; } = null; } From 582e268462c3fc7a5797017c7dc281e8bb092d7b Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 17 Dec 2024 11:54:01 +0000 Subject: [PATCH 34/40] EES-5740 Add filter hierarchy test data sets --- .../Models/FilterAndIndicatorValuesReader.cs | 2 +- .../Models/MetaDataFileReader.cs | 2 +- .../filter-hierarchies/filter_hierarchy1.csv | 17 +++++++++++++++++ .../filter_hierarchy1.meta.csv | 4 ++++ .../filter-hierarchies/filter_hierarchy2.csv | 17 +++++++++++++++++ .../filter_hierarchy2.meta.csv | 5 +++++ .../filter-hierarchies/filter_hierarchy3.csv | 17 +++++++++++++++++ .../filter_hierarchy3.meta.csv | 7 +++++++ 8 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.csv create mode 100644 tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.meta.csv create mode 100644 tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.csv create mode 100644 tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.meta.csv create mode 100644 tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.csv create mode 100644 tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.meta.csv diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs index db0957b5475..b97817cbce8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/FilterAndIndicatorValuesReader.cs @@ -77,7 +77,7 @@ public string GetFilterItemLabel( return rowValues[columnIndex].Trim().NullIfWhiteSpace() ?? DefaultFilterItemLabel; } - public string GetFilterGroupLabel( // @MarkFix + public string GetFilterGroupLabel( IReadOnlyList rowValues, Guid filterId) { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs index 655711916b3..1ab5d82993a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Models/MetaDataFileReader.cs @@ -42,7 +42,7 @@ public MetaRow GetMetaRow(IReadOnlyList rowValues) ColumnName = ReadMetaColumnValue(MetaColumns.col_name, rowValues), ColumnType = Enum.Parse(columnType!), Label = ReadMetaColumnValue(MetaColumns.label, rowValues), - FilterGroupingColumn = ReadMetaColumnValue(MetaColumns.filter_grouping_column, rowValues), // @MarkFix + FilterGroupingColumn = ReadMetaColumnValue(MetaColumns.filter_grouping_column, rowValues), FilterHint = ReadMetaColumnValue(MetaColumns.filter_hint, rowValues), IndicatorGrouping = ReadMetaColumnValue(MetaColumns.indicator_grouping, rowValues), IndicatorUnit = EnumUtil.GetFromEnumValue(!indicatorUnit.IsNullOrEmpty() ? indicatorUnit : ""), diff --git a/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.csv b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.csv new file mode 100644 index 00000000000..14e94abe616 --- /dev/null +++ b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.csv @@ -0,0 +1,17 @@ +time_period,time_identifier,geographic_level,country_code,country_name,old_la_code,new_la_code,la_name,filter_one,filter_two,ind_one +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,One,1 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,One,2 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneOne,One,3 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,One,4 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,Two,5 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneOne,Two,6 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,Two,7 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,Two,8 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,Three,9 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,Three,10 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,Three,11 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,Three,12 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,Four,13 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,Four,14 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,Four,15 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,Four,16 diff --git a/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.meta.csv b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.meta.csv new file mode 100644 index 00000000000..14834096d63 --- /dev/null +++ b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy1.meta.csv @@ -0,0 +1,4 @@ +col_name,col_type,label,indicator_grouping,indicator_unit,indicator_dp,filter_hint,filter_grouping_column +ind_one,Indicator,Indicator one,,,,, +filter_one,Filter,Filter one,,,,, +filter_two,Filter,Filter two,,,,,filter_one diff --git a/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.csv b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.csv new file mode 100644 index 00000000000..53174a16549 --- /dev/null +++ b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.csv @@ -0,0 +1,17 @@ +time_period,time_identifier,geographic_level,country_code,country_name,old_la_code,new_la_code,la_name,filter_one,filter_two,filter_three,ind_one +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoOne,ThreeOne,1 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoOne,ThreeOne,2 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneOne,TwoOne,ThreeTwo,3 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoOne,ThreeTwo,4 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoTwo,ThreeThree,5 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneOne,TwoTwo,ThreeThree,6 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoTwo,ThreeFour,7 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoTwo,ThreeFour,8 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoThree,ThreeFive,9 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoThree,ThreeFive,10 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoThree,ThreeSix,11 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoThree,ThreeSix,12 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoFour,ThreeSeven,13 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoFour,ThreeSeven,14 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoFour,ThreeEight,15 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoFour,ThreeEight,16 diff --git a/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.meta.csv b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.meta.csv new file mode 100644 index 00000000000..8d656a54566 --- /dev/null +++ b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy2.meta.csv @@ -0,0 +1,5 @@ +col_name,col_type,label,indicator_grouping,indicator_unit,indicator_dp,filter_hint,filter_grouping_column +ind_one,Indicator,Indicator one,,,,, +filter_one,Filter,Filter one,,,,, +filter_two,Filter,Filter two,,,,,filter_one +filter_three,Filter,Filter three,,,,,filter_two diff --git a/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.csv b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.csv new file mode 100644 index 00000000000..f473ca9fdb2 --- /dev/null +++ b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.csv @@ -0,0 +1,17 @@ +time_period,time_identifier,geographic_level,country_code,country_name,old_la_code,new_la_code,la_name,filter_one,filter_two,filter_three,other_filter_one,other_filter_two,ind_one +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoOne,ThreeOne,OneOne,TwoOne,1 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoOne,ThreeOne,OneOne,TwoTwo,2 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneOne,TwoOne,ThreeTwo,OneOne,TwoThree,3 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoOne,ThreeTwo,OneTwo,TwoThree,4 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoTwo,ThreeThree,OneTwo,TwoFour,5 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneOne,TwoTwo,ThreeThree,OneTwo,TwoFive,6 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoTwo,ThreeFour,OneThree,TwoSix,7 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneOne,TwoTwo,ThreeFour,OneThree,TwoSix,8 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoThree,ThreeFive,OneThree,TwoSeven,9 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoThree,ThreeFive,OneFour,TwoEight,10 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoThree,ThreeSix,OneFour,TwoNine,11 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoThree,ThreeSix,OneFour,TwoNine,12 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoFour,ThreeSeven,OneFive,TwoTen,13 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoFour,ThreeSeven,OneFive,TwoEleven,14 +2018,Calendar year,Local authority,E92000001,England,330,E08000025,Birmingham,OneTwo,TwoFour,ThreeEight,OneFive,TwoTwelve,15 +2018,Calendar year,Local authority,E92000001,England,370,E08000016,Barnsley,OneTwo,TwoFour,ThreeEight,OneSix,TwoTwelve,16 diff --git a/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.meta.csv b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.meta.csv new file mode 100644 index 00000000000..bc25785e670 --- /dev/null +++ b/tests/test-data/files/small-files/filter-hierarchies/filter_hierarchy3.meta.csv @@ -0,0 +1,7 @@ +col_name,col_type,label,indicator_grouping,indicator_unit,indicator_dp,filter_hint,filter_grouping_column +ind_one,Indicator,Indicator one,,,,, +filter_one,Filter,Filter one,,,,, +filter_two,Filter,Filter two,,,,,filter_one +filter_three,Filter,Filter three,,,,,filter_two +other_filter_one,Filter,Other filter one,,,,, +other_filter_two,Filter,Other filter two,,,,,other_filter_one From a0778c392b04ea3f329276de5a7ea95ad9df48ce Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Thu, 19 Dec 2024 13:50:41 +0000 Subject: [PATCH 35/40] EES-5740 Add tests --- .../Services/DataImportServiceTests.cs | 317 ++++++++++++++++++ .../Services/DataImportService.cs | 6 +- .../SubjectMetaServiceTests.cs | 2 +- 3 files changed, 319 insertions(+), 6 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs index de19c60ad62..14f392dddc8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor.Tests/Services/DataImportServiceTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; 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.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; @@ -313,6 +314,322 @@ public async Task WriteDataSetMetaFile_Success() Assert.Equal(indicatorGroup.Indicators[0].Id, dbIndicator.Id); Assert.Equal(indicatorGroup.Indicators[0].Label, dbIndicator.Label); Assert.Equal(indicatorGroup.Indicators[0].Name, dbIndicator.ColumnName); + + Assert.NotNull(updatedFile.FilterHierarchies); + Assert.Empty(updatedFile.FilterHierarchies); + } + } + + [Fact] + public async Task GenerateFilterHierarchies_Success() + { + var subject = new Subject { Id = Guid.NewGuid() }; + + var filters = new List + { + new() + { + SubjectId = subject.Id, + Name = "root_filter", + FilterGroups = [ + new FilterGroup + { + Label = "Root filter group", + FilterItems = [ + new FilterItem { Label = "RootFilterItem0", }, + new FilterItem { Label = "RootFilterItem1", }, + ], + }, + ], + GroupCsvColumn = null, + }, + new() + { + SubjectId = subject.Id, + Name = "child_filter1", + FilterGroups = [ + new FilterGroup + { + Label = "Child filter 1 group", + FilterItems = [ + new FilterItem { Label = "ChildFilter1Item0", }, + new FilterItem { Label = "ChildFilter1Item1", }, + new FilterItem { Label = "ChildFilter1Item2", }, + ], + }, + ], + GroupCsvColumn = "root_filter", + }, + new() + { + SubjectId = subject.Id, + Name = "child_filter2", + FilterGroups = [ + new FilterGroup + { + Label = "Child filter 2 group", + FilterItems = [ + new FilterItem { Label = "ChildFilter2Item0", }, + new FilterItem { Label = "ChildFilter2Item1", }, + new FilterItem { Label = "ChildFilter2Item2", }, + new FilterItem { Label = "ChildFilter2Item3", }, + new FilterItem { Label = "ChildFilter2Item4", }, + new FilterItem { Label = "ChildFilter2Item5", }, + ], + }, + ], + GroupCsvColumn = "child_filter1", + } + }; + + var rootFilterItem0 = filters[0].FilterGroups[0].FilterItems[0]; + var rootFilterItem1 = filters[0].FilterGroups[0].FilterItems[1]; + var childFilter1Item0 = filters[1].FilterGroups[0].FilterItems[0]; + var childFilter1Item1 = filters[1].FilterGroups[0].FilterItems[1]; + var childFilter1Item2 = filters[1].FilterGroups[0].FilterItems[2]; + var childFilter2Item0 = filters[2].FilterGroups[0].FilterItems[0]; + var childFilter2Item1 = filters[2].FilterGroups[0].FilterItems[1]; + var childFilter2Item2 = filters[2].FilterGroups[0].FilterItems[2]; + var childFilter2Item3 = filters[2].FilterGroups[0].FilterItems[3]; + var childFilter2Item4 = filters[2].FilterGroups[0].FilterItems[4]; + var childFilter2Item5 = filters[2].FilterGroups[0].FilterItems[5]; + + var observation0 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilterItem0, + childFilter1Item0, + childFilter2Item0, + ]); + + var observation1 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilterItem1, + childFilter1Item1, + childFilter2Item1, + ]); + + var observation2 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilterItem1, + childFilter1Item1, + childFilter2Item2, + ]); + + var observation3 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilterItem1, + childFilter1Item2, + childFilter2Item3, + ]); + + var observation4 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilterItem1, + childFilter1Item2, + childFilter2Item4, + ]); + + var observation5 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilterItem1, + childFilter1Item2, + childFilter2Item5, + ]); + + var statisticsDbContextId = Guid.NewGuid().ToString(); + await using (var statisticsDbContext = InMemoryStatisticsDbContext(statisticsDbContextId)) + { + statisticsDbContext.Filter.AddRange(filters); + statisticsDbContext.Observation.AddRange( + observation0, observation1, observation2, observation3, observation4, observation5); + await statisticsDbContext.SaveChangesAsync(); + } + + await using (var statisticsDbContext = InMemoryStatisticsDbContext(statisticsDbContextId)) + { + var results = await DataImportService.GenerateFilterHierarchies( + statisticsDbContext, + filters.Select(f => new FilterMeta + { + Id = f.Id, + Label = f.Label, + Hint = f.Hint, + ColumnName = f.Name, + GroupCsvColumn = f.GroupCsvColumn, + }).ToList()); + + var hierarchy = Assert.Single(results); + Assert.Equal(filters[0].Id, hierarchy.RootFilterId); + Assert.Equal([filters[1].Id, filters[2].Id], hierarchy.ChildFilterIds); + + Assert.Equal( + [rootFilterItem0.Id, rootFilterItem1.Id], + hierarchy.RootOptionIds); + + Assert.Equal(2, hierarchy.Tiers.Count); + + // tier 0 + Assert.Equal(2, hierarchy.Tiers[0].Count); // two root filter items + Assert.Equal([rootFilterItem0.Id, rootFilterItem1.Id], hierarchy.Tiers[0].Keys.ToList()); + Assert.Equal([childFilter1Item0.Id], hierarchy.Tiers[0][rootFilterItem0.Id]); + Assert.Equal([childFilter1Item1.Id, childFilter1Item2.Id], hierarchy.Tiers[0][rootFilterItem1.Id]); + + // tier 1 + Assert.Equal(3, hierarchy.Tiers[1].Count); // three child filter 1 items + Assert.Equal( + [childFilter1Item0.Id, childFilter1Item1.Id, childFilter1Item2.Id], + hierarchy.Tiers[1].Keys.ToList()); + Assert.Equal([childFilter2Item0.Id], hierarchy.Tiers[1][childFilter1Item0.Id]); + Assert.Equal([childFilter2Item1.Id, childFilter2Item2.Id], hierarchy.Tiers[1][childFilter1Item1.Id]); + Assert.Equal([childFilter2Item3.Id, childFilter2Item4.Id, childFilter2Item5.Id], + hierarchy.Tiers[1][childFilter1Item2.Id]); + } + } + + [Fact] + public async Task GenerateFilterHierarchies_TwoRootFilters_Success() + { + var subject = new Subject { Id = Guid.NewGuid() }; + + var filters = new List + { + new() + { + SubjectId = subject.Id, + Name = "root_filter0", + FilterGroups = [ + new FilterGroup + { + Label = "Root filter 0 group", + FilterItems = [ + new FilterItem { Label = "RootFilter0Item0", }, + ], + }, + ], + GroupCsvColumn = null, + }, + new() + { + SubjectId = subject.Id, + Name = "child_filter0", + FilterGroups = [ + new FilterGroup + { + Label = "Child filter 0 group", + FilterItems = [ + new FilterItem { Label = "ChildFilter0Item0", }, + ], + }, + ], + GroupCsvColumn = "root_filter0", + }, + new() + { + SubjectId = subject.Id, + Name = "root_filter1", + FilterGroups = [ + new FilterGroup + { + Label = "Root filter 1 group", + FilterItems = [ + new FilterItem { Label = "RootFilter1Item0", }, + ], + }, + ], + GroupCsvColumn = null, + }, + new() + { + SubjectId = subject.Id, + Name = "child_filter1", + FilterGroups = [ + new FilterGroup + { + Label = "Child filter 1 group", + FilterItems = [ + new FilterItem { Label = "ChildFilter1Item0", }, + ], + }, + ], + GroupCsvColumn = "root_filter1", + }, + }; + + var rootFilter0Item0 = filters[0].FilterGroups[0].FilterItems[0]; + var childFilter0Item0 = filters[1].FilterGroups[0].FilterItems[0]; + var rootFilter1Item0 = filters[2].FilterGroups[0].FilterItems[0]; + var childFilter1Item0 = filters[3].FilterGroups[0].FilterItems[0]; + + var observation0 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilter0Item0, + childFilter0Item0, + ]); + + var observation1 = _fixture.DefaultObservation() + .WithSubject(subject) + .WithFilterItems([ + rootFilter1Item0, + childFilter1Item0, + ]); + + var statisticsDbContextId = Guid.NewGuid().ToString(); + await using (var statisticsDbContext = InMemoryStatisticsDbContext(statisticsDbContextId)) + { + statisticsDbContext.Filter.AddRange(filters); + statisticsDbContext.Observation.AddRange(observation0, observation1); + await statisticsDbContext.SaveChangesAsync(); + } + + await using (var statisticsDbContext = InMemoryStatisticsDbContext(statisticsDbContextId)) + { + var results = await DataImportService.GenerateFilterHierarchies( + statisticsDbContext, + filters.Select(f => new FilterMeta + { + Id = f.Id, + Label = f.Label, + Hint = f.Hint, + ColumnName = f.Name, + GroupCsvColumn = f.GroupCsvColumn, + }).ToList()); + + Assert.Equal(2, results.Count); + + // hierarchy for root filter 0 + var hierarchy0 = results[0]; + + Assert.Equal(filters[0].Id, hierarchy0.RootFilterId); + Assert.Equal([filters[1].Id], hierarchy0.ChildFilterIds); + + Assert.Equal([rootFilter0Item0.Id], hierarchy0.RootOptionIds); + + Assert.Single(hierarchy0.Tiers); + + Assert.Single(hierarchy0.Tiers[0]); // single root filter 0 item + Assert.Equal([rootFilter0Item0.Id], hierarchy0.Tiers[0].Keys.ToList()); + Assert.Equal([childFilter0Item0.Id], hierarchy0.Tiers[0][rootFilter0Item0.Id]); + + // hierarchy for root filter 1 + var hierarchy1 = results[1]; + + Assert.Equal(filters[2].Id, hierarchy1.RootFilterId); + Assert.Equal([filters[3].Id], hierarchy1.ChildFilterIds); + + Assert.Equal([rootFilter1Item0.Id], hierarchy1.RootOptionIds); + + Assert.Single(hierarchy1.Tiers); + + Assert.Single(hierarchy1.Tiers[0]); // single root filter 1 item + Assert.Equal([rootFilter1Item0.Id], hierarchy1.Tiers[0].Keys.ToList()); + Assert.Equal([childFilter1Item0.Id], hierarchy1.Tiers[0][rootFilter1Item0.Id]); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs index adabc4a28d5..3d7c037dd1a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs @@ -1,17 +1,13 @@ #nullable enable using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using AngleSharp.Dom; -using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Processor.Services.Interfaces; using Microsoft.EntityFrameworkCore; @@ -240,7 +236,7 @@ public async Task WriteDataSetFileMeta(Guid fileId, Guid subjectId) await contentDbContext.SaveChangesAsync(); } - private static async Task> GenerateFilterHierarchies( + public static async Task> GenerateFilterHierarchies( StatisticsDbContext statisticsDbContext, List filters) { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs index 8500bbc7cf7..348958539b6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Services.Tests/SubjectMetaServiceTests.cs @@ -662,7 +662,7 @@ public async Task FilterSubjectMeta_FiltersAndIndicators() { SubjectId = releaseSubject.SubjectId, Type = FileType.Data, - FilterHierarchies = [], // @MarkFix + FilterHierarchies = [], }, }; From 89ff28d6e46c85ba10f27186ce0af7154bc88b65 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 31 Dec 2024 10:34:48 +0000 Subject: [PATCH 36/40] EES-5757 On the test branch, use the test branch Data Factory ARM template --- infrastructure/parameters/test.parameters.json | 2 +- infrastructure/templates/template.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/infrastructure/parameters/test.parameters.json b/infrastructure/parameters/test.parameters.json index fb49adeefd3..e559d0e3a73 100644 --- a/infrastructure/parameters/test.parameters.json +++ b/infrastructure/parameters/test.parameters.json @@ -60,7 +60,7 @@ "value": "G-ZQ5V7CBWMJ" }, "branch": { - "value": "dev" + "value": "test" }, "enableThemeDeletion": { "value": true diff --git a/infrastructure/templates/template.json b/infrastructure/templates/template.json index 0f89037c48a..861e32bb723 100644 --- a/infrastructure/templates/template.json +++ b/infrastructure/templates/template.json @@ -594,7 +594,8 @@ "defaultValue": "master", "allowedValues": [ "master", - "dev" + "dev", + "test" ] }, "skuContentDb": { From 3ddcaffebf38717c4907500fc785b2fae3b78143 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Fri, 20 Dec 2024 11:57:49 +0000 Subject: [PATCH 37/40] EES-XXXX - adding correct restrictions to container limits. Adding default profiles for environments. --- .../application/public-api/publicApiApp.bicep | 4 +- .../public-api/ci/azure-pipelines.yml | 5 +++ .../public-api/ci/jobs/deploy-api-docs.yml | 1 + .../ci/jobs/deploy-infrastructure.yml | 2 + .../public-api/ci/tasks/deploy-bicep.yml | 3 ++ .../public-api/components/containerApp.bicep | 39 +++++++------------ .../templates/public-api/main.bicep | 8 +++- .../public-api/parameters/main-dev.bicepparam | 10 ++++- .../templates/public-api/types.bicep | 5 ++- 9 files changed, 47 insertions(+), 30 deletions(-) diff --git a/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep b/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep index 04edeef331b..2b62ca27c18 100644 --- a/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep +++ b/infrastructure/templates/public-api/application/public-api/publicApiApp.bicep @@ -149,9 +149,11 @@ module apiContainerAppModule '../../components/containerApp.bicep' = { requireAuthentication: false } cpuCores: resourceAndScalingConfig.cpuCores - memorySizeGis: resourceAndScalingConfig.memoryGis + memoryGis: resourceAndScalingConfig.memoryGis minReplicas: resourceAndScalingConfig.minReplicas maxReplicas: resourceAndScalingConfig.maxReplicas + scaleAtConcurrentHttpRequests: resourceAndScalingConfig.scaleAtConcurrentHttpRequests + workloadProfileName: resourceAndScalingConfig.workloadProfileName tagValues: tagValues } } diff --git a/infrastructure/templates/public-api/ci/azure-pipelines.yml b/infrastructure/templates/public-api/ci/azure-pipelines.yml index 5412ea98168..a90d6cc9c57 100644 --- a/infrastructure/templates/public-api/ci/azure-pipelines.yml +++ b/infrastructure/templates/public-api/ci/azure-pipelines.yml @@ -13,6 +13,9 @@ parameters: - name: deployDataProcessor displayName: Does the Data Processor need creating or updating? default: true + - name: deployDocsSite + displayName: Does the Public API static docs site need creating or updating? + default: true - name: deployAlerts displayName: Whether to create or update Azure Monitor alerts during this deploy. default: false @@ -58,6 +61,8 @@ variables: value: ${{ parameters.deployContainerApp }} - name: deployDataProcessor value: ${{ parameters.deployDataProcessor }} + - name: deployDocsSite + value: ${{ parameters.deployDocsSite }} - name: deployAlerts value: ${{ parameters.deployAlerts }} - name: awaitActiveOrchestrations diff --git a/infrastructure/templates/public-api/ci/jobs/deploy-api-docs.yml b/infrastructure/templates/public-api/ci/jobs/deploy-api-docs.yml index a7e17c543f8..1fe51fc720a 100644 --- a/infrastructure/templates/public-api/ci/jobs/deploy-api-docs.yml +++ b/infrastructure/templates/public-api/ci/jobs/deploy-api-docs.yml @@ -10,6 +10,7 @@ parameters: jobs: - deployment: DeployPublicApiDocs displayName: Deploy Public API docs + condition: and(succeeded(), eq(variables.deployDocsSite, true)) dependsOn: ${{ parameters.dependsOn }} environment: ${{ parameters.environment }} strategy: diff --git a/infrastructure/templates/public-api/ci/jobs/deploy-infrastructure.yml b/infrastructure/templates/public-api/ci/jobs/deploy-infrastructure.yml index af0c39481de..056be35954f 100644 --- a/infrastructure/templates/public-api/ci/jobs/deploy-infrastructure.yml +++ b/infrastructure/templates/public-api/ci/jobs/deploy-infrastructure.yml @@ -39,6 +39,7 @@ jobs: deployPsqlFlexibleServer: false deployContainerApp: false deployDataProcessor: false + deployDocsSite: false deployAlerts: false dataProcessorExists: false @@ -68,6 +69,7 @@ jobs: deployPsqlFlexibleServer: $(deployPsqlFlexibleServer) deployContainerApp: $(deployContainerApp) deployDataProcessor: $(deployDataProcessor) + deployDocsSite: $(deployDocsSite) deployAlerts: $(deployAlerts) dataProcessorExists: $(dataProcessorExists) diff --git a/infrastructure/templates/public-api/ci/tasks/deploy-bicep.yml b/infrastructure/templates/public-api/ci/tasks/deploy-bicep.yml index 2e9d5ca3518..39a0196c10a 100644 --- a/infrastructure/templates/public-api/ci/tasks/deploy-bicep.yml +++ b/infrastructure/templates/public-api/ci/tasks/deploy-bicep.yml @@ -20,6 +20,8 @@ parameters: default: true - name: deployDataProcessor type: string + - name: deployDocsSite + type: string - name: deployAlerts type: string - name: dataProcessorExists @@ -55,6 +57,7 @@ steps: deployPsqlFlexibleServer=${{ parameters.deployPsqlFlexibleServer }} \ deployContainerApp=${{ parameters.deployContainerApp }} \ deployDataProcessor=${{ parameters.deployDataProcessor }} \ + deployDocsSite=${{ parameters.deployDocsSite }} \ deployAlerts=${{ parameters.deployAlerts }} \ dataProcessorFunctionAppExists=${{ parameters.dataProcessorExists }} \ dataProcessorAppRegistrationClientId='$(dataProcessorAppRegistrationClientId)' \ diff --git a/infrastructure/templates/public-api/components/containerApp.bicep b/infrastructure/templates/public-api/components/containerApp.bicep index 24ad3fe1562..331ef0c0133 100644 --- a/infrastructure/templates/public-api/components/containerApp.bicep +++ b/infrastructure/templates/public-api/components/containerApp.bicep @@ -29,37 +29,28 @@ param corsPolicy { param workloadProfileName string = 'Consumption' @description('Number of CPU cores the container can use. Can be with a maximum of two decimals.') -@allowed([ - 1 - 2 - 3 - 4 -]) +@minValue(1) +@maxValue(8) param cpuCores int = 4 -@description('Amount of memory (in gibibytes, GiB) allocated to the container up to 4GiB. Can be with a maximum of two decimals. Ratio with CPU cores must be equal to 2.') -@allowed([ - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 -]) -param memorySizeGis int = 8 +@description('Amount of memory (in gibibytes, GiB) allocated to the container up to 32GiB. Can be with a maximum of two decimals. Ratio with CPU cores must be equal to 2.') +@minValue(1) +@maxValue(32) +param memoryGis int = 8 @description('Minimum number of replicas that will be deployed') @minValue(0) -@maxValue(25) param minReplicas int = 1 @description('Maximum number of replicas that will be deployed') @minValue(0) -@maxValue(25) +@maxValue(1000) param maxReplicas int = 3 +@description('Number of concurrent requests required in order to trigger scaling out.') +@minValue(1) +param scaleAtConcurrentHttpRequests int? + @description('Specifies the database connection string') param appSettings { name: string @@ -164,7 +155,7 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { env: appSettings resources: { cpu: json(string(cpuCores)) - memory: '${memorySizeGis}Gi' + memory: '${memoryGis}Gi' } volumeMounts: volumeMounts } @@ -172,16 +163,16 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { scale: { minReplicas: minReplicas maxReplicas: maxReplicas - rules: [ + rules: scaleAtConcurrentHttpRequests != null ? [ { name: 'http-requests' http: { metadata: { - concurrentRequests: '10' + concurrentRequests: string(scaleAtConcurrentHttpRequests) } } } - ] + ] : [] } volumes: volumes } diff --git a/infrastructure/templates/public-api/main.bicep b/infrastructure/templates/public-api/main.bicep index 188456f9e8a..4fe68b7a649 100644 --- a/infrastructure/templates/public-api/main.bicep +++ b/infrastructure/templates/public-api/main.bicep @@ -78,6 +78,9 @@ param deployContainerApp bool = true @description('Does the Data Processor need creating or updating?') param deployDataProcessor bool = true +@description('Does the Public API static docs site need creating or updating?') +param deployDocsSite bool = true + param deployAlerts bool = false @description('Public URLs of other components in the service.') @@ -116,6 +119,7 @@ param publicApiContainerAppConfig ContainerAppResourceConfig = { memoryGis: 8 minReplicas: 0 maxReplicas: 3 + scaleAtConcurrentHttpRequests: 10 workloadProfileName: 'Consumption' } @@ -309,7 +313,7 @@ module apiAppModule 'application/public-api/publicApiApp.bicep' = if (deployCont } // Deploy Public API docs. -module docsModule 'application/public-api/publicApiDocs.bicep' = { +module docsModule 'application/public-api/publicApiDocs.bicep' = if (deployDocsSite) { name: 'publicApiDocsModuleDeploy' params: { appSku: docsAppSku @@ -321,7 +325,7 @@ module docsModule 'application/public-api/publicApiDocs.bicep' = { var docsRewriteSetName = '${publicApiResourcePrefix}-docs-rewrites' // Create an Application Gateway to serve public traffic for the Public API Container App. -module appGatewayModule 'application/shared/appGateway.bicep' = if (deployContainerApp) { +module appGatewayModule 'application/shared/appGateway.bicep' = if (deployContainerApp && deployDocsSite) { name: 'appGatewayModuleDeploy' params: { location: location diff --git a/infrastructure/templates/public-api/parameters/main-dev.bicepparam b/infrastructure/templates/public-api/parameters/main-dev.bicepparam index d9b2425967e..38b87677050 100644 --- a/infrastructure/templates/public-api/parameters/main-dev.bicepparam +++ b/infrastructure/templates/public-api/parameters/main-dev.bicepparam @@ -18,8 +18,16 @@ param publicApiContainerAppConfig = { cpuCores: 4 memoryGis: 8 minReplicas: 1 - maxReplicas: 16 + maxReplicas: 100 + scaleAtConcurrentHttpRequests: 5 workloadProfileName: 'Consumption' } +param publicApiContainerAppWorkloadProfiles = [{ + name: 'D8' + workloadProfileType: 'D8' + minimumCount: 0 + maximumCount: 10 +}] + param enableThemeDeletion = true diff --git a/infrastructure/templates/public-api/types.bicep b/infrastructure/templates/public-api/types.bicep index 44ea8544cfa..76ab2098458 100644 --- a/infrastructure/templates/public-api/types.bicep +++ b/infrastructure/templates/public-api/types.bicep @@ -226,10 +226,11 @@ type StaticWebAppSku = 'Free' | 'Standard' @export() type ContainerAppResourceConfig = { workloadProfileName: string - minReplicas: int - maxReplicas: int cpuCores: int memoryGis: int + minReplicas: int + maxReplicas: int + scaleAtConcurrentHttpRequests: int? } @export() From 7035c31279eef50645ba9eb9155013bf558aefd1 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 31 Dec 2024 12:05:16 +0000 Subject: [PATCH 38/40] EES-5740 Changes in response to PR comments --- .../Services/DataImportService.cs | 3 ++- .../Meta/SubjectMetaViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs index 3d7c037dd1a..a0709354c82 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.Processor/Services/DataImportService.cs @@ -275,7 +275,8 @@ private static async Task GenerateFilterHierarchy( var childFilter = filters .Single(f => f.GroupCsvColumn == parentFilter.ColumnName); - while (true) // one iteration of loop per tier + // Loop over each parent/child or tier, starting with the root filter, until no child is found + while (true) { var currentParentFilterId = parentFilter.Id; // avoid closure madness var currentChildFilterId = childFilter.Id; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs index 7b4bbbcec7d..c322b952c25 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Data.ViewModels/Meta/SubjectMetaViewModel.cs @@ -12,5 +12,5 @@ public record SubjectMetaViewModel public TimePeriodsMetaViewModel TimePeriod { get; set; } = new(); - public List? FilterHierarchies { get; set; } = null; + public List? FilterHierarchies { get; set; } } From e24e278b47bb7ad753bac0a1ad921b277794c362 Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 31 Dec 2024 14:57:22 +0000 Subject: [PATCH 39/40] EES-5598 Fix changing geog lvl on mobile --- .../src/modules/data-catalogue/DataCataloguePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx index 039b1dd1c12..fff064ee12b 100644 --- a/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx +++ b/src/explore-education-statistics-frontend/src/modules/data-catalogue/DataCataloguePage.tsx @@ -52,10 +52,9 @@ import omit from 'lodash/omit'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { ParsedUrlQuery } from 'querystring'; -import locationLevelsMap, { +import { GeographicLevelCode, geographicLevelCodesMap, - LocationLevelKey, } from '@common/utils/locationLevelsMap'; const defaultPageTitle = 'Data catalogue'; @@ -454,6 +453,7 @@ const DataCataloguePage: NextPage = ({ showTypeFilter }) => { publicationId={publicationId} publications={publications} releaseId={releaseId} + geographicLevel={geographicLevel} releases={releases} showTypeFilter={showTypeFilter} themeId={themeId} From 22338db960a9a734a44d9cb7f933e97c43354f0a Mon Sep 17 00:00:00 2001 From: Mark Youngman Date: Tue, 31 Dec 2024 15:46:13 +0000 Subject: [PATCH 40/40] EES-5758 Update data block table title limit to 220 chars --- .../Requests/DataBlockRequests.cs | 4 ++-- .../components/DataBlockDetailsForm.tsx | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/DataBlockRequests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/DataBlockRequests.cs index d81e49514cd..37b38fde281 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/DataBlockRequests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Requests/DataBlockRequests.cs @@ -27,7 +27,7 @@ public Validator() { RuleFor(request => request.Heading) .NotEmpty() - .MaximumLength(120); + .MaximumLength(220); RuleFor(request => request.Name) .NotEmpty(); @@ -58,7 +58,7 @@ public Validator() { RuleFor(request => request.Heading) .NotEmpty() - .MaximumLength(120); + .MaximumLength(220); RuleFor(request => request.Name) .NotEmpty(); 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 bc0ba0fa6c6..6173b5b6f56 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 @@ -21,8 +21,10 @@ interface FormValues { export type DataBlockDetailsFormValues = OmitStrict; -const titleMaxLength = 120; -const descriptionMaxLength = 200; +const dataBlockTableTitleMaxLength = 220; +const featuredTableNameMaxLength = 120; +const featuredTableDescriptionMaxLength = 200; + const formId = 'dataBlockDetailsForm'; interface Props { @@ -61,8 +63,8 @@ const DataBlockDetailsForm = ({ heading: Yup.string() .required('Enter a table title') .max( - titleMaxLength, - `Table title must be ${titleMaxLength} characters or less`, + dataBlockTableTitleMaxLength, + `Table title must be ${dataBlockTableTitleMaxLength} characters or less`, ), source: Yup.string(), highlightName: Yup.string().when('isHighlight', { @@ -71,8 +73,8 @@ const DataBlockDetailsForm = ({ s .required('Enter a featured table name') .max( - titleMaxLength, - `Featured table name must be ${titleMaxLength} characters or less`, + featuredTableNameMaxLength, + `Featured table name must be ${featuredTableNameMaxLength} characters or less`, ), }), highlightDescription: Yup.string().when('isHighlight', { @@ -81,8 +83,8 @@ const DataBlockDetailsForm = ({ s .required('Enter a featured table description') .max( - descriptionMaxLength, - `Featured table description must be ${descriptionMaxLength} characters or less`, + featuredTableDescriptionMaxLength, + `Featured table description must be ${featuredTableDescriptionMaxLength} characters or less`, ), }), isHighlight: Yup.boolean(), @@ -119,7 +121,7 @@ const DataBlockDetailsForm = ({ onBlur={() => { onTitleChange?.(getValues('heading')); }} - maxLength={titleMaxLength} + maxLength={dataBlockTableTitleMaxLength} /> @@ -145,14 +147,14 @@ const DataBlockDetailsForm = ({ label="Featured table name" hint="We will show this name to table builder users as a featured table" className="govuk-!-width-two-thirds" - maxLength={titleMaxLength} + maxLength={featuredTableNameMaxLength} /> name="highlightDescription" label="Featured table description" hint="Describe the contents of this featured table to table builder users" className="govuk-!-width-two-thirds" - maxLength={descriptionMaxLength} + maxLength={featuredTableDescriptionMaxLength} /> }