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..f5906b91e79 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 { @@ -327,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); @@ -678,7 +669,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 +679,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.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..de4e453532c 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,26 +2988,16 @@ public async Task UpdateReleaseSeries() new ReleaseSeriesItemUpdateRequest { LegacyLinkDescription = "Legacy link new", - LegacyLinkUrl = "https://test.com/new", - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2021.Id + LegacyLinkUrl = "https://test.com/new" }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2020.Id - }, - new ReleaseSeriesItemUpdateRequest - { - ReleaseId = release2022.Id - } + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2022.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2020.Id }, + new ReleaseSeriesItemUpdateRequest { ReleaseId = release2021.Id } ]); VerifyAllMocks(publicationCacheService); var viewModels = result.AssertRight(); - Assert.Equal(4, viewModels.Count); Assert.True(viewModels[0].IsLegacyLink); @@ -3009,11 +3009,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 +3025,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 +3044,217 @@ 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, actualPublication.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.Equal(release2021.Id, viewModels[0].ReleaseId); + Assert.True(viewModels[0].IsLatest); + Assert.True(viewModels[0].IsPublished); + + Assert.Equal(release2020.Id, viewModels[1].ReleaseId); + Assert.False(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 2021 should now be the publication's latest published release + // version since it was positioned as the first release + Assert.Equal(expectedLatestPublishedReleaseVersionId, + actualPublication.LatestPublishedReleaseVersionId); + } + } + + [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); } } @@ -3153,14 +3361,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}", @@ -3246,6 +3448,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 +3461,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/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/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/Services/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs index 1341e018e56..43b256454ac 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs @@ -31,59 +31,36 @@ 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, + IReleaseCacheService releaseCacheService, + 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 +75,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 +92,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 +102,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 +111,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 +123,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 +139,7 @@ public async Task> UpdatePublication( { if (publication.SupersededById != updatedPublication.SupersededById) { - return await _userService.CheckCanUpdatePublication(); + return await userService.CheckCanUpdatePublication(); } return Unit.Instance; @@ -180,7 +157,7 @@ public async Task> UpdatePublication( { if (publication.ThemeId != updatedPublication.ThemeId) { - return await _userService.CheckCanUpdatePublication(); + return await userService.CheckCanUpdatePublication(); } return Unit.Instance; @@ -206,7 +183,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 +192,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 +211,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 +225,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 +246,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 +290,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 +311,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 +329,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 +358,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 +392,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 +413,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 +432,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 +460,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 +472,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 +485,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 +507,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(); @@ -547,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 { @@ -556,9 +551,19 @@ public async Task>> LegacyLinkUrl = request.LegacyLinkUrl, }).ToList(); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); - await _publicationCacheService.UpdatePublication(publication.Slug); + // 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); }); @@ -567,7 +572,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 +580,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 +591,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 +599,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 +619,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 +634,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 +644,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; 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 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/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.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.Tests/Repository/ReleaseVersionRepositoryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/ReleaseVersionRepositoryTests.cs index 8cf258824ce..54a95d1034a 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; @@ -17,220 +16,152 @@ public class ReleaseVersionRepositoryTests { private readonly DataFixture _dataFixture = new(); - public class GetLatestPublishedReleaseVersionTests : ReleaseVersionRepositoryTests + public class GetLatestPublishedReleaseVersionByReleaseSlugTests : 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: 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); + var result = + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22"); - // 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 }); + // 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) - .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: 2021)]) + .GenerateTuple2(); + + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id)); - } + var result = + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication1.Id, releaseSlug: "2021-22"); - [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); + // Expect the result to be from the specified publication + var expectedReleaseVersion = publication1.Releases.Single().Versions.Single(); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id)); + Assert.NotNull(result); + Assert.Equal(expectedReleaseVersion.Id, result.Id); } [Fact] - public async Task PublicationDoesNotExist_ReturnsNull() + public async Task PublicationHasNoPublishedReleaseVersions_ReturnsNull() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1) - .Generate(1)); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 0, draftVersion: true, year: 2021)]); var contextId = await AddTestData(publication); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(Guid.NewGuid())); + Assert.Null( + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22")); } [Fact] - public async Task SpecificReleaseSlug_Success() + public async Task PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNull() { - 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); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - var result = await repository.GetLatestPublishedReleaseVersion(publications[0].Id, "2021-22"); + .WithReleases([_dataFixture.DefaultRelease(publishedVersions: 1, year: 2020)]); - // 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 }); - - Assert.NotNull(result); - Assert.Equal(expectedReleaseVersion.Id, result.Id); - } - - [Fact] - public async Task SpecificReleaseSlug_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))) - .GenerateList(2); - - 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, "2021-22")); + Assert.Null( + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22")); } [Fact] - public async Task SpecificReleaseSlug_PublicationHasNoPublishedReleaseVersionsMatchingSlug_ReturnsNull() + public async Task PublicationHasNoReleaseVersions_ReturnsNull() { - Publication publication = _dataFixture - .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1, year: 2020) - .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.GetLatestPublishedReleaseVersion(publication.Id, "2021-22")); + Assert.Null( + await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publication.Id, releaseSlug: "2021-22")); } [Fact] - public async Task SpecificReleaseSlug_PublicationHasNoReleaseVersions_ReturnsNull() + public async Task PublicationDoesNotExist_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))) - .GenerateList(2); - - var contextId = await AddTestData(publications); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); + var repository = BuildRepository(); - Assert.Null(await repository.GetLatestPublishedReleaseVersion(publications[0].Id, "2021-22")); + Assert.Null(await repository.GetLatestPublishedReleaseVersionByReleaseSlug(publicationId: Guid.NewGuid(), + releaseSlug: "2021-22")); } + } + public class GetLatestReleaseVersionTests : ReleaseVersionRepositoryTests + { [Fact] - public async Task SpecificReleaseSlug_PublicationDoesNotExist_ReturnsNull() + public async Task Success() { Publication publication = _dataFixture .DefaultPublication() - .WithReleases(_dataFixture - .DefaultRelease(publishedVersions: 1, year: 2021) - .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.Null(await repository.GetLatestPublishedReleaseVersion(Guid.NewGuid(), "2021-22")); + var result = await repository.GetLatestReleaseVersion(publication.Id); + + // 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); } - } - public class GetLatestReleaseVersionTests : ReleaseVersionRepositoryTests - { [Fact] - public async Task Success() + public async Task MultiplePublications_ReturnsReleaseVersionAssociatedWithPublication() { - var publications = _dataFixture + var (publication1, publication2) = _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: 0, draftVersion: true)]) + .GenerateTuple2(); + + var contextId = await AddTestData(publication1, publication2); await using var contentDbContext = InMemoryContentDbContext(contextId); var repository = BuildRepository(contentDbContext); - var result = await repository.GetLatestReleaseVersion(publications[0].Id); + var result = await repository.GetLatestReleaseVersion(publication1.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 from the specified publication + var expectedReleaseVersion = publication1.Releases.Single().Versions.Single(); Assert.NotNull(result); Assert.Equal(expectedReleaseVersion.Id, result.Id); @@ -239,36 +170,21 @@ public async Task Success() [Fact] public async Task PublicationHasNoReleaseVersions_ReturnsNull() { - var publications = _dataFixture - .DefaultPublication() - // Index 0 has no release versions - // Index 1 has a 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.Null(await repository.GetLatestReleaseVersion(publications[0].Id)); + Assert.Null(await repository.GetLatestReleaseVersion(publication.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); + var repository = BuildRepository(); - Assert.Null(await repository.GetLatestReleaseVersion(Guid.NewGuid())); + Assert.Null(await repository.GetLatestReleaseVersion(publicationId: Guid.NewGuid())); } } @@ -279,20 +195,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] @@ -300,15 +214,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)); } @@ -316,17 +228,9 @@ public async Task ReleaseHasNoPublishedReleaseVersions_ReturnsFalse() [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.IsLatestPublishedReleaseVersion(Guid.NewGuid())); + Assert.False(await repository.IsLatestPublishedReleaseVersion(releaseVersionId: Guid.NewGuid())); } } @@ -337,281 +241,412 @@ 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 repository = BuildRepository(); - var contextId = await AddTestData(publication); - await using var contentDbContext = InMemoryContentDbContext(contextId); - var repository = BuildRepository(contentDbContext); - - Assert.False(await repository.IsLatestReleaseVersion(Guid.NewGuid())); + Assert.False(await repository.IsLatestReleaseVersion(releaseVersionId: Guid.NewGuid())); } } - public class ListLatestPublishedReleaseVersionIdsTests : ReleaseVersionRepositoryTests + public class ListLatestReleaseVersionsTests : 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.ListLatestPublishedReleaseVersionIds(publications[0].Id); - - // Expect the result to contain the highest published version of each release for the specified publication - AssertIdsAreEqualIgnoringOrder( + [Fact] + public async Task Success() + { + 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 = [ - publications[0].ReleaseVersions[2].Id, - publications[0].ReleaseVersions[5].Id - ], - result); + 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(_ => + [ + _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 ListLatestReleaseVersions(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() + { + Publication publication = _dataFixture.DefaultPublication(); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + 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); + } } - [Fact] - public async Task PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() + public class PublishedOnlyTests : ListLatestReleaseVersionsTests { - 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() - { - 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 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.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( + [Fact] + public async Task Success() + { + 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 published version of each release, ordered by release series + Guid[] expectedReleaseVersionIds = [ - 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())); + 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)); + } + + [Fact] + public async Task MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() + { + var (publication1, publication2) = _dataFixture + .DefaultPublication() + .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 ListLatestReleaseVersions(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 PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() + { + Publication publication = _dataFixture + .DefaultPublication() + .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 ListLatestReleaseVersions(repository, publication.Id)); + } + + [Fact] + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + { + Publication publication = _dataFixture.DefaultPublication(); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + 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: 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() + { + 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) + ]); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await ListLatestReleaseVersionIds(repository, publication.Id); + + // Expect the latest version id's of each release + AssertIdsAreEqualIgnoringOrder( + [ + 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 MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() + { + var (publication1, publication2) = _dataFixture + .DefaultPublication() + .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 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() + { + Publication publication = _dataFixture.DefaultPublication(); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + 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); + } } - [Fact] - public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + public class PublishedOnlyTests : 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 Success() + { + 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) + ]); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + var result = await ListLatestReleaseVersionIds(repository, publication.Id); + + // Expect the latest published version id's of each release + AssertIdsAreEqualIgnoringOrder( + [ + 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 MultiplePublications_ReturnsReleaseVersionsAssociatedWithPublication() + { + var (publication1, publication2) = _dataFixture + .DefaultPublication() + .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 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 PublicationHasNoPublishedReleaseVersions_ReturnsEmpty() + { + Publication publication = _dataFixture + .DefaultPublication() + .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 ListLatestReleaseVersionIds(repository, publication.Id)); + } + + [Fact] + public async Task PublicationHasNoReleaseVersions_ReturnsEmpty() + { + Publication publication = _dataFixture.DefaultPublication(); + + var contextId = await AddTestData(publication); + await using var contentDbContext = InMemoryContentDbContext(contextId); + var repository = BuildRepository(contentDbContext); + + 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); + } } + } - [Fact] - public async Task PublicationDoesNotExist_ReturnsEmpty() + private List GenerateReleaseSeries(IReadOnlyList releases, params int[] years) + { + return years.Select(year => { - 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())); - } + var release = releases.Single(r => r.Year == year); + return _dataFixture.DefaultReleaseSeriesItem().WithReleaseId(release.Id).Generate(); + }).ToList(); } private static void AssertIdsAreEqualIgnoringOrder( @@ -646,11 +681,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 new file mode 100644 index 00000000000..a7a09eec584 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Extensions/ReleaseSeriesItemExtensions.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; + +public static class ReleaseSeriesItemExtensions +{ + /// + /// 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) => + SelectReleaseIds(releaseSeriesItems).ToList(); + + private static IEnumerable SelectReleaseIds(List releaseSeriesItems) => + releaseSeriesItems + .Where(rsi => !rsi.IsLegacyLink) + .Select(rs => rs.ReleaseId!.Value); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Predicates/ReleaseVersionPredicates.cs index 90fd93d88a5..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; @@ -11,7 +12,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,22 +51,31 @@ 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. + /// 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 releaseVersionsQueryable, + public static IQueryable LatestReleaseVersion( + this IQueryable releaseVersions, Guid releaseId, - bool publishedOnly = false) + bool publishedOnly = false, + IReadOnlyList? includeUnpublishedVersionIds = null) { - 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) + .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.Content.Model/Repository/Interfaces/IReleaseRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs new file mode 100644 index 00000000000..9a7bbbef143 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseRepository.cs @@ -0,0 +1,30 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; + +public interface IReleaseRepository +{ + /// + /// 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 in release series order 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/Interfaces/IReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IReleaseVersionRepository.cs index f17b7e734cb..fb1e7400467 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. /// @@ -29,17 +19,17 @@ 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); /// - /// 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); @@ -65,42 +55,26 @@ Task IsLatestReleaseVersion( CancellationToken cancellationToken = default); /// - /// Retrieves the latest published release version id's 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 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 in reverse chronological order. + /// 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. + /// 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, 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..ce4e1aa401e --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseRepository.cs @@ -0,0 +1,52 @@ +#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.Extensions; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; + +public class ReleaseRepository(ContentDbContext contentDbContext) : IReleaseRepository +{ + public async Task> ListPublishedReleases( + Guid publicationId, + CancellationToken cancellationToken = default) + { + 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 async Task> ListPublishedReleaseIds( + Guid publicationId, + CancellationToken cancellationToken = default) + { + return await 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 contentDbContext.Releases + .Where(r => r.PublicationId == publicationId) + .Where(r => r.Versions.Any(rv => rv.Published != null)); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/ReleaseVersionRepository.cs index 7d7071ae153..685151bbb79 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; @@ -54,27 +55,23 @@ 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) { - 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( + public async Task GetLatestPublishedReleaseVersionByReleaseSlug( Guid publicationId, string releaseSlug, CancellationToken cancellationToken = default) @@ -82,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( @@ -103,41 +100,40 @@ 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) + 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(); } @@ -181,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); - } -} 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.Tests/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/ReleaseServiceTests.cs index d54156bf6b2..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; @@ -745,7 +744,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); @@ -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/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/PublicationService.cs index 7792a5f2b93..8548db4a29c 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 @@ -292,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..49bb95807e8 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.GetLatestPublishedReleaseVersionByReleaseSlug(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,15 +104,13 @@ 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 => { 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)) @@ -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) 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.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.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/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(); } } 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; } 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, 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 3fc0b16ebdd..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(), @@ -123,6 +124,7 @@ public static IHostBuilder ConfigurePublisherHostBuilder(this IHostBuilder hostB .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() 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 2269a2b9363..bc469d819da 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs @@ -3,7 +3,6 @@ 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.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces; @@ -18,7 +17,6 @@ public class PublishingCompletionService( INotificationsService notificationsService, IReleasePublishingStatusService releasePublishingStatusService, IPublicationCacheService publicationCacheService, - IReleaseVersionRepository releaseVersionRepository, IReleaseService releaseService, IRedirectsCacheService redirectsCacheService, IDataSetPublishingService dataSetPublishingService) @@ -29,10 +27,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 +67,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 +83,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,16 +118,15 @@ 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; + var latestPublishedReleaseVersion = await releaseService.GetLatestPublishedReleaseVersion(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)) 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: '', 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..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 @@ -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,63 @@ 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 + user waits until page contains link View latest data: ${RELEASE_1_NAME}