Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace DigitalLearningSolutions.Data.Tests.DataServices

public class CourseDataServiceTests
{
private CourseDataDataService courseDataService;
private CourseDataService courseDataService = null!;

[OneTimeSetUp]
public void OneTimeSetUp()
Expand All @@ -26,8 +26,8 @@ public void OneTimeSetUp()
public void Setup()
{
var connection = ServiceTestHelper.GetDatabaseConnection();
var logger = A.Fake<ILogger<CourseDataDataService>>();
courseDataService = new CourseDataDataService(connection, logger);
var logger = A.Fake<ILogger<CourseDataService>>();
courseDataService = new CourseDataService(connection, logger);
}

[Test]
Expand Down Expand Up @@ -216,5 +216,35 @@ public void GetNumberOfActiveCoursesAtCentre_with_filtered_category_returns_expe
// Then
count.Should().Be(3);
}

[Test]
public void GetCourseStatisticsAtCentreForCategoryID_should_return_course_statistics_correctly()
{
// Given
const int centreId = 101;
const int categoryId = 0;

// When
var result = courseDataService.GetCourseStatisticsAtCentreForCategoryId(centreId, categoryId).ToList();

// Then
var expectedFirstCourse = new CourseStatistics
{
CustomisationId = 100,
CentreId = 101,
Active = false,
AllCentres = false,
ArchivedDate = null,
ApplicationName = "Entry Level - Win XP, Office 2003/07 OLD",
CustomisationName = "Standard",
DelegateCount = 25,
AllAttempts = 49,
AttemptsPassed = 34,
CompletedCount = 5
};

result.Should().HaveCount(267);
result.First().Should().BeEquivalentTo(expectedFirstCourse);
}
}
}
66 changes: 66 additions & 0 deletions DigitalLearningSolutions.Data.Tests/Models/CourseStatisticTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace DigitalLearningSolutions.Data.Tests.Models
{
using DigitalLearningSolutions.Data.Models.Courses;
using FluentAssertions;
using NUnit.Framework;

public class CourseStatisticTests
{
[Test]
public void Pass_rate_should_be_calculated_from_attempts_and_rounded()
{
// When
var courseStatistics = new CourseStatistics
{
AllAttempts = 1000,
AttemptsPassed = 514
};

// Then
courseStatistics.PassRate.Should().Be(51);
}

[Test]
public void InProgressCount_should_be_calculated_from_total_delegates_and_total_completed()
{
// When
var courseStatistics = new CourseStatistics
{
DelegateCount = 90,
CompletedCount = 48
};

// Then
courseStatistics.InProgressCount.Should().Be(42);
}

[Test]
public void Course_name_should_be_application_name_if_customisation_name_is_null()
{
// When
var courseStatistics = new CourseStatistics
{
ApplicationName = "Test application",
CustomisationName = null,
};

// Then
courseStatistics.CourseName.Should().BeEquivalentTo("Test application");
}

[Test]
public void Course_name_should_include_customisation_name_if_it_is_not_null()
{
// When
var courseStatistics = new CourseStatistics
{
ApplicationName = "Test application",
CustomisationName = "customisation",
};

// Then
courseStatistics.CourseName.Should().BeEquivalentTo("Test application - customisation");
}

}
}
52 changes: 49 additions & 3 deletions DigitalLearningSolutions.Data/DataServices/CourseDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,35 @@ public interface ICourseDataService
void RemoveCurrentCourse(int progressId, int candidateId);
void EnrolOnSelfAssessment(int selfAssessmentId, int candidateId);
int GetNumberOfActiveCoursesAtCentreForCategory(int centreId, int categoryId);
IEnumerable<CourseStatistics> GetCourseStatisticsAtCentreForCategoryId(int centreId, int categoryId);
}

public class CourseDataDataService : ICourseDataService
public class CourseDataService : ICourseDataService
{
private const string DelegateCountQuery =
@"(SELECT COUNT(CandidateID)
FROM dbo.Progress AS pr
WHERE pr.CustomisationID = cu.CustomisationID) AS DelegateCount";

