diff --git a/.github/workflows/contentful-schema-migrate.yml b/.github/workflows/contentful-schema-migrate.yml new file mode 100644 index 00000000..bb0de602 --- /dev/null +++ b/.github/workflows/contentful-schema-migrate.yml @@ -0,0 +1,141 @@ +name: Contentful Schema Migrate + +on: + workflow_dispatch: + inputs: + target_environment: + required: true + type: string + +env: + MANAGEMENT_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_TOKEN }} + DELIVERY_KEY: ${{ secrets.TF_VAR_CPD_DELIVERY_KEY }} + SPACE_ID: ${{ secrets.TF_VAR_CPD_SPACE_ID }} + ENVIRONMENT: ${{ inputs.target_environment }} + SPACE_CAPACITY: ${{ vars.CONTENTFUL_SPACE_CAPACITY }} + +jobs: + setup: + runs-on: ubuntu-latest + + outputs: + staging-environment: ${{ steps.staging-env.outputs.staging-environment }} + required-migrations: ${{ steps.required-migrations.outputs.required-migrations }} + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install npm packages used by util scripts + working-directory: ./Contentful-Schema + run: npm ci + + - name: Verify contentful space has available environment capacity + working-directory: ./Contentful-Schema/utils + run: node verify-space-capacity.js + + - name: Get target environment current migration version + id: current-migration-version + working-directory: ./Contentful-Schema/utils + run: node get-environment-version.js + + - name: Determine timestamp for new staging environment + run: echo "TIMESTAMP=$(date +%Y-%m-%d-%H-%M-%S)" >> $GITHUB_ENV + + - name: Set var for name of new environment + id: staging-env + run: echo "staging-environment=$(echo ${{ inputs.target_environment }}-${{ env.TIMESTAMP }})" >> $GITHUB_OUTPUT + + - name: Extract migration files from archive + working-directory: ./Contentful-Schema/migrations + run: rm *.cjs && rm manifest.txt && tar -zxf migrations.tar.gz && rm migrations.tar.gz + + - name: Verify migration files against manifest + working-directory: ./Contentful-Schema/utils + run: node verify-migrations-against-manifest.js + + - name: Determine required migrations for environment + id: required-migrations + working-directory: ./Contentful-Schema/utils + run: node get-required-migrations.js --currentVersion ${{ steps.current-migration-version.outputs.migration-version }} + + clone: + if: ${{ join(needs.setup.outputs.required-migrations, '') != '[]' }} + needs: [setup] + runs-on: ubuntu-latest + steps: + + - name: Install Contentful CLI + run: npm install -g contentful-cli + + - name: Login to Contentful with management token + run: contentful login --management-token "${{ env.MANAGEMENT_TOKEN }}" + + - name: Set target space + run: contentful space use --space-id ${{ env.SPACE_ID }} --environment-id ${{ inputs.target_environment }} + + - name: Clone target environment ${{ inputs.target_environment }} to staging environment ${{ needs.setup.outputs.staging-environment }} + run: contentful space environment create --name ${{ needs.setup.outputs.staging-environment }} --environment-id ${{ needs.setup.outputs.staging-environment }} --source ${{ inputs.target_environment }} + + migrate: + if: ${{ join(needs.setup.outputs.required-migrations, '') != '[]' }} + needs: [setup,clone] + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + matrix: + value: ${{fromJSON(needs.setup.outputs.required-migrations)}} + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install npm packages used by migration script + working-directory: ./Contentful-Schema + run: npm ci + + - name: Install Contentful CLI + run: npm install -g contentful-cli + + - name: Extract migration files from archive + working-directory: ./Contentful-Schema/migrations + run: rm *.cjs && rm manifest.txt && tar -zxf migrations.tar.gz && rm migrations.tar.gz + + - name: Run migration script ${{ matrix.value }} + working-directory: ./Contentful-Schema/migrations + run: contentful space migration --space-id "${{ env.SPACE_ID }}" --environment-id ${{ needs.setup.outputs.staging-environment }} --management-token "${{ env.MANAGEMENT_TOKEN }}" ${{ matrix.value }} --yes + + - name: Update environment's migration version + working-directory: ./Contentful-Schema/utils + env: + STAGING_ENVIRONMENT: ${{ needs.setup.outputs.staging-environment }} + MIGRATION_FILENAME: ${{ matrix.value }} + run: node set-environment-version.js + + repoint-alias: + if: ${{ join(needs.setup.outputs.required-migrations, '') != '[]' }} + needs: [setup,migrate] + runs-on: ubuntu-latest + env: + STAGING_ENVIRONMENT: ${{ needs.setup.outputs.staging-environment }} + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install npm packages used by migration script + working-directory: ./Contentful-Schema + run: npm ci + + - name: Run script to point alias ${{ inputs.target_environment }} at new environment ${{ env.STAGING_ENVIRONMENT }} + working-directory: ./Contentful-Schema/utils + run: node point-alias-at-environment.js \ No newline at end of file diff --git a/.github/workflows/create-schema-release.yml b/.github/workflows/create-schema-release.yml deleted file mode 100644 index 2a75052c..00000000 --- a/.github/workflows/create-schema-release.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Production Create Schema Release - -on: workflow_dispatch - -jobs: - create-schema-release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - working-directory: ./Contentful-Schema/ - run: | - npm ci - - name: Build scripts - working-directory: ./Contentful-Schema/ - run: | - npx tsc - - name: Run script - env: - CONTENTFUL_MANAGEMENT_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_TOKEN }} - CPD_SPACE_ID: ${{ secrets.TF_VAR_CPD_SPACE_ID }} - working-directory: ./Contentful-Schema - run: | - node ./dist/scripts/schema-release-process/create-new-env-version.js -s pre-prod -t master \ No newline at end of file diff --git a/.github/workflows/docker-dev-integration-tests.yml b/.github/workflows/docker-dev-integration-tests.yml index 5a8a5540..5593ec67 100644 --- a/.github/workflows/docker-dev-integration-tests.yml +++ b/.github/workflows/docker-dev-integration-tests.yml @@ -1,10 +1,6 @@ --- name: Run integration tests -on: - pull_request: - branches: - - main - - next +on: workflow_dispatch jobs: build-test-scan: name: Build image and integration test diff --git a/.github/workflows/prepare-environment-for-migrations.yml b/.github/workflows/prepare-environment-for-migrations.yml new file mode 100644 index 00000000..925389ac --- /dev/null +++ b/.github/workflows/prepare-environment-for-migrations.yml @@ -0,0 +1,39 @@ +name: Prepare Contentful Environment for Migrations + +on: + workflow_dispatch: + inputs: + target_environment: + required: true + type: string + +env: + MANAGEMENT_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_TOKEN }} + SPACE_ID: ${{ secrets.TF_VAR_CPD_SPACE_ID }} + ENVIRONMENT: ${{ inputs.target_environment }} + +jobs: + prepare-environment: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install npm packages used by migration script + working-directory: ./Contentful-Schema + run: npm ci + + - name: Install Contentful CLI + run: npm install -g contentful-cli + + - name: Run script to create migrationVersion content type + working-directory: ./Contentful-Schema/utils + run: contentful space migration --space-id "${{ secrets.TF_VAR_CPD_SPACE_ID }}" --environment-id ${{ inputs.target_environment }} --management-token "${{ secrets.CONTENTFUL_MANAGEMENT_TOKEN }}" create-migration-version-content-type.cjs --yes + + - name: Run script to create inital migration version entry + working-directory: ./Contentful-Schema/utils + run: node create-initial-migration-version.js + \ No newline at end of file diff --git a/.github/workflows/rollback-master-alias-to-previous-release.yml b/.github/workflows/rollback-master-alias-to-previous-release.yml deleted file mode 100644 index cf886ccc..00000000 --- a/.github/workflows/rollback-master-alias-to-previous-release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Production rollback master alias to previous release - -on: workflow_dispatch - -jobs: - create-schema-release: - runs-on: ubuntu-latest - environment: Prod - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - working-directory: ./Contentful-Schema/ - run: | - npm ci - - name: Build scripts - working-directory: ./Contentful-Schema/ - run: | - npx tsc - - name: Run script - env: - CONTENTFUL_MANAGEMENT_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_TOKEN }} - CPD_SPACE_ID: ${{ secrets.TF_VAR_CPD_SPACE_ID }} - working-directory: ./Contentful-Schema - run: | - node ./dist/scripts/schema-release-process/switch-alias-for-release.js -a master -r \ No newline at end of file diff --git a/.github/workflows/switch-master-alias-to-new-release.yml b/.github/workflows/switch-master-alias-to-new-release.yml deleted file mode 100644 index 3e3cf5bc..00000000 --- a/.github/workflows/switch-master-alias-to-new-release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Production switch master alias to new release - -on: workflow_dispatch - -jobs: - create-schema-release: - runs-on: ubuntu-latest - environment: Prod - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - working-directory: ./Contentful-Schema/ - run: | - npm ci - - name: Build scripts - working-directory: ./Contentful-Schema/ - run: | - npx tsc - - name: Run script - env: - CONTENTFUL_MANAGEMENT_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_TOKEN }} - CPD_SPACE_ID: ${{ secrets.TF_VAR_CPD_SPACE_ID }} - working-directory: ./Contentful-Schema - run: | - node ./dist/scripts/schema-release-process/switch-alias-for-release.js -a master \ No newline at end of file diff --git a/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs b/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs index 05441adc..a7486e1e 100644 --- a/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs @@ -10,14 +10,18 @@ namespace Childrens_Social_Care_CPD_Tests.Contentful; public class EntityResolverTests { [Test] + [TestCase("accordion", typeof(Accordion))] + [TestCase("accordionSection", typeof(AccordionSection))] [TestCase("areaOfPractice", typeof(AreaOfPractice))] [TestCase("areaOfPracticeList", typeof(AreaOfPracticeList))] [TestCase("applicationFeature", typeof(ApplicationFeature))] [TestCase("applicationFeatures", typeof(ApplicationFeatures))] [TestCase("audioResource", typeof(AudioResource))] + [TestCase("backToTop", typeof(BackToTop))] [TestCase("columnLayout", typeof(ColumnLayout))] [TestCase("content", typeof(Content))] [TestCase("contentLink", typeof(ContentLink))] + [TestCase("contentsAnchor", typeof(ContentsAnchor))] [TestCase("contentSeparator", typeof(ContentSeparator))] [TestCase("detailedPathway", typeof(DetailedPathway))] [TestCase("detailedRole", typeof(DetailedRole))] @@ -26,12 +30,15 @@ public class EntityResolverTests [TestCase("imageCard", typeof(ImageCard))] [TestCase("linkCard", typeof(LinkCard))] [TestCase("linkListCard", typeof(LinkListCard))] + [TestCase("pageContents", typeof(PageContents))] + [TestCase("pageContentsItem", typeof(PageContentsItem))] [TestCase("pdfFileResource", typeof(PdfFileResource))] [TestCase("richTextBlock", typeof(RichTextBlock))] [TestCase("roleList", typeof(RoleList))] [TestCase("navigationMenu", typeof(NavigationMenu))] [TestCase("textBlock", typeof(TextBlock))] [TestCase("videoResource", typeof(VideoResource))] + [TestCase("infoBox", typeof(InfoBox))] public void Resolves_Correctly(string contentTypeId, Type expectedType) { var resolver = new EntityResolver(); diff --git a/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs b/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs index cca1c8c3..4387a32d 100644 --- a/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs @@ -11,13 +11,17 @@ public partial class PartialsFactoryTests { private static readonly object[] Successful_Resolves = { + new object[] { new Accordion(), "_Accordion" }, + new object[] { new AccordionSection(), "_AccordionSection" }, new object[] { new AreaOfPractice(), "_AreaOfPractice" }, new object[] { new AreaOfPracticeList(), "_AreaOfPracticeList" }, new object[] { new AudioResource(), "_AudioResource" }, + new object[] { new BackToTop(), "_BackToTop" }, new object[] { new ColumnLayout(), "_ColumnLayout" }, new object[] { new Content(), "_Content" }, new object[] { new ContentLink(), "_ContentLink" }, new object[] { new ContentSeparator(), "_ContentSeparator" }, + new object[] { new ContentsAnchor(), "_ContentsAnchor" }, new object[] { new DetailedPathway(), "_DetailedPathway" }, new object[] { new DetailedRole(), "_DetailedRole" }, new object[] { new Feedback(), "_Feedback" }, @@ -26,11 +30,14 @@ public partial class PartialsFactoryTests new object[] { new ImageCard(), "_ImageCard" }, new object[] { new NavigationMenu(), "_NavigationMenu" }, new object[] { new LinkListCard(), "_LinkListCard" }, + new object[] { new PageContents(), "_PageContents" }, + new object[] { new PageContentsItem(), "_PageContentsItem" }, new object[] { new PdfFileResource(), "_PdfFileResource" }, new object[] { new RichTextBlock(), "_RichTextBlock" }, new object[] { new RoleList(), "_RoleList" }, new object[] { new TextBlock(), "_TextBlock" }, new object[] { new VideoResource(), "_VideoResource" }, + new object[] { new InfoBox(), "_InfoBox" }, }; [TestCaseSource(nameof(Successful_Resolves))] diff --git a/Childrens-Social-Care-CPD-Tests/Controllers/FeedbackControllerTests.cs b/Childrens-Social-Care-CPD-Tests/Controllers/FeedbackControllerTests.cs deleted file mode 100644 index c9671cef..00000000 --- a/Childrens-Social-Care-CPD-Tests/Controllers/FeedbackControllerTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -using Childrens_Social_Care_CPD.Configuration; -using Childrens_Social_Care_CPD.Configuration.Features; -using Childrens_Social_Care_CPD.Contentful; -using Childrens_Social_Care_CPD.Contentful.Models; -using Childrens_Social_Care_CPD.Controllers; -using Contentful.Core.Models; -using Contentful.Core.Search; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Childrens_Social_Care_CPD_Tests.Controllers; - -public class FeedbackControllerTests -{ - private IFeaturesConfig _featuresConfig; - private FeedbackController _feedbackController; - private IRequestCookieCollection _cookies; - private HttpContext _httpContext; - private HttpRequest _httpRequest; - private ICpdContentfulClient _contentfulClient; - - [SetUp] - public void SetUp() - { - _contentfulClient = Substitute.For(); - - _cookies = Substitute.For(); - _httpContext = Substitute.For(); - _httpRequest = Substitute.For(); - var controllerContext = Substitute.For(); - - _httpRequest.Cookies.Returns(_cookies); - _httpContext.Request.Returns(_httpRequest); - controllerContext.HttpContext = _httpContext; - - _featuresConfig = Substitute.For(); - _featuresConfig.IsEnabled(Features.FeedbackControl).Returns(true); - _feedbackController = new FeedbackController(_featuresConfig, _contentfulClient) - { - ControllerContext = controllerContext, - TempData = Substitute.For() - }; - } - - [Test] - public async Task Feedback_Returns_404_When_Resource_Feature_Disabled() - { - // arrange - _featuresConfig.IsEnabled(Features.FeedbackControl).Returns(false); - - // act - var actual = await _feedbackController.Feedback(new FeedbackModel()); - - // assert - actual.Should().BeOfType(); - } - - [Test] - public async Task JsonFeedback_Returns_404_When_Resource_Feature_Disabled() - { - // arrange - _featuresConfig.IsEnabled(Features.FeedbackControl).Returns(false); - - // act - var actual = await _feedbackController.JsonFeedback(new FeedbackModel()); - - // assert - actual.Should().BeOfType(); - } - - [Test] - public async Task Feedback_Redirects_To_PageId_When_Page_Exists() - { - // arrange - var content = new Content() - { - Id = "fooId" - }; - - _contentfulClient - .GetEntries(Arg.Any>(), Arg.Any()) - .Returns(new ContentfulCollection() { Items = new List() { content } }); - - var model = new FeedbackModel() - { - Page = "foo" - }; - - // act - var actual = await _feedbackController.Feedback(model) as RedirectResult; - - // assert - actual.Should().NotBeNull(); - actual.Url.Should().StartWith("~/fooId?fs=true"); - } - - [Test] - public async Task Feedback_Returns_BadRequest_When_Page_Does_Not_Exist() - { - // arrange - _contentfulClient - .GetEntries(Arg.Any>(), Arg.Any()) - .Returns(new ContentfulCollection() { Items = new List() }); - - var model = new FeedbackModel() - { - Page = "foo" - }; - - // act - var actual = await _feedbackController.Feedback(model) as BadRequestResult; - - // assert - actual.Should().NotBeNull(); - } - - [Test] - public async Task JsonFeedback_Returns_BadRequest_When_Page_Does_Not_Exist() - { - // arrange - _contentfulClient - .GetEntries(Arg.Any>(), Arg.Any()) - .Returns(new ContentfulCollection() { Items = new List() }); - - var model = new FeedbackModel() - { - Page = "foo" - }; - - // act - var actual = await _feedbackController.JsonFeedback(model) as BadRequestResult; - - // assert - actual.Should().NotBeNull(); - } - - [Test] - public async Task JsonFeedback_Returns_Ok_When_Page_Exists() - { - // arrange - var content = new Content() - { - Id = "fooId" - }; - - _contentfulClient - .GetEntries(Arg.Any>(), Arg.Any()) - .Returns(new ContentfulCollection() { Items = new List() { content } }); - - var model = new FeedbackModel() - { - Page = "foo" - }; - - // act - var actual = await _feedbackController.JsonFeedback(model) as OkResult; - - // assert - actual.Should().NotBeNull(); - } - - [TestCase("!")] - [TestCase("foo.")] - [TestCase("_foo")] - [TestCase("some-fo:o")] - [TestCase("'some-foo'")] - [TestCase("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")] - public async Task Feedback_Rejects_Invalid_PageId(string pageId) - { - // arrange - var model = new FeedbackModel { Page = pageId }; - - // act - var actual = await _feedbackController.Feedback(model) as BadRequestResult; - - // assert - actual.Should().NotBeNull(); - } - - [TestCase("!")] - [TestCase("foo.")] - [TestCase("_foo")] - [TestCase("some-fo:o")] - [TestCase("'some-foo'")] - [TestCase("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")] - public async Task JsonFeedback_Rejects_Invalid_PageId(string pageId) - { - // arrange - var model = new FeedbackModel { Page = pageId }; - - // act - var actual = await _feedbackController.JsonFeedback(model) as BadRequestResult; - - // assert - actual.Should().NotBeNull(); - } - - [Test] - public async Task Feedback_Rejects_Invalid_Comments() - { - // arrange - var model = new FeedbackModel { Comments = new string('a', 501) }; - - // act - var actual = await _feedbackController.Feedback(model) as BadRequestResult; - - // assert - actual.Should().NotBeNull(); - } - - [Test] - public async Task JsonFeedback_Rejects_Invalid_Comments() - { - // arrange - var model = new FeedbackModel { Comments = new string('a', 501) }; - - // act - var actual = await _feedbackController.JsonFeedback(model) as BadRequestResult; - - // assert - actual.Should().NotBeNull(); - } -} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj b/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj index 4018ec00..dc117b5e 100644 --- a/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj +++ b/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj @@ -15,7 +15,7 @@ - + diff --git a/Childrens-Social-Care-CPD/Configuration/Features/Features.cs b/Childrens-Social-Care-CPD/Configuration/Features/Features.cs index 3cb3223e..db36f1e5 100644 --- a/Childrens-Social-Care-CPD/Configuration/Features/Features.cs +++ b/Childrens-Social-Care-CPD/Configuration/Features/Features.cs @@ -4,4 +4,5 @@ public static class Features { public const string ResourcesAndLearning = "resources-learning"; public const string FeedbackControl = "feedback-control"; + public const string EmployerStandards = "employer-standards"; } diff --git a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs index 2b5dae70..997dbb43 100644 --- a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs @@ -60,12 +60,12 @@ public interface IApplicationConfiguration [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] string GoogleTagManagerKey { get; } - [RequiredForEnvironment(ApplicationEnvironment.None, Hidden = false)] // TODO: when released, set the env to ALL + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] string SearchApiKey { get; } - [RequiredForEnvironment(ApplicationEnvironment.None, Hidden = false)] // TODO: when released, set the env to ALL + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] string SearchEndpoint { get; } - [RequiredForEnvironment(ApplicationEnvironment.None, Hidden = false, Obfuscate = false)] // TODO: when released, set the env to ALL + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] string SearchIndexName { get; } } diff --git a/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs b/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs index eb88b5c8..2f5fab1c 100644 --- a/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs +++ b/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs @@ -13,14 +13,18 @@ public Type Resolve(string contentTypeId) { return contentTypeId switch { + "accordion" => typeof(Accordion), + "accordionSection" => typeof(AccordionSection), "areaOfPractice" => typeof(AreaOfPractice), "areaOfPracticeList" => typeof(AreaOfPracticeList), "applicationFeature" => typeof(ApplicationFeature), "applicationFeatures" => typeof(ApplicationFeatures), "audioResource" => typeof(AudioResource), + "backToTop" => typeof(BackToTop), "columnLayout" => typeof(ColumnLayout), "content" => typeof(Content), "contentLink" => typeof(ContentLink), + "contentsAnchor" => typeof(ContentsAnchor), "contentSeparator" => typeof(ContentSeparator), "detailedPathway" => typeof(DetailedPathway), "detailedRole" => typeof(DetailedRole), @@ -29,12 +33,15 @@ public Type Resolve(string contentTypeId) "imageCard" => typeof(ImageCard), "linkCard" => typeof(LinkCard), "linkListCard" => typeof(LinkListCard), + "pageContents" => typeof(PageContents), + "pageContentsItem" => typeof(PageContentsItem), "pdfFileResource" => typeof(PdfFileResource), "richTextBlock" => typeof(RichTextBlock), "roleList" => typeof(RoleList), "navigationMenu" => typeof(NavigationMenu), "textBlock" => typeof(TextBlock), "videoResource" => typeof(VideoResource), + "infoBox" => typeof(InfoBox), _ => null }; } diff --git a/Childrens-Social-Care-CPD/Contentful/Models/Accordion.cs b/Childrens-Social-Care-CPD/Contentful/Models/Accordion.cs new file mode 100644 index 00000000..426fb9b4 --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/Accordion.cs @@ -0,0 +1,9 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class Accordion : IContent +{ + public string Name { get; set; } + public List Sections { get; set; } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Contentful/Models/AccordionSection.cs b/Childrens-Social-Care-CPD/Contentful/Models/AccordionSection.cs new file mode 100644 index 00000000..42339f57 --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/AccordionSection.cs @@ -0,0 +1,11 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class AccordionSection : IContent +{ + public string Name { get; set; } + public string Heading { get; set; } + public string SummaryLine { get; set; } + public List Content { get; set; } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Contentful/Models/BackToTop.cs b/Childrens-Social-Care-CPD/Contentful/Models/BackToTop.cs new file mode 100644 index 00000000..a0b99d88 --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/BackToTop.cs @@ -0,0 +1,9 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class BackToTop : IContent +{ + public string DisplayText { get; set; } + public string Icon { get; set; } +} diff --git a/Childrens-Social-Care-CPD/Contentful/Models/ContentsAnchor.cs b/Childrens-Social-Care-CPD/Contentful/Models/ContentsAnchor.cs new file mode 100644 index 00000000..8dd00e6b --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/ContentsAnchor.cs @@ -0,0 +1,8 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class ContentsAnchor : IContent +{ + public string AnchorTag { get; set; } +} diff --git a/Childrens-Social-Care-CPD/Contentful/Models/ImageCard.cs b/Childrens-Social-Care-CPD/Contentful/Models/ImageCard.cs index b617c380..448c364f 100644 --- a/Childrens-Social-Care-CPD/Contentful/Models/ImageCard.cs +++ b/Childrens-Social-Care-CPD/Contentful/Models/ImageCard.cs @@ -6,6 +6,6 @@ public class ImageCard : IContent { public string Id { get; set; } public Asset Image { get; set; } - public string ImageSide { get; set; } public Document Text { get; set; } + public string TextPosition { get; set; } } diff --git a/Childrens-Social-Care-CPD/Contentful/Models/InfoBox.cs b/Childrens-Social-Care-CPD/Contentful/Models/InfoBox.cs new file mode 100644 index 00000000..510d07af --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/InfoBox.cs @@ -0,0 +1,11 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class InfoBox : IContent +{ + public string Title { get; set; } + public bool DisplayTitle { get; set; } + public int TitleLevel { get; set; } + public Document Document { get; set; } +} diff --git a/Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs b/Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs index 08c520ef..e47d0490 100644 --- a/Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs +++ b/Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs @@ -1,4 +1,5 @@ -using Contentful.Core.Models; +using System.Runtime.CompilerServices; +using Contentful.Core.Models; namespace Childrens_Social_Care_CPD.Contentful.Models; @@ -6,4 +7,6 @@ public class NavigationMenu: IContent { public string Name { get; set; } public List Items { get; set; } + public string Header { get; set; } + public int HeaderLevel { get; set; } } diff --git a/Childrens-Social-Care-CPD/Contentful/Models/PageContents.cs b/Childrens-Social-Care-CPD/Contentful/Models/PageContents.cs new file mode 100644 index 00000000..ed47309d --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/PageContents.cs @@ -0,0 +1,10 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class PageContents : IContent +{ + public string Name { get; set; } + public string DisplayText { get; set; } + public List ContentLinks { get; set; } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Contentful/Models/PageContentsItem.cs b/Childrens-Social-Care-CPD/Contentful/Models/PageContentsItem.cs new file mode 100644 index 00000000..9c249616 --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Models/PageContentsItem.cs @@ -0,0 +1,9 @@ +using Contentful.Core.Models; + +namespace Childrens_Social_Care_CPD.Contentful.Models; + +public class PageContentsItem : IContent +{ + public string ItemText { get; set; } + public string AnchorLink { get; set; } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs b/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs index f35bc384..73669f0d 100644 --- a/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs +++ b/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs @@ -14,13 +14,17 @@ public static string GetPartialFor(IContent item) { return item switch { + Accordion => "_Accordion", + AccordionSection => "_AccordionSection", AreaOfPractice => "_AreaOfPractice", AreaOfPracticeList => "_AreaOfPracticeList", AudioResource => "_AudioResource", + BackToTop => "_BackToTop", ColumnLayout => "_ColumnLayout", Content => "_Content", ContentLink => "_ContentLink", ContentSeparator => "_ContentSeparator", + ContentsAnchor => "_ContentsAnchor", DetailedRole => "_DetailedRole", DetailedPathway => "_DetailedPathway", Feedback => "_Feedback", @@ -28,12 +32,15 @@ public static string GetPartialFor(IContent item) ImageCard => "_ImageCard", LinkCard => "_LinkCard", LinkListCard => "_LinkListCard", + PageContents => "_PageContents", + PageContentsItem => "_PageContentsItem", PdfFileResource => "_PdfFileResource", RichTextBlock => "_RichTextBlock", RoleList => "_RoleList", NavigationMenu => "_NavigationMenu", TextBlock => "_TextBlock", VideoResource => "_VideoResource", + InfoBox => "_InfoBox", _ => "_UnknownContentWarning", }; } diff --git a/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs b/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs index 309224c2..6b386270 100644 --- a/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs @@ -26,6 +26,11 @@ public JsonResult AppInfo() [Route("application/configuration")] public IActionResult Configuration() { + if (!ModelState.IsValid) + { + return BadRequest(); + } + var configurationInformation = new ConfigurationInformation(applicationConfiguration); if (Request.Headers.Accept == MediaTypeNames.Application.Json) diff --git a/Childrens-Social-Care-CPD/Controllers/ContentController.cs b/Childrens-Social-Care-CPD/Controllers/ContentController.cs index 136cb811..d986697f 100644 --- a/Childrens-Social-Care-CPD/Controllers/ContentController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ContentController.cs @@ -37,6 +37,11 @@ private async Task FetchPageContentAsync(string contentId, Cancellation [Route("/{*pagename:regex(^[[0-9a-z]]+[[0-9a-z\\/\\-]]*$)}")] public async Task Index(string pageName = "home", bool preferenceSet = false, bool fs = false, CancellationToken cancellationToken = default) { + if (!ModelState.IsValid) + { + return BadRequest(); + } + pageName = pageName?.TrimEnd('/'); var content = await FetchPageContentAsync(pageName, cancellationToken); if (content == null) diff --git a/Childrens-Social-Care-CPD/Controllers/CookieController.cs b/Childrens-Social-Care-CPD/Controllers/CookieController.cs index 138cebaf..ecc4cc94 100644 --- a/Childrens-Social-Care-CPD/Controllers/CookieController.cs +++ b/Childrens-Social-Care-CPD/Controllers/CookieController.cs @@ -16,6 +16,11 @@ public class CookieController(ICpdContentfulClient cpdClient, ICookieHelper cook [Route("/cookies/setpreferences")] public IActionResult SetPreferences(string consentValue, string sourcePage = null, bool fromCookies = false) { + if (!ModelState.IsValid) + { + return BadRequest(); + } + var consentState = AnalyticsConsentStateHelper.Parse(consentValue); cookieHelper.SetResponseAnalyticsCookieState(HttpContext, consentState); @@ -32,6 +37,11 @@ public IActionResult SetPreferences(string consentValue, string sourcePage = nul [Route("/cookies")] public async Task Cookies(CancellationToken cancellationToken, string sourcePage = null, bool preferenceSet = false) { + if (!ModelState.IsValid) + { + return BadRequest(); + } + sourcePage ??= string.Empty; var queryBuilder = QueryBuilder.New diff --git a/Childrens-Social-Care-CPD/Controllers/ErrorController.cs b/Childrens-Social-Care-CPD/Controllers/ErrorController.cs index 33543a4f..ed08f36e 100644 --- a/Childrens-Social-Care-CPD/Controllers/ErrorController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ErrorController.cs @@ -33,6 +33,11 @@ public IActionResult Error() [Route("error/{code:int}")] public IActionResult Error(int code) { + if (!ModelState.IsValid) + { + return BadRequest(); + } + ViewData["pageName"] = $"error/{code}"; return code switch { diff --git a/Childrens-Social-Care-CPD/Controllers/FeedbackController.cs b/Childrens-Social-Care-CPD/Controllers/FeedbackController.cs deleted file mode 100644 index 77943d5a..00000000 --- a/Childrens-Social-Care-CPD/Controllers/FeedbackController.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Childrens_Social_Care_CPD.Configuration.Features; -using Childrens_Social_Care_CPD.Contentful; -using Childrens_Social_Care_CPD.Contentful.Models; -using Contentful.Core.Search; -using Microsoft.AspNetCore.Mvc; -using System.Text.RegularExpressions; - -namespace Childrens_Social_Care_CPD.Controllers; - -public class FeedbackModel -{ - public string Page { get; set; } - public bool? IsUseful { get; set; } - public string Comments { get; set; } -} - -public class FeedbackController : Controller -{ - private readonly IFeaturesConfig _featuresConfig; - private readonly ICpdContentfulClient _cpdClient; - - public FeedbackController(IFeaturesConfig featuresConfig, ICpdContentfulClient cpdClient) - { - ArgumentNullException.ThrowIfNull(featuresConfig); - - _featuresConfig = featuresConfig; - _cpdClient = cpdClient; - } - - private async Task FetchPageContentAsync(string contentId, CancellationToken cancellationToken) - { - var queryBuilder = QueryBuilder.New - .ContentTypeIs("content") - .FieldEquals("fields.id", contentId) - .Include(10); - - var result = await _cpdClient.GetEntries(queryBuilder, cancellationToken); - - return result?.FirstOrDefault(); - } - - private static bool IsModelValid(FeedbackModel model, out string pageId) - { - pageId = model.Page ?? string.Empty; - pageId = pageId.Trim('/'); - - if (pageId.Length > 512 - || model.Comments?.Length > 400 - || !Regex.IsMatch(pageId, @"^[0-9a-z](\/?[0-9a-z\-])*\/?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1))) - { - return false; - } - - return true; - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("feedback")] - public async Task Feedback([FromForm]FeedbackModel feedback, CancellationToken cancellationToken = default) - { - if (!_featuresConfig.IsEnabled(Features.FeedbackControl)) - { - return NotFound(); - } - - // Validate the page id - if (!IsModelValid(feedback, out var pageId)) - { - return BadRequest(); - } - - // Check the page exists - var content = await FetchPageContentAsync(pageId, cancellationToken); - if (content == null) - { - return BadRequest(); - } - - // TODO: do something with the feedback - - return Redirect($"~/{content.Id}?fs=true"); - } - - [HttpPost] - [ValidateAntiForgeryToken] - [Route("api/feedback")] - public async Task JsonFeedback([FromBody]FeedbackModel feedback, CancellationToken cancellationToken = default) - { - if (!_featuresConfig.IsEnabled(Features.FeedbackControl)) - { - return NotFound(); - } - - // Validate the page id - if (!IsModelValid(feedback, out var pageId)) - { - return BadRequest(); - } - - // Check the page exists - var content = await FetchPageContentAsync(pageId, cancellationToken); - if (content == null) - { - return BadRequest(); - } - - // TODO: do something with the feedback - - return Ok(); - } -} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs b/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs index d38e39f9..0d8eb61b 100644 --- a/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs @@ -44,6 +44,11 @@ void AddProperty(string key, string prefix) [Route("resources-learning/{*pagename:regex(^[[0-9a-z]]+[[0-9a-z\\/\\-]]*$)}")] public async Task Index(string pageName = "home", bool preferenceSet = false, bool fs = false, CancellationToken cancellationToken = default) { + if (!ModelState.IsValid) + { + return BadRequest(); + } + if (!featuresConfig.IsEnabled(Features.ResourcesAndLearning)) { return NotFound(); diff --git a/Childrens-Social-Care-CPD/Controllers/SearchResourcesController.cs b/Childrens-Social-Care-CPD/Controllers/SearchResourcesController.cs index 200e3aac..ba250530 100644 --- a/Childrens-Social-Care-CPD/Controllers/SearchResourcesController.cs +++ b/Childrens-Social-Care-CPD/Controllers/SearchResourcesController.cs @@ -24,6 +24,11 @@ public SearchResourcesController(IFeaturesConfig featuresConfig, ISearchResultsV [HttpGet] public async Task SearchResources([FromQuery] SearchRequestModel query, bool preferencesSet = false, CancellationToken cancellationToken = default) { + if (!ModelState.IsValid) + { + return BadRequest(); + } + if (!_featuresConfig.IsEnabled(Features.ResourcesAndLearning)) { return NotFound(); diff --git a/Childrens-Social-Care-CPD/Views/SearchResources/SearchResources.cshtml b/Childrens-Social-Care-CPD/Views/SearchResources/SearchResources.cshtml index 70e5f2da..b5a0ace4 100644 --- a/Childrens-Social-Care-CPD/Views/SearchResources/SearchResources.cshtml +++ b/Childrens-Social-Care-CPD/Views/SearchResources/SearchResources.cshtml @@ -25,7 +25,8 @@ @* Filter column *@

