diff --git a/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/ObjectiveTests.cs b/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/ObjectiveTests.cs new file mode 100644 index 0000000000..08d9347dd3 --- /dev/null +++ b/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/ObjectiveTests.cs @@ -0,0 +1,40 @@ +namespace DigitalLearningSolutions.Data.Tests.DataServices.TutorialContentDataServiceTests +{ + using System.Linq; + using System.Transactions; + using Dapper; + using FluentAssertions; + using FluentAssertions.Execution; + using NUnit.Framework; + + internal partial class TutorialContentDataServiceTests + { + [Test] + public void GetNonArchivedObjectivesBySectionAndCustomisationId_returns_objectives_correctly() + { + using (new TransactionScope()) + { + connection.Execute("UPDATE Tutorials SET OriginalTutorialID = 1 WHERE TutorialID = 1137"); + + // When + var result = tutorialContentDataService.GetNonArchivedObjectivesBySectionAndCustomisationId(248, 22062) + .ToList(); + + // Then + using (new AssertionScope()) + { + result.Count.Should().Be(4); + result.First().TutorialId.Should().Be(1); + result.First().Interactions.Should().BeEquivalentTo(new[] { 0, 1, 2, 3 }); + result.First().Possible.Should().Be(4); + result.First().MyScore.Should().Be(0); + + result.Last().TutorialId.Should().Be(1257); + result.Last().Interactions.Should().BeEquivalentTo(new[] { 8, 9, 10 }); + result.Last().Possible.Should().Be(3); + result.Last().MyScore.Should().Be(0); + } + } + } + } +} diff --git a/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/TutorialContentDataServiceTests.cs b/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/TutorialContentDataServiceTests.cs index 966294f26f..172eab9e32 100644 --- a/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/TutorialContentDataServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/DataServices/TutorialContentDataServiceTests/TutorialContentDataServiceTests.cs @@ -2,19 +2,21 @@ { using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Tests.TestHelpers; + using Microsoft.Data.SqlClient; using NUnit.Framework; internal partial class TutorialContentDataServiceTests { - private TutorialContentDataService tutorialContentDataService; - private TutorialContentTestHelper tutorialContentTestHelper; - private SectionContentTestHelper sectionContentTestHelper; - private CourseContentTestHelper courseContentTestHelper; + private SqlConnection connection = null!; + private TutorialContentDataService tutorialContentDataService = null!; + private TutorialContentTestHelper tutorialContentTestHelper = null!; + private SectionContentTestHelper sectionContentTestHelper = null!; + private CourseContentTestHelper courseContentTestHelper = null!; [SetUp] public void Setup() { - var connection = ServiceTestHelper.GetDatabaseConnection(); + connection = ServiceTestHelper.GetDatabaseConnection(); tutorialContentDataService = new TutorialContentDataService(connection); tutorialContentTestHelper = new TutorialContentTestHelper(connection); sectionContentTestHelper = new SectionContentTestHelper(connection); diff --git a/DigitalLearningSolutions.Data.Tests/Services/TrackerActionServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/TrackerActionServiceTests.cs new file mode 100644 index 0000000000..b9cc687a0b --- /dev/null +++ b/DigitalLearningSolutions.Data.Tests/Services/TrackerActionServiceTests.cs @@ -0,0 +1,86 @@ +namespace DigitalLearningSolutions.Data.Tests.Services +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Models.Tracker; + using DigitalLearningSolutions.Data.Services; + using FakeItEasy; + using FluentAssertions; + using NUnit.Framework; + + public class TrackerActionServiceTests + { + private ITutorialContentDataService dataService = null!; + private ITrackerActionService trackerActionService = null!; + + [SetUp] + public void Setup() + { + dataService = A.Fake(); + + trackerActionService = new TrackerActionService(dataService); + } + + [Test] + public void GetObjectiveArray_returns_results_in_specified_json_format() + { + // given + A.CallTo(() => dataService.GetNonArchivedObjectivesBySectionAndCustomisationId(A._, A._)) + .Returns( + new[] + { + new Objective(1, new List { 6, 7, 8 }, 4), + new Objective(2, new List { 17, 18, 19 }, 0), + } + ); + + // when + var result = trackerActionService.GetObjectiveArray(1, 1); + + // then + result.Should().BeEquivalentTo( + new TrackerObjectiveArray( + new[] + { + new Objective(1, new List { 6, 7, 8 }, 4), + new Objective(2, new List { 17, 18, 19 }, 0), + } + ) + ); + } + + [Test] + public void GetObjectiveArray_returns_empty_object_json_if_no_results_found() + { + // given + A.CallTo(() => dataService.GetNonArchivedObjectivesBySectionAndCustomisationId(A._, A._)) + .Returns(new List()); + + // when + var result = trackerActionService.GetObjectiveArray(1, 1); + + // then + result.Should().Be(null); + } + + [Test] + [TestCase(null, null)] + [TestCase(1, null)] + [TestCase(null, 1)] + public void GetObjectiveArray_returns_null_if_parameter_missing( + int? customisationId, + int? sectionId + ) + { + // given + A.CallTo(() => dataService.GetNonArchivedObjectivesBySectionAndCustomisationId(A._, A._)) + .Returns(new[] { new Objective(1, new List { 1 }, 9) }); + + // when + var result = trackerActionService.GetObjectiveArray(customisationId, sectionId); + + // then + result.Should().Be(null); + } + } +} diff --git a/DigitalLearningSolutions.Data.Tests/Services/TrackerServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/TrackerServiceTests.cs index d1b73cdfdf..9aa6c33a82 100644 --- a/DigitalLearningSolutions.Data.Tests/Services/TrackerServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Services/TrackerServiceTests.cs @@ -1,5 +1,6 @@ namespace DigitalLearningSolutions.Data.Tests.Services { + using System.Collections.Generic; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Models.Tracker; using DigitalLearningSolutions.Data.Services; @@ -10,15 +11,17 @@ public class TrackerServiceTests { + private ITrackerActionService actionService = null!; private ILogger logger = null!; private ITrackerService trackerService = null!; [SetUp] public void Setup() { - logger = A.Fake>(); + logger = A.Fake>(); + actionService = A.Fake(); - trackerService = new TrackerService(logger); + trackerService = new TrackerService(logger, actionService); } [Test] @@ -38,7 +41,7 @@ public void ProcessQuery_with_null_action_returns_NullAction_response() public void ProcessQuery_with_unknown_action_returns_InvalidAction_response() { // Given - var query = new TrackerEndpointQueryParams{Action = "InvalidAction"}; + var query = new TrackerEndpointQueryParams { Action = "InvalidAction" }; // When var result = trackerService.ProcessQuery(query); @@ -46,5 +49,61 @@ public void ProcessQuery_with_unknown_action_returns_InvalidAction_response() // Then result.Should().Be(TrackerEndpointErrorResponse.InvalidAction); } + + [Test] + public void ProcessQuery_with_GetObjectiveArray_action_passes_query_params() + { + // Given + var query = new TrackerEndpointQueryParams + { Action = "GetObjectiveArray", CustomisationId = 1, SectionId = 2 }; + + // When + trackerService.ProcessQuery(query); + + // Then + A.CallTo(() => actionService.GetObjectiveArray(1, 2)).MustHaveHappenedOnceExactly(); + } + + [Test] + public void ProcessQuery_with_GetObjectiveArray_action_correctly_serialises_contentful_response() + { + // Given + var dataToReturn = new TrackerObjectiveArray( + new[] + { + new Objective(1, new List { 6, 7, 8 }, 4), new Objective(2, new List { 17, 18, 19 }, 0), + } + ); + var expectedJson = + "{\"objectives\":[{\"tutorialid\":1,\"interactions\":[6,7,8],\"possible\":4,\"myscore\":0}," + + "{\"tutorialid\":2,\"interactions\":[17,18,19],\"possible\":0,\"myscore\":0}]}"; + + var query = new TrackerEndpointQueryParams + { Action = "GetObjectiveArray", CustomisationId = 1, SectionId = 1 }; + A.CallTo(() => actionService.GetObjectiveArray(1, 1)).Returns(dataToReturn); + + // When + var result = trackerService.ProcessQuery(query); + + // Then + result.Should().Be(expectedJson); + } + + [Test] + public void ProcessQuery_with_GetObjectiveArray_action_correctly_serialises_null_response() + { + // Given + TrackerObjectiveArray? dataToReturn = null; + + var query = new TrackerEndpointQueryParams + { Action = "GetObjectiveArray", CustomisationId = 1, SectionId = 1 }; + A.CallTo(() => actionService.GetObjectiveArray(1, 1)).Returns(dataToReturn); + + // When + var result = trackerService.ProcessQuery(query); + + // Then + result.Should().Be("{}"); + } } } diff --git a/DigitalLearningSolutions.Data/DataServices/TutorialContentDataService.cs b/DigitalLearningSolutions.Data/DataServices/TutorialContentDataService.cs index 3674827ba6..5f0b095aae 100644 --- a/DigitalLearningSolutions.Data/DataServices/TutorialContentDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/TutorialContentDataService.cs @@ -4,7 +4,9 @@ using System.Data; using Dapper; using DigitalLearningSolutions.Data.Exceptions; + using DigitalLearningSolutions.Data.Mappers; using DigitalLearningSolutions.Data.Models; + using DigitalLearningSolutions.Data.Models.Tracker; using DigitalLearningSolutions.Data.Models.TutorialContent; public interface ITutorialContentDataService @@ -20,7 +22,15 @@ int tutorialId TutorialVideo? GetTutorialVideo(int customisationId, int sectionId, int tutorialId); IEnumerable GetTutorialsBySectionId(int sectionId, int customisationId); IEnumerable GetTutorialIdsForCourse(int customisationId); - void UpdateOrInsertCustomisationTutorialStatuses(int tutorialId, int customisationId, bool diagnosticEnabled, bool learningEnabled); + + void UpdateOrInsertCustomisationTutorialStatuses( + int tutorialId, + int customisationId, + bool diagnosticEnabled, + bool learningEnabled + ); + + IEnumerable GetNonArchivedObjectivesBySectionAndCustomisationId(int sectionId, int customisationId); } public class TutorialContentDataService : ITutorialContentDataService @@ -30,6 +40,7 @@ public class TutorialContentDataService : ITutorialContentDataService public TutorialContentDataService(IDbConnection connection) { this.connection = connection; + SqlMapper.AddTypeHandler(new EnumerableIntHandler()); } public TutorialInformation? GetTutorialInformation( @@ -338,5 +349,25 @@ INSERT INTO CustomisationTutorials (CustomisationID, TutorialID, [Status], DiagS new { customisationId, tutorialId, learningEnabled, diagnosticEnabled } ); } + + public IEnumerable GetNonArchivedObjectivesBySectionAndCustomisationId(int sectionId, int customisationId) + { + return connection.Query( + @"SELECT + CASE + WHEN tu.OriginalTutorialID > 0 THEN tu.OriginalTutorialID + ELSE tu.TutorialID + END AS TutorialID, + tu.CMIInteractionIDs AS Interactions, + tu.DiagAssessOutOf AS Possible + FROM dbo.Tutorials AS tu + LEFT JOIN dbo.CustomisationTutorials AS ct + ON ct.TutorialID = tu.TutorialID + WHERE tu.SectionID = @sectionId + AND ct.CustomisationID = @customisationId + AND tu.ArchivedDate IS NULL", + new { sectionId, customisationId } + ); + } } } diff --git a/DigitalLearningSolutions.Data/Enums/TrackerEndpointAction.cs b/DigitalLearningSolutions.Data/Enums/TrackerEndpointAction.cs index 3577d05f5c..602b7f7526 100644 --- a/DigitalLearningSolutions.Data/Enums/TrackerEndpointAction.cs +++ b/DigitalLearningSolutions.Data/Enums/TrackerEndpointAction.cs @@ -1,4 +1,7 @@ namespace DigitalLearningSolutions.Data.Enums { - public enum TrackerEndpointAction { } + public enum TrackerEndpointAction + { + GetObjectiveArray, + } } diff --git a/DigitalLearningSolutions.Data/Mappers/EnumerableIntHandler.cs b/DigitalLearningSolutions.Data/Mappers/EnumerableIntHandler.cs new file mode 100644 index 0000000000..dbd9666c89 --- /dev/null +++ b/DigitalLearningSolutions.Data/Mappers/EnumerableIntHandler.cs @@ -0,0 +1,21 @@ +namespace DigitalLearningSolutions.Data.Mappers +{ + using System.Data; + using System.Linq; + using System.Collections.Generic; + using Dapper; + + public class EnumerableIntHandler : SqlMapper.TypeHandler> + { + public override void SetValue(IDbDataParameter parameter, IEnumerable value) + { + parameter.DbType = DbType.String; + parameter.Value = string.Join(",", value); + } + + public override IEnumerable Parse(object value) + { + return value.ToString()?.Split(',').Select(int.Parse) ?? new List(); + } + } +} diff --git a/DigitalLearningSolutions.Data/Models/Tracker/ITrackerEndpointDataModel.cs b/DigitalLearningSolutions.Data/Models/Tracker/ITrackerEndpointDataModel.cs new file mode 100644 index 0000000000..2bb49d5597 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/Tracker/ITrackerEndpointDataModel.cs @@ -0,0 +1,6 @@ +namespace DigitalLearningSolutions.Data.Models.Tracker +{ + interface ITrackerEndpointDataModel + { + } +} diff --git a/DigitalLearningSolutions.Data/Models/Tracker/Objective.cs b/DigitalLearningSolutions.Data/Models/Tracker/Objective.cs new file mode 100644 index 0000000000..a63c079770 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/Tracker/Objective.cs @@ -0,0 +1,20 @@ +namespace DigitalLearningSolutions.Data.Models.Tracker +{ + using System.Collections.Generic; + + public class Objective + { + public Objective(int tutorialId, IEnumerable interactions, int possible) + { + TutorialId = tutorialId; + Interactions = interactions; + Possible = possible; + MyScore = 0; + } + + public int TutorialId { get; set; } + public IEnumerable Interactions { get; set; } + public int Possible { get; set; } + public int MyScore { get; set; } + } +} diff --git a/DigitalLearningSolutions.Data/Models/Tracker/TrackerEndpointQueryParams.cs b/DigitalLearningSolutions.Data/Models/Tracker/TrackerEndpointQueryParams.cs index 8693d07308..9d2d363212 100644 --- a/DigitalLearningSolutions.Data/Models/Tracker/TrackerEndpointQueryParams.cs +++ b/DigitalLearningSolutions.Data/Models/Tracker/TrackerEndpointQueryParams.cs @@ -3,5 +3,7 @@ public class TrackerEndpointQueryParams { public string? Action { get; set; } + public int? CustomisationId { get; set; } + public int? SectionId { get; set; } } } diff --git a/DigitalLearningSolutions.Data/Models/Tracker/TrackerObjectiveArray.cs b/DigitalLearningSolutions.Data/Models/Tracker/TrackerObjectiveArray.cs new file mode 100644 index 0000000000..141b8c9790 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/Tracker/TrackerObjectiveArray.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace DigitalLearningSolutions.Data.Models.Tracker +{ + public class TrackerObjectiveArray : ITrackerEndpointDataModel + { + public TrackerObjectiveArray(IEnumerable objectives) + { + Objectives = objectives; + } + + public IEnumerable Objectives { get; set; } + } +} diff --git a/DigitalLearningSolutions.Data/Services/TrackerActionService.cs b/DigitalLearningSolutions.Data/Services/TrackerActionService.cs new file mode 100644 index 0000000000..131a16c7cd --- /dev/null +++ b/DigitalLearningSolutions.Data/Services/TrackerActionService.cs @@ -0,0 +1,37 @@ +namespace DigitalLearningSolutions.Data.Services +{ + using System.Linq; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Models.Tracker; + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; + + public interface ITrackerActionService + { + TrackerObjectiveArray? GetObjectiveArray(int? customisationId, int? sectionId); + } + + public class TrackerActionService : ITrackerActionService + { + private readonly ITutorialContentDataService tutorialContentDataService; + + public TrackerActionService(ITutorialContentDataService tutorialContentDataService) + { + this.tutorialContentDataService = tutorialContentDataService; + } + + public TrackerObjectiveArray? GetObjectiveArray(int? customisationId, int? sectionId) + { + if (!customisationId.HasValue || !sectionId.HasValue) + { + return null; + } + + var objectives = tutorialContentDataService + .GetNonArchivedObjectivesBySectionAndCustomisationId(sectionId.Value, customisationId.Value) + .ToList(); + + return objectives.Any() ? new TrackerObjectiveArray(objectives) : null; + } + } +} diff --git a/DigitalLearningSolutions.Data/Services/TrackerService.cs b/DigitalLearningSolutions.Data/Services/TrackerService.cs index c175eb59c0..8ae1afc631 100644 --- a/DigitalLearningSolutions.Data/Services/TrackerService.cs +++ b/DigitalLearningSolutions.Data/Services/TrackerService.cs @@ -4,6 +4,8 @@ using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Models.Tracker; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; public interface ITrackerService { @@ -14,9 +16,15 @@ public class TrackerService : ITrackerService { private readonly ILogger logger; - public TrackerService(ILogger logger) + private readonly JsonSerializerSettings settings = new JsonSerializerSettings + { ContractResolver = new LowercaseContractResolver() }; + + private readonly ITrackerActionService trackerActionService; + + public TrackerService(ILogger logger, ITrackerActionService trackerActionService) { this.logger = logger; + this.trackerActionService = trackerActionService; } public string ProcessQuery(TrackerEndpointQueryParams query) @@ -30,10 +38,16 @@ public string ProcessQuery(TrackerEndpointQueryParams query) { if (Enum.TryParse(query.Action, true, out var action)) { - return action switch + var actionDataResult = action switch { - _ => throw new ArgumentOutOfRangeException() + TrackerEndpointAction.GetObjectiveArray => trackerActionService.GetObjectiveArray( + query.CustomisationId, + query.SectionId + ), + _ => throw new ArgumentOutOfRangeException(), }; + + return ConvertToJsonString(actionDataResult); } return TrackerEndpointErrorResponse.InvalidAction; @@ -44,5 +58,23 @@ public string ProcessQuery(TrackerEndpointQueryParams query) return TrackerEndpointErrorResponse.UnexpectedException; } } + + private string ConvertToJsonString(ITrackerEndpointDataModel? data) + { + if (data == null) + { + return JsonConvert.SerializeObject(new { }); + } + + return JsonConvert.SerializeObject(data, settings); + } + + private class LowercaseContractResolver : DefaultContractResolver + { + protected override string ResolvePropertyName(string propertyName) + { + return propertyName.ToLower(); + } + } } } diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index 75ffb8766a..ba1e7d689a 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -193,6 +193,7 @@ private static void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();