private const string CompletedCountQuery =
@"(SELECT COUNT(CandidateID)
FROM dbo.Progress AS pr
WHERE pr.CustomisationID = cu.CustomisationID AND pr.Completed IS NOT NULL) AS CompletedCount";

private const string AllAttemptsQuery =
@"(SELECT COUNT(AssessAttemptID)
FROM dbo.AssessAttempts AS aa
WHERE aa.CustomisationID = cu.CustomisationID AND aa.[Status] IS NOT NULL) AS AllAttempts";

private const string AttemptsPassedQuery =
@"(SELECT COUNT(AssessAttemptID)
FROM dbo.AssessAttempts AS aa
WHERE aa.CustomisationID = cu.CustomisationID AND aa.[Status] = 1) AS AttemptsPassed";

private readonly IDbConnection connection;
private readonly ILogger<CourseDataDataService> logger;
private readonly ILogger<CourseDataService> logger;

public CourseDataDataService(IDbConnection connection, ILogger<CourseDataDataService> logger)
public CourseDataService(IDbConnection connection, ILogger<CourseDataService> logger)
{
this.connection = connection;
this.logger = logger;
Expand Down Expand Up @@ -127,5 +148,30 @@ FROM Customisations AS c
new { centreId, adminCategoryId }
);
}