Results

-

We found @Model.TotalCount results

+

We found @Model.TotalCount + results

@if (Model.SelectedTags.Any()) { @@ -34,7 +35,7 @@ } -
+ diff --git a/Childrens-Social-Care-CPD/Views/Shared/_Feedback.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_Feedback.cshtml index 74825730..6e1388c2 100644 --- a/Childrens-Social-Care-CPD/Views/Shared/_Feedback.cshtml +++ b/Childrens-Social-Care-CPD/Views/Shared/_Feedback.cshtml @@ -1,45 +1,29 @@ @using Childrens_Social_Care_CPD.Configuration.Features @inject IFeaturesConfig featureConfig +@inject ICookieHelper cookieHelper @if (!featureConfig.IsEnabled(Features.FeedbackControl)) return; +@if (cookieHelper.GetRequestAnalyticsCookieState(Context) != AnalyticsConsentState.Accepted) return; @{ var contextModel = (ContextModel)ViewBag.ContextModel; - var commentsId = $"comments-{Guid.NewGuid()}"; } -
- @if (contextModel.FeedbackSubmitted) - { -
- - - Give feedback about this page - - -
-
Thank you for your feedback on this resource! If you have more general feedback about this website, please use this feedback form.
-
-
- - return; - } -
- + Give feedback about this page - +
- Use this form to provide feedback about this page. If you have more general feedback about this website, please use this feedback form. + Use this form to provide feedback about this page.
@@ -51,48 +35,39 @@