public IEnumerable<CourseStatistics> GetCourseStatisticsAtCentreForCategoryId(int centreId, int categoryId)
{
return connection.Query<CourseStatistics>(
@$"SELECT
cu.CustomisationID,
cu.CentreID,
cu.Active,
cu.AllCentres,
ap.ArchivedDate,
ap.ApplicationName,
cu.CustomisationName,
{DelegateCountQuery},
{CompletedCountQuery},
{AllAttemptsQuery},
{AttemptsPassedQuery}
FROM dbo.Customisations AS cu
INNER JOIN dbo.CentreApplications AS ca ON ca.ApplicationID = cu.ApplicationID
INNER JOIN dbo.Applications AS ap ON ap.ApplicationID = ca.ApplicationID
WHERE (ap.CourseCategoryID = @categoryId OR @categoryId = 0)
AND (cu.CentreID = @centreId OR (cu.AllCentres = 1 AND ca.Active = 1))
AND ca.CentreID = @centreId",
new { centreId, categoryId }
);
}
}
}
25 changes: 25 additions & 0 deletions DigitalLearningSolutions.Data/Models/Courses/CourseStatistics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace DigitalLearningSolutions.Data.Models.Courses
{
using System;

public class CourseStatistics
{
public int CustomisationId { get; set; }
public int CentreId { get; set; }
public bool Active { get; set; }
public bool AllCentres { get; set; }
public DateTime? ArchivedDate { get; set; }
public string ApplicationName { get; set; }
public string? CustomisationName { get; set; }
public int DelegateCount { get; set; }
public int CompletedCount { get; set; }
public int InProgressCount => DelegateCount - CompletedCount;
public int AllAttempts { get; set; }
public int AttemptsPassed { get; set; }

public string CourseName => string.IsNullOrWhiteSpace(CustomisationName)
? ApplicationName
: ApplicationName + " - " + CustomisationName;
public double PassRate => AllAttempts == 0 ? 0 : Math.Round(100 * AttemptsPassed / (double)AllAttempts);
}
}
28 changes: 28 additions & 0 deletions DigitalLearningSolutions.Data/Services/CourseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace DigitalLearningSolutions.Data.Services
{
using System.Collections.Generic;
using System.Linq;
using DigitalLearningSolutions.Data.DataServices;
using DigitalLearningSolutions.Data.Models.Courses;

public interface ICourseService
{
IEnumerable<CourseStatistics> GetTopCourseStatistics(int centreId, int categoryId);
}

public class CourseService : ICourseService
{
private readonly ICourseDataService courseDataService;

public CourseService(ICourseDataService courseDataService)
{
this.courseDataService = courseDataService;
}

public IEnumerable<CourseStatistics> GetTopCourseStatistics(int centreId, int categoryId)
{
var allCourses = courseDataService.GetCourseStatisticsAtCentreForCategoryId(centreId, categoryId);
return allCourses.Where(c => c.Active).OrderByDescending(c => c.InProgressCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public void Page_has_no_accessibility_errors(string url, string pageTitle)
[InlineData("/TrackingSystem/CentreConfiguration/RegistrationPrompts", "Manage delegate registration prompts")]
[InlineData("/TrackingSystem/CentreConfiguration/RegistrationPrompts/1/Remove", "Remove delegate registration prompt")]
[InlineData("/TrackingSystem/Centre/Reports", "Centre reports")]
[InlineData("/TrackingSystem/Centre/TopCourses", "Top courses")]
[InlineData("/TrackingSystem/Delegates/Approve", "Approve delegate registrations")]
[InlineData("/NotificationPreferences", "Notification preferences")]
[InlineData("/NotificationPreferences/Edit/AdminUser", "Update notification preferences")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Centre.Dashboard
{
using System.Linq;
using DigitalLearningSolutions.Data.Services;
using DigitalLearningSolutions.Web.Helpers;
using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.TopCourses;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = CustomPolicies.UserCentreAdmin)]
[Route("/TrackingSystem/Centre/TopCourses")]
public class TopCoursesController : Controller
{
private readonly ICourseService courseService;
private const int NumberOfTopCourses = 10;

public TopCoursesController(ICourseService courseService)
{
this.courseService = courseService;
}

public IActionResult Index()
{
var centreId = User.GetCentreId();
var adminCategoryId = User.GetAdminCategoryId()!;

var topCourses =
courseService.GetTopCourseStatistics(centreId, adminCategoryId.Value).Take(NumberOfTopCourses);

var model = new TopCoursesViewModel(topCourses);

return View(model);
}
}
}
5 changes: 5 additions & 0 deletions DigitalLearningSolutions.Web/Helpers/CustomClaimHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public static int GetCentreId(this ClaimsPrincipal user)
return user.GetCustomClaimAsRequiredInt(CustomClaimTypes.UserCentreId);
}

public static int? GetAdminCategoryId(this ClaimsPrincipal user)
{
return user.GetCustomClaimAsInt(CustomClaimTypes.AdminCategoryId);
}

public static string? GetCustomClaim(this ClaimsPrincipal user, string customClaimType)
{
return user.FindFirst(customClaimType)?.Value;
Expand Down
3 changes: 2 additions & 1 deletion DigitalLearningSolutions.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddScoped<ICentresService, CentresService>();
services.AddScoped<ICentresDataService, CentresDataService>();
services.AddScoped<IConfigService, ConfigService>();
services.AddScoped<ICourseDataService, CourseDataDataService>();
services.AddScoped<ICourseService, CourseService>();
services.AddScoped<ICourseDataService, CourseDataService>();
services.AddScoped<ILogoService, LogoService>();
services.AddScoped<ISmtpClientFactory, SmtpClientFactory>();
services.AddScoped<INotificationDataService, NotificationDataService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.TopCourses
{
using System.Collections.Generic;
using DigitalLearningSolutions.Data.Models.Courses;

public class TopCoursesViewModel
{
public TopCoursesViewModel(IEnumerable<CourseStatistics> topCourses)
{
TopCourses = topCourses;
}
public IEnumerable<CourseStatistics> TopCourses { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<div class="nhsuk-card nhsuk-card--clickable">
<div class="nhsuk-card__content">
<h2 class="nhsuk-card__heading nhsuk-heading-m">
<a class="nhsuk-card__link" href="#">Top courses</a>
<a class="nhsuk-card__link" asp-controller="TopCourses" asp-action="Index">Top courses</a>
</h2>
<p class="nhsuk-card__description">See information about your top courses</p>
</div>
Expand Down
Loading