- -
- -
+
-
-
- - - -
-
- You can enter up to 400 characters -
-
- +
- - +
+ +
+ If you have more general feedback about this website, please use this feedback form.
@@ -101,4 +76,4 @@ @{ Html.RequireScriptUrl("~/javascript/components/feedback.js"); -} \ No newline at end of file +} diff --git a/Childrens-Social-Care-CPD/Views/Shared/_Header.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_Header.cshtml index 837b59f5..21f47a98 100644 --- a/Childrens-Social-Care-CPD/Views/Shared/_Header.cshtml +++ b/Childrens-Social-Care-CPD/Views/Shared/_Header.cshtml @@ -62,6 +62,10 @@ { RenderMenuItem("resources", "Resources and learning", "resources-learning", category == "Resources"); } + if (featuresConfig.IsEnabled(Features.EmployerStandards)) + { + RenderMenuItem("employerStandards", "Employer standards", "employer-standards", category == "Employer standards"); + } }
diff --git a/Childrens-Social-Care-CPD/Views/Shared/_ImageCard.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_ImageCard.cshtml index 3acdf077..a68d8d54 100644 --- a/Childrens-Social-Care-CPD/Views/Shared/_ImageCard.cshtml +++ b/Childrens-Social-Care-CPD/Views/Shared/_ImageCard.cshtml @@ -3,18 +3,30 @@ @model ImageCard @{ - var bannerCss = Model.ImageSide == "Left" - ? "dfe-o-banner dfe-o-banner--mirrored govuk-!-margin-top-2" - : "dfe-o-banner govuk-!-margin-top-2"; + string bannerCss = ""; + + switch (Model.TextPosition) { + case "Left": + bannerCss = "dfe-o-banner govuk-!-margin-top-2"; + break; + case "Right": + bannerCss = "dfe-o-banner dfe-o-banner--mirrored govuk-!-margin-top-2"; + break; + default: + break; + } }
-
-
- + @if (bannerCss.Length > 0) + { +
+
+ +
-
+ }
@Model.Image.Description
diff --git a/Childrens-Social-Care-CPD/Views/Shared/_InfoBox.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_InfoBox.cshtml new file mode 100644 index 00000000..0ce82c0d --- /dev/null +++ b/Childrens-Social-Care-CPD/Views/Shared/_InfoBox.cshtml @@ -0,0 +1,33 @@ +@using Childrens_Social_Care_CPD.Contentful.Models; +@using Childrens_Social_Care_CPD.Contentful; +@using Childrens_Social_Care_CPD.Contentful.Renderers; + +@model InfoBox + +@functions +{ + void RenderTitle() + { + if (Model.DisplayTitle) + { + var level = Model.TitleLevel > 0 + ? Model.TitleLevel + : 2; + + var tagBuilder = new TagBuilder($"h{level}"); + tagBuilder.InnerHtml.Append(Model.Title); + tagBuilder.AddCssClass("govuk-heading-l"); + + @tagBuilder + } + } +} + +
+
+
+ @{ RenderTitle(); } + +
+
+
\ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml index 54ecf44f..eefec196 100644 --- a/Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml +++ b/Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml @@ -2,6 +2,39 @@ @model NavigationMenu +@functions +{ + void RenderHeader() + { + if (!String.IsNullOrEmpty(Model.Header?.Trim())) + { + string size; + switch (Model.HeaderLevel) + { + case 1: + size = "l"; + break; + + case 2: + default: + size = "m"; + break; + + case 3: + size = "s"; + break; + } + + var tagBuilder = new TagBuilder($"h{Model.HeaderLevel}"); + tagBuilder.InnerHtml.Append(Model.Header); + tagBuilder.AddCssClass("govuk-heading-" + size); + + @tagBuilder + } + } +} + +@{ RenderHeader(); }