diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index 695e93e2..f474d537 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -9,10 +9,11 @@ True + - + diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index e300e80e..42778aee 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -265,5 +265,10 @@ public Settings() /// Gets or sets AllCataloguePageSize. /// public int AllCataloguePageSize { get; set; } + + /// + /// Gets or sets the StatMandId. + /// + public int StatMandId { get; set; } = 12; } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs new file mode 100644 index 00000000..4ea7d3a5 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -0,0 +1,429 @@ +namespace LearningHub.Nhs.WebUI.Controllers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Azure; + using GDS.MultiPageFormData; + using GDS.MultiPageFormData.Enums; + using LearningHub.Nhs.Caching; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Moodle; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Configuration; + using LearningHub.Nhs.WebUI.Filters; + using LearningHub.Nhs.WebUI.Helpers; + using LearningHub.Nhs.WebUI.Interfaces; + using LearningHub.Nhs.WebUI.Models; + using LearningHub.Nhs.WebUI.Models.Account; + using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; + using LearningHub.Nhs.WebUI.Models.Learning; + using LearningHub.Nhs.WebUI.Models.Report; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// Defines the . + /// + // [ServiceFilter(typeof(LoginWizardFilter))] + [Authorize] + [Route("Reports")] + public class ReportsController : BaseController + { + private const int ReportPageSize = 10; + private readonly ICacheService cacheService; + private readonly ICategoryService categoryService; + private readonly IMultiPageFormService multiPageFormService; + private readonly IReportService reportService; + private readonly IFileService fileService; + + /// + /// Initializes a new instance of the class. + /// + /// httpClientFactory. + /// cacheService. + /// multiPageFormService. + /// reportService. + /// categoryService. + /// fileService. + /// The hostingEnvironment. + /// The logger. + /// settings. + public ReportsController(IHttpClientFactory httpClientFactory, IWebHostEnvironment hostingEnvironment, ILogger logger, IOptions settings, ICacheService cacheService, IMultiPageFormService multiPageFormService, IReportService reportService, ICategoryService categoryService, IFileService fileService) + : base(hostingEnvironment, httpClientFactory, logger, settings.Value) + { + this.cacheService = cacheService; + this.multiPageFormService = multiPageFormService; + this.reportService = reportService; + this.categoryService = categoryService; + this.fileService = fileService; + } + + /// + /// The Report landing page. + /// + /// reportHistoryViewModel. + /// The . + [ResponseCache(CacheProfileName = "Never")] + public async Task Index(ReportHistoryViewModel reportHistoryViewModel = null) + { + int page = 1; + this.TempData.Clear(); + var newReport = new DatabricksRequestModel { Take = ReportPageSize, Skip = 0 }; + + await this.multiPageFormService.SetMultiPageFormData( + newReport, + MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), + this.TempData); + + var historyRequest = new PagingRequestModel + { + PageSize = ReportPageSize, + }; + + switch (reportHistoryViewModel.ReportFormActionType) + { + case ReportFormActionTypeEnum.NextPageChange: + reportHistoryViewModel.CurrentPageIndex += 1; + break; + + case ReportFormActionTypeEnum.PreviousPageChange: + reportHistoryViewModel.CurrentPageIndex -= 1; + break; + default: + reportHistoryViewModel.CurrentPageIndex = 0; + break; + } + + page = page + reportHistoryViewModel.CurrentPageIndex; + historyRequest.Page = page; + + // get a list of report history and send to view + var result = await this.reportService.GetReportHistory(historyRequest); + if (result != null) + { + reportHistoryViewModel.TotalCount = result.TotalItemCount; + reportHistoryViewModel.ReportHistoryModels = result.Items; + } + + this.ViewData["AllCourses"] = await this.GetCoursesAsync(); + reportHistoryViewModel.ReportPaging = new ReportPagingModel() { CurrentPage = reportHistoryViewModel.CurrentPageIndex, PageSize = ReportPageSize, TotalItems = reportHistoryViewModel.TotalCount, HasItems = reportHistoryViewModel.TotalCount > 0 }; + return this.View(reportHistoryViewModel); + } + + /// + /// CreateReportCourseSelection. + /// + /// searchText. + /// A representing the result of the asynchronous operation. + [Route("CreateReportCourseSelection")] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportCourseSelection(string searchText = "") + { + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + var coursevm = new ReportCreationCourseSelection { SearchText = searchText, Courses = reportCreation.Courses != null ? reportCreation.Courses : new List() }; + var getCourses = await this.GetCoursesAsync(); + if (!string.IsNullOrWhiteSpace(searchText)) + { + getCourses = getCourses.Where(x => x.Value.ToLower().Contains(searchText.ToLower())).ToList(); + } + + if (coursevm.Courses.Count == 0 && !string.IsNullOrWhiteSpace(reportCreation.TimePeriod)) + { + coursevm.Courses = new List { "all" }; + } + + coursevm.BuildCourses(getCourses); + return this.View(coursevm); + } + + /// + /// CreateReportCourseSelection. + /// + /// courseSelection. + /// A representing the result of the asynchronous operation. + [HttpPost] + [Route("CreateReportCourseSelection")] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportCourseSelection(ReportCreationCourseSelection courseSelection) + { + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + if (courseSelection != null) + { + if (courseSelection.Courses.Any()) + { + reportCreation.Courses = courseSelection.Courses.Contains("all") ? new List() : courseSelection.Courses; + await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + return this.RedirectToAction("CreateReportDateSelection"); + } + } + + this.ModelState.AddModelError("Courses", CommonValidationErrorMessages.CourseRequired); + courseSelection.BuildCourses(await this.GetCoursesAsync()); + courseSelection.Courses = reportCreation.Courses; + return this.View("CreateReportCourseSelection", courseSelection); + } + + /// + /// CreateReportDateSelection. + /// + /// A representing the result of the asynchronous operation. + [Route("CreateReportDateSelection")] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportDateSelection() + { + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + var dateVM = new ReportCreationDateSelection(); + dateVM.TimePeriod = reportCreation.TimePeriod; + if (reportCreation.StartDate.HasValue) + { + dateVM.StartDay = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Day : 0; + dateVM.StartMonth = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Month : 0; + dateVM.StartYear = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Year : 0; + dateVM.EndDay = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Day : 0; + dateVM.EndMonth = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Month : 0; + dateVM.EndYear = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Year : 0; + } + else + { + var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel + { + StartDate = reportCreation.StartDate, + EndDate = reportCreation.EndDate, + TimePeriod = reportCreation.TimePeriod, + Courses = reportCreation.Courses, + ReportHistoryId = reportCreation.ReportHistoryId, + Take = 1, + Skip = 1, + }); + + DateTime startDate = DateTime.MinValue; + var validDate = DateTime.TryParse(result.MinValidDate, out startDate); + dateVM.StartDay = validDate ? startDate.Day : 0; + dateVM.StartMonth = validDate ? startDate.Month : 0; + dateVM.StartYear = validDate ? startDate.Year : 0; + } + + return this.View(dateVM); + } + + /// + /// CreateReportDateSelection. + /// + /// reportCreationDate. + /// A representing the result of the asynchronous operation. + [Route("CreateReportSummary")] + [HttpPost] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportSummary(ReportCreationDateSelection reportCreationDate) + { + // validate date + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + reportCreation.TimePeriod = reportCreationDate.TimePeriod; + reportCreation.StartDate = reportCreationDate.GetStartDate(); + reportCreation.EndDate = reportCreationDate.GetEndDate(); + await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + return this.RedirectToAction("CourseCompletionReport"); + } + + /// + /// ViewReport. + /// + /// reportHistoryId. + /// A representing the result of the asynchronous operation. + [Route("ViewReport/{reportHistoryId}")] + [HttpGet] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task ViewReport(int reportHistoryId) + { + this.TempData.Clear(); + var report = await this.reportService.GetReportHistoryById(reportHistoryId); + if (report == null) + { + return this.RedirectToAction("Index"); + } + + var reportRequest = new DatabricksRequestModel { Take = ReportPageSize, Skip = 0 }; + var periodCheck = int.TryParse(report.PeriodDays.ToString(), out int numberOfDays); + if (report.PeriodDays > 0 && periodCheck) + { + reportRequest.TimePeriod = report.PeriodDays.ToString(); + reportRequest.StartDate = DateTime.Now.AddDays(-numberOfDays); + reportRequest.EndDate = DateTime.Now; + } + else + { + reportRequest.TimePeriod = "Custom"; + reportRequest.StartDate = report.StartDate; + reportRequest.EndDate = report.EndDate; + reportRequest.ReportHistoryId = reportHistoryId; + } + + if (report.CourseFilter == "all") + { + report.CourseFilter = string.Empty; + } + + reportRequest.Courses = report.CourseFilter.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(f => f.Trim()).ToList(); + + await this.multiPageFormService.SetMultiPageFormData(reportRequest, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + return this.RedirectToAction("CourseCompletionReport"); + } + + /// + /// DownloadReport. + /// + /// reportHistoryId. + /// A representing the result of the asynchronous operation. + [Route("DownloadReport/{reportHistoryId}")] + [HttpGet] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task DownloadReport(int reportHistoryId) + { + var report = await this.reportService.DownloadReport(reportHistoryId); + if (report == null) + { + return this.RedirectToAction("Index"); + } + + var result = await this.fileService.DownloadBlobFileAsync(report.FilePath); + return this.File(result.Stream, result.ContentType, result.FileName); + } + + /// + /// ViewReport. + /// + /// reportHistoryId. + /// A representing the result of the asynchronous operation. + [Route("QueueReportDownload")] + [HttpPost] + [ResponseCache(CacheProfileName = "Never")] + public async Task QueueReportDownload(int reportHistoryId) + { + await this.reportService.QueueReportDownload(reportHistoryId); + return this.RedirectToAction("CourseCompletionReport"); + } + + /// + /// CourseCompletionReport. + /// + /// courseCompletion. + /// A representing the result of the asynchronous operation. + [Route("CourseCompletionReport")] + [HttpGet] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CourseCompletionReport(CourseCompletionViewModel courseCompletion = null) + { + int page = 1; + + // validate date + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + switch (courseCompletion.ReportFormActionType) + { + case ReportFormActionTypeEnum.NextPageChange: + courseCompletion.CurrentPageIndex += 1; + page = page + courseCompletion.CurrentPageIndex; + reportCreation.Skip = courseCompletion.CurrentPageIndex * ReportPageSize; + break; + + case ReportFormActionTypeEnum.PreviousPageChange: + courseCompletion.CurrentPageIndex -= 1; + page = page + courseCompletion.CurrentPageIndex; + reportCreation.Skip = courseCompletion.CurrentPageIndex * ReportPageSize; + break; + default: + courseCompletion.CurrentPageIndex = 0; + reportCreation.Skip = 0; + break; + } + + DateTimeOffset today = DateTimeOffset.Now.Date; + DateTimeOffset? startDate = null; + DateTimeOffset? endDate = null; + + if (int.TryParse(reportCreation.TimePeriod, out int days)) + { + startDate = today.AddDays(-days); + endDate = today; + } + else if (reportCreation.TimePeriod == "Custom") + { + startDate = reportCreation.StartDate; + endDate = reportCreation.EndDate; + } + + var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel + { + StartDate = startDate, + EndDate = endDate, + TimePeriod = reportCreation.TimePeriod, + Courses = reportCreation.Courses, + ReportHistoryId = reportCreation.ReportHistoryId, + Take = reportCreation.Take, + Skip = page, + }); + + var response = new CourseCompletionViewModel(reportCreation); + + if (result != null) + { + response.TotalCount = result.TotalCount; + response.CourseCompletionRecords = result.CourseCompletionRecords; + response.ReportHistoryModel = await this.reportService.GetReportHistoryById(result.ReportHistoryId); + reportCreation.ReportHistoryId = result.ReportHistoryId; + } + + await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + var allCourses = await this.GetCoursesAsync(); + + List matchedCourseNames; + + if (reportCreation.Courses.Count == 0) + { + matchedCourseNames = allCourses.Select(course => course.Value).ToList(); + } + else + { + matchedCourseNames = allCourses + .Where(course => reportCreation.Courses.Contains(course.Key)) + .Select(course => course.Value) + .ToList(); + } + + this.ViewData["matchedCourseNames"] = matchedCourseNames; + response.ReportPaging = new ReportPagingModel() { CurrentPage = courseCompletion.CurrentPageIndex, PageSize = ReportPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; + return this.View(response); + } + + private async Task>> GetCoursesAsync() + { + int categoryId = this.Settings.StatMandId; + var courses = new List>(); + var subCategories = await this.categoryService.GetCoursesByCategoryIdAsync(categoryId); + + foreach (var subCategory in subCategories.Courses) + { + courses.Add(new KeyValuePair(subCategory.Id.ToString(), subCategory.Displayname)); + } + + return courses; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs index e41b794f..e763fefa 100644 --- a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs +++ b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs @@ -259,5 +259,10 @@ public static class CommonValidationErrorMessages /// Security question Required. /// public const string SecurityQuestionRequired = "Please select a security question"; + + /// + /// Course Required. + /// + public const string CourseRequired = "Select a course"; } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs b/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs index eb94d018..8ab50935 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs @@ -53,5 +53,12 @@ public interface IFileService /// . /// The . Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, List filePaths = null); + + /// + /// The DownloadBlobFile. + /// + /// uri. + /// The . + Task<(Stream Stream, string FileName, string ContentType)> DownloadBlobFileAsync(string uri); } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs b/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs new file mode 100644 index 00000000..8afa2cbc --- /dev/null +++ b/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs @@ -0,0 +1,57 @@ +namespace LearningHub.Nhs.WebUI.Interfaces +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + + /// + /// Defines the . + /// + public interface IReportService + { + /// + /// The GetReporterPermission. + /// + /// A representing the result of the asynchronous operation. + Task GetReporterPermission(); + + /// + /// The GetCourseCompletionReport. + /// + /// The requestModel.. + /// The . + Task GetCourseCompletionReport(DatabricksRequestModel requestModel); + + /// + /// The GetReportHistory. + /// + /// The requestModel.. + /// The . + Task> GetReportHistory(PagingRequestModel requestModel); + + /// + /// The GetReportHistory. + /// + /// The reportHistoryId.. + /// The . + Task GetReportHistoryById(int reportHistoryId); + + /// + /// The QueueReportDownload. + /// + /// The reportHistoryId.. + /// The . + Task QueueReportDownload(int reportHistoryId); + + /// + /// The DownloadReport. + /// + /// reportHistoryId. + /// The . + Task DownloadReport(int reportHistoryId); + } +} diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 39df64b2..adada3f1 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs new file mode 100644 index 00000000..47cd5ea4 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs @@ -0,0 +1,28 @@ +namespace LearningHub.Nhs.WebUI.Models.DynamicCheckbox +{ + /// + /// DynamicCheckboxItemViewModel. + /// + public class DynamicCheckboxItemViewModel + { + /// + /// Gets or sets a value. + /// + public string Value { get; set; } + + /// + /// Gets or sets a Label. + /// + public string Label { get; set; } + + /// + /// Gets or sets a HintText. + /// + public string? HintText { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets a selected. + /// + public bool Selected { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs new file mode 100644 index 00000000..85738837 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs @@ -0,0 +1,45 @@ +namespace LearningHub.Nhs.WebUI.Models.DynamicCheckbox +{ + using System.Collections.Generic; + + /// + /// DynamicCheckboxesViewModel. + /// + public class DynamicCheckboxesViewModel + { + /// + /// Gets or sets a Label. + /// + public string Label { get; set; } + + /// + /// Gets or sets a HintText. + /// + public string HintText { get; set; } + + /// + /// Gets or sets a ErrorMessage. + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets a Required. + /// + public bool Required { get; set; } + + /// + /// Gets or sets a CssClass. + /// + public string CssClass { get; set; } + + /// + /// Gets or sets SelectedValues. + /// + public List SelectedValues { get; set; } = []; + + /// + /// Gets or sets a Checkboxes. + /// + public List Checkboxes { get; set; } = []; + } +} diff --git a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs index 25b51671..93456496 100644 --- a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs +++ b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs @@ -75,6 +75,11 @@ public class NavigationModel /// public bool ShowBrowseCatalogues { get; set; } + /// + /// Gets or sets a value indicating whether to show reports. + /// + public bool ShowReports { get; set; } + /// /// Gets or sets a value indicating whether ShowHome. /// diff --git a/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs b/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs new file mode 100644 index 00000000..3e183c63 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs @@ -0,0 +1,66 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System.Collections.Generic; + using System.Reflection; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Models.Learning; + + /// + /// CourseCompletionViewModel. + /// + public class CourseCompletionViewModel : DatabricksRequestModel + { + /// + /// Initializes a new instance of the class. + /// + public CourseCompletionViewModel() + { + this.CourseCompletionRecords = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// DatabricksRequestModel. + public CourseCompletionViewModel(DatabricksRequestModel requestModel) + { + this.CourseCompletionRecords = new List(); + foreach (PropertyInfo prop in requestModel.GetType().GetProperties()) + { + this.GetType().GetProperty(prop.Name).SetValue(this, prop.GetValue(requestModel, null), null); + } + } + + /// + /// Gets or sets the CurrentPageIndex. + /// + public int CurrentPageIndex { get; set; } = 0; + + /// + /// Gets or sets the TotalCount. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the report result paging. + /// + public PagingViewModel ReportPaging { get; set; } + + /// + /// Gets or sets the ReportFormActionTypeEnum. + /// + public ReportFormActionTypeEnum ReportFormActionType { get; set; } + + /// + /// Gets or sets the CourseCompletionRecords. + /// + public List CourseCompletionRecords { get; set; } + + /// + /// Gets or sets the ReportHistoryModel. + /// + public ReportHistoryModel ReportHistoryModel { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs new file mode 100644 index 00000000..8d760098 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs @@ -0,0 +1,46 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Linq; + using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// CourseSelection. + /// + public class ReportCreationCourseSelection + { + /// + /// Gets or sets the list of courses. + /// + public List Courses { get; set; } + + /// + /// Gets or sets the list of all courses. + /// + public List AllCources { get; set; } + + /// + /// Gets or sets the list of SearchText. + /// + public string SearchText { get; set; } + + /// + /// BuildCourses. + /// + /// The all Courses. + /// The . + public List BuildCourses(List> allCourses) + { + this.AllCources = allCourses.Select(r => new DynamicCheckboxItemViewModel + { + Value = r.Key.ToString(), + Label = r.Value, + }).ToList(); + this.AllCources.Insert(0, new DynamicCheckboxItemViewModel { Value = "all", Label = "All Courses", }); + return this.AllCources; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs new file mode 100644 index 00000000..2263a0aa --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs @@ -0,0 +1,225 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using LearningHub.Nhs.WebUI.Helpers; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// ReportCreationDateSelection. + /// + public class ReportCreationDateSelection : IValidatableObject + { + /// + /// Gets or sets the start date to define on the search. + /// + public string TimePeriod { get; set; } + + /// + /// Gets or sets the start date to define on the search. + /// + /// + /// Gets or sets the Day. + /// + public int? StartDay { get; set; } + + /// + /// Gets or sets the end Day. + /// + public int? EndDay { get; set; } + + /// + /// Gets or sets the Country. + /// + public int? StartMonth { get; set; } + + /// + /// Gets or sets the Country. + /// + public int? EndMonth { get; set; } + + /// + /// Gets or sets the Year. + /// + public int? StartYear { get; set; } + + /// + /// Gets or sets the Year. + /// + public int? EndYear { get; set; } + + /// + /// Gets or sets the Year. + /// + public DateTime? DataStart { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets the EndDate. + /// + public bool EndDate { get; set; } + + /// + /// Gets or sets the GetDate. + /// + /// DateTime. + public DateTime? GetStartDate() + { + return (this.StartDay.HasValue && this.StartMonth.HasValue && this.StartYear.HasValue) ? new DateTime(this.StartYear!.Value, this.StartMonth!.Value, this.StartDay!.Value) : (DateTime?)null; + } + + /// + /// Gets or sets the GetDate. + /// + /// DateTime. + public DateTime? GetEndDate() + { + return (this.EndDay.HasValue && this.EndMonth.HasValue && this.EndYear.HasValue) ? new DateTime(this.EndYear!.Value, this.EndMonth!.Value, this.EndDay!.Value) : (DateTime?)null; + } + + /// + public IEnumerable Validate(ValidationContext validationContext) + { + var validationResults = new List(); + if (this.TimePeriod == "Custom") + { + this.ValidateStartDate(validationResults); + + if (this.EndDate) + { + this.ValidateEndDate(validationResults); + } + } + + return validationResults; + } + + /// + /// Gets or sets the GetValidatedStartDate. + /// + /// DateTime. + public DateTime GetValidatedStartDate() + { + return new DateTime(this.StartYear!.Value, this.StartMonth!.Value, this.StartDay!.Value); + } + + /// + /// Gets or sets the GetValidatedEndDate. + /// + /// DateTime. + public DateTime? GetValidatedEndDate() + { + return this.EndDate + ? new DateTime(this.EndYear!.Value, this.EndMonth!.Value, this.EndDay!.Value) + : (DateTime?)null; + } + + /// + /// sets the list of radio region. + /// + /// The . + public List PopulateDateRange() + { + var radios = new List() + { + new RadiosItemViewModel("7", "7 days", false, null), + new RadiosItemViewModel("30", "30 days", false, null), + new RadiosItemViewModel("90", "90 days", false, null), + }; + + // if (string.IsNullOrWhiteSpace(this.TimePeriod)) + // { + // this.TimePeriod = "Custom"; + // } + return radios; + } + + /// + // public IEnumerable Validate(ValidationContext validationContext) + // { + // var results = new List(); + // if (this.TimePeriod == "dateRange") + // { + // var startDateValidation = DateValidator.ValidateDate(this.StartDay, this.StartMonth, this.StartYear, "valid start date") + // .ToValidationResultList(nameof(this.StartDay), nameof(this.StartMonth), nameof(this.StartYear)); + // if (startDateValidation.Any()) + // { + // results.AddRange(startDateValidation); + // } + // var endDateValidation = DateValidator.ValidateDate(this.EndDay, this.EndMonth, this.EndYear, "valid end date") + // .ToValidationResultList(nameof(this.EndDay), nameof(this.EndMonth), nameof(this.EndYear)); + // if (endDateValidation.Any()) + // { + // results.AddRange(endDateValidation); + // } + // } + // return results; + // } + private void ValidateStartDate(List validationResults) + { + var startDateValidationResults = DateValidator.ValidateDate( + this.StartDay, + this.StartMonth, + this.StartYear, + "Start date", + true, + false, + true) + .ToValidationResultList(nameof(this.StartDay), nameof(this.StartMonth), nameof(this.StartYear)); + + if (!startDateValidationResults.Any()) + { + this.ValidateStartDateIsAfterDataStart(startDateValidationResults); + } + + validationResults.AddRange(startDateValidationResults); + } + + private void ValidateStartDateIsAfterDataStart(List startDateValidationResults) + { + var startDate = this.GetValidatedStartDate(); + + if (startDate.AddDays(1) < this.DataStart) + { + startDateValidationResults.Add( + new ValidationResult( + "Enter a start date after the start of data in the platform", new[] { nameof(this.StartDay), })); + startDateValidationResults.Add( + new ValidationResult( + string.Empty, + new[] { nameof(this.StartMonth), nameof(this.StartYear), })); + } + } + + private void ValidateEndDate(List validationResults) + { + var endDateValidationResults = DateValidator.ValidateDate( + this.EndDay, + this.EndMonth, + this.EndYear, + "End date", + true, + false, + true) + .ToValidationResultList(nameof(this.EndDay), nameof(this.EndMonth), nameof(this.EndYear)); + + this.ValidateEndDateIsAfterStartDate(endDateValidationResults); + + validationResults.AddRange(endDateValidationResults); + } + + private void ValidateEndDateIsAfterStartDate(List endDateValidationResults) + { + if (this.StartYear > this.EndYear + || (this.StartYear == this.EndYear && this.StartMonth > this.EndMonth) + || (this.StartYear == this.EndYear && this.StartMonth == this.EndMonth && this.StartDay > this.EndDay)) + { + endDateValidationResults.Add( + new ValidationResult("Enter an end date after the start date", new[] { nameof(this.EndDay), })); + endDateValidationResults.Add( + new ValidationResult(string.Empty, new[] { nameof(this.EndMonth), nameof(this.EndYear), })); + } + } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs new file mode 100644 index 00000000..2e38defc --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs @@ -0,0 +1,23 @@ +namespace LearningHub.Nhs.WebUI.Models.Learning +{ + /// + /// Defines the ReportFormActionTypeEnum. + /// + public enum ReportFormActionTypeEnum + { + /// + /// Defines the basic search for mylearning + /// + BasicSearch = 0, + + /// + /// Previoous page change. + /// + PreviousPageChange = 1, + + /// + /// Next page change. + /// + NextPageChange = 2, + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs new file mode 100644 index 00000000..a958c39f --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs @@ -0,0 +1,38 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System.Collections.Generic; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Models.Learning; + + /// + /// ReportHistoryViewModel. + /// + public class ReportHistoryViewModel + { + /// + /// Gets or sets the CurrentPageIndex. + /// + public int CurrentPageIndex { get; set; } = 0; + + /// + /// Gets or sets the TotalCount. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the report result paging. + /// + public PagingViewModel ReportPaging { get; set; } + + /// + /// Gets or sets the ReportFormActionTypeEnum. + /// + public ReportFormActionTypeEnum ReportFormActionType { get; set; } + + /// + /// Gets or sets the ReportHistoryModels. + /// + public List ReportHistoryModels { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs new file mode 100644 index 00000000..5468bab7 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs @@ -0,0 +1,20 @@ +namespace LearningHub.Nhs.WebUI.Models.Learning +{ + using LearningHub.Nhs.Models.Paging; + + /// + /// Defines the . + /// + public class ReportPagingModel : PagingViewModel + { + /// + /// Gets or sets the page previous action value. + /// + public int PreviousActionValue { get; set; } + + /// + /// Gets or sets the page next action value. + /// + public int NextActionValue { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index c24d9057..dda754ec 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -35,6 +35,19 @@ builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); builder.Host.UseNLog(); + string corsMoodleUrl = builder.Configuration.GetValue("MoodleAPIConfig:BaseUrl"); + + builder.Services.AddCors(options => + { + options.AddPolicy("MoodleCORS", builder => + { + builder.WithOrigins(corsMoodleUrl.TrimEnd('/')) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + builder.Services.AddHostedService(); builder.Services.ConfigureServices(builder.Configuration, builder.Environment); @@ -80,6 +93,8 @@ app.UseRouting(); + app.UseCors("MoodleCORS"); + app.UseAuthentication(); app.UseAuthorization(); diff --git a/LearningHub.Nhs.WebUI/Services/FileService.cs b/LearningHub.Nhs.WebUI/Services/FileService.cs index a09195d6..fda4a044 100644 --- a/LearningHub.Nhs.WebUI/Services/FileService.cs +++ b/LearningHub.Nhs.WebUI/Services/FileService.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; + using Azure.Storage.Blobs; using Azure.Storage.Files.Shares; using Azure.Storage.Files.Shares.Models; using Azure.Storage.Sas; @@ -12,6 +13,7 @@ using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Options; @@ -249,6 +251,26 @@ public async Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, } } + /// + /// The DownloadBlobFile. + /// + /// url. + /// The . + public async Task<(Stream Stream, string FileName, string ContentType)> DownloadBlobFileAsync(string url) + { + var uri = new Uri(url); + string containerName = uri.Segments[1].TrimEnd('/'); + string blobName = string.Join(string.Empty, uri.Segments, 2, uri.Segments.Length - 2); + BlobClient blobClient = new BlobClient(this.settings.AzureBlobSettings.ConnectionString, containerName, blobName); + + var properties = await blobClient.GetPropertiesAsync(); + string contentType = properties.Value.ContentType ?? "application/octet-stream"; + string fileName = Path.GetFileName(blobClient.Name); + var stream = await blobClient.OpenReadAsync(); + + return (stream, fileName, contentType); + } + private static async Task WaitForCopyAsync(ShareFileClient fileClient) { // Wait for the copy operation to complete diff --git a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs index 7c357ada..bae61e8e 100644 --- a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs +++ b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs @@ -12,18 +12,22 @@ public class NavigationPermissionService : INavigationPermissionService { private readonly IResourceService resourceService; private readonly IUserGroupService userGroupService; + private readonly IReportService reportService; /// /// Initializes a new instance of the class. /// /// Resource service. /// UserGroup service. + /// Report Service. public NavigationPermissionService( IResourceService resourceService, - IUserGroupService userGroupService) + IUserGroupService userGroupService, + IReportService reportService) { this.resourceService = resourceService; this.userGroupService = userGroupService; + this.reportService = reportService; } /// @@ -87,6 +91,7 @@ public NavigationModel NotAuthenticated() ShowSignOut = false, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -113,6 +118,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = true, }; } @@ -139,6 +145,7 @@ private async Task AuthenticatedBlueUser(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = await this.reportService.GetReporterPermission(), }; } @@ -164,6 +171,7 @@ private NavigationModel AuthenticatedGuest() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -190,6 +198,7 @@ private async Task AuthenticatedReadOnly(string controllerName) ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = true, + ShowReports = await this.reportService.GetReporterPermission(), }; } @@ -215,6 +224,7 @@ private async Task AuthenticatedBasicUserOnly() ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = false, }; } @@ -240,6 +250,7 @@ private NavigationModel InLoginWizard() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } } diff --git a/LearningHub.Nhs.WebUI/Services/ReportService.cs b/LearningHub.Nhs.WebUI/Services/ReportService.cs new file mode 100644 index 00000000..de383e4d --- /dev/null +++ b/LearningHub.Nhs.WebUI/Services/ReportService.cs @@ -0,0 +1,194 @@ +namespace LearningHub.Nhs.WebUI.Services +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.WebUI.Interfaces; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + + /// + /// Defines the . + /// + public class ReportService : BaseService, IReportService + { + /// + /// Initializes a new instance of the class. + /// + /// The Web Api Http Client. + /// The Open Api Http Client. + /// logger. + public ReportService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger) + : base(learningHubHttpClient, openApiHttpClient, logger) + { + } + + /// + /// The GetAllAsync. + /// + /// The . + public async Task GetReporterPermission() + { + bool viewmodel = false; + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetReporterPermission"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + viewmodel = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized + || + response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return viewmodel; + } + + /// + /// The GetCourseCompletionReport. + /// + /// The requestModel.. + /// The . + public async Task GetCourseCompletionReport(DatabricksRequestModel requestModel) + { + DatabricksDetailedViewModel apiResponse = null; + var json = JsonConvert.SerializeObject(requestModel); + var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetCourseCompletionReport"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The GetReportHistory. + /// + /// The requestModel.. + /// The . + public async Task> GetReportHistory(PagingRequestModel requestModel) + { + PagedResultSet apiResponse = null; + var json = JsonConvert.SerializeObject(requestModel); + var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetReportHistory"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject>(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The GetReportHistory. + /// + /// The reportHistoryId.. + /// The . + public async Task GetReportHistoryById(int reportHistoryId) + { + ReportHistoryModel apiResponse = null; + var client = await this.OpenApiHttpClient.GetClientAsync(); + var request = $"Report/GetReportHistoryById/{reportHistoryId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The QueueReportDownload. + /// + /// The reportHistoryId.. + /// The . + public async Task QueueReportDownload(int reportHistoryId) + { + bool apiResponse = false; + var client = await this.OpenApiHttpClient.GetClientAsync(); + var request = $"Report/QueueReportDownload/{reportHistoryId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The DownloadReport. + /// + /// reportHistoryId. + /// The . + public async Task DownloadReport(int reportHistoryId) + { + ReportHistoryModel apiResponse = null; + var client = await this.OpenApiHttpClient.GetClientAsync(); + var request = $"Report/DownloadReport/{reportHistoryId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index 6ed25835..26f75aeb 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -81,6 +81,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss new file mode 100644 index 00000000..0f6d9990 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss @@ -0,0 +1,56 @@ +@use "../../abstracts/all" as *; + + +.user-report { + .nhsuk-details__summary { + display: flex; + justify-content: space-between; + align-items: center; + } + + .override-summary-color { + color: black !important; + font-weight: normal; + text-decoration: none; + } + + .nhsuk-summary-list__key--tight { + flex: 0 0 20%; + width: auto; + } + + .nhsuk-summary-list__row { + display: flex !important; + align-items: flex-start; /* optional: aligns top of key/value */ + } + + .nhsuk-summary-list__row { + border-bottom: none; + } + + .nhsuk-summary-list__key, .nhsuk-summary-list__value, .nhsuk-summary-list__actions { + border: none; + } + + .nhsuk-date-inline { + display: flex; + align-items: center; + gap: 0.5rem; /* space between label and input */ + } + + .nhsuk-date-inline .nhsuk-label { + margin-bottom: 0; + white-space: nowrap; + } + + .nhsuk-button--with-border { + border: 2px solid #005eb8; + background-color: #ffffff; + color: #005eb8; + } + + .nhsuk-button--with-border:hover { + background-color: #f0f8ff; + border-color: #003087; + } +} diff --git a/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs b/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs new file mode 100644 index 00000000..01ef484a --- /dev/null +++ b/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs @@ -0,0 +1,58 @@ +namespace LearningHub.Nhs.WebUI.ViewComponents +{ + using System.Collections.Generic; + using System.Linq; + using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; + using Microsoft.AspNetCore.Mvc; + + /// + /// Defines the . + /// + public class DynamicCheckboxesViewComponent : ViewComponent + { + /// + /// The Invoke. + /// + /// label. + /// checkboxes. + /// required. + /// errorMessage. + /// hintText. + /// cssClass. + /// selectedValues. + /// propertyName. + /// A representing the result of the synchronous operation. + public IViewComponentResult Invoke( + string label, + IEnumerable checkboxes, + bool required = false, + string? errorMessage = null, + string? hintText = null, + string? cssClass = null, + IEnumerable? selectedValues = null, + string propertyName = "SelectedValues") + { + var selectedList = selectedValues?.ToList() ?? new List(); + + var viewModel = new DynamicCheckboxesViewModel + { + Label = label, + HintText = string.IsNullOrWhiteSpace(hintText) ? null : hintText, + ErrorMessage = errorMessage, + Required = required, + CssClass = string.IsNullOrWhiteSpace(cssClass) ? null : cssClass, + SelectedValues = selectedList, + Checkboxes = checkboxes.Select(cb => new DynamicCheckboxItemViewModel + { + Value = cb.Value, + Label = cb.Label, + HintText = cb.HintText, + Selected = selectedList.Contains(cb.Value), + }).ToList(), + }; + + this.ViewData["PropertyName"] = propertyName; + return this.View(viewModel); + } + } +} diff --git a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml index 1b37e297..af85c380 100644 --- a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml @@ -92,10 +92,13 @@

4.5.22 contain harmful material;

4.5.23 give the impression that the Contribution emanates from us, if this is not the case; or

4.5.24 disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).

-

4.6 You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive, indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.

-

4.7 When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).

+

4.5.25 contain or request any material, the provision of which is not compliant NHS England Information Governance guidance[https://www.england.nhs.uk/ig/ig-resources/].

+

4.6 Posts must be made with consideration of the NHS People Promise[https://www.england.nhs.uk/our-nhs-people/online-version/lfaop/our-nhs-people-promise/].

+

4.7 You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive,indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.

+

4.8 When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).

5 Metadata

When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select).

+

When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources evidence are valid (for example, by peer review).

6 Updates

You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform.

7 Accessibility

@@ -121,13 +124,13 @@

10.4 legal proceedings against you for reimbursement of all costs on an indemnity basis (including, but not limited to, reasonable administrative and legal costs) resulting from the breach, and/or further legal action against you;

10.5 disclosure of such information to law enforcement authorities as we reasonably feel is necessary or as required by law; and/or

10.6 any other action we reasonably deem appropriate.

-

Moderation of Contributions

+

11 Moderation of Contributions

11.1 Course Manager means a person authorised by NHS England who is responsible for creating and managing courses and learning resources on the Learning Hub, including the moderation of learner's Contributions within social learning environments of an online course, such as discussion forums.

11.2 Course Managers shall be responsible for monitoring local forum activity and ensuring compliance of all Contributions with the Learning Hub’s Acceptable Use Policy. Inappropriate Contributions shall be addressed promptly and escalated where necessary to the Learning Hub, who will review the user’s access to the Platform and the relevant forum.

11.3 Course Managers shall also take responsibility for the following:

-

11.3.1 Clear Expectations: Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

-

11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

-

11.3.3 Data Protection and Safety: Ensure that no sensitive or personal data i shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

+

11.3.1Clear Expectations:Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

+

11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

+

11.3.3 Data Protection and Safety:Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

11.4 All users must comply with this Acceptable Use Policy. Breaches may result in removal of Contributions and/or withdrawal or suspension of user access without notice.

diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml new file mode 100644 index 00000000..9677805f --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml @@ -0,0 +1,135 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.Models.Enums.Report +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel + +@{ + ViewData["Title"] = "Course completion report"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + + + var pagingModel = Model.ReportPaging; + int currentPage = pagingModel.CurrentPage; + int pageSize = pagingModel.PageSize; + int totalRows = pagingModel.TotalItems; + + int startRow = (currentPage * pageSize) + 1; + int endRow = Math.Min(startRow + pageSize - 1, totalRows); + var distinctCourses = this.ViewData["matchedCourseNames"] as List; + +} + +@section styles { + +} + +
+
+
+ + +
+ +
+ Home + + Reports +
+ +

Course completion report

+
+ +
+
+ Course@(distinctCourses.Count() > 1 ? "s" : "") +
+
+
    + @foreach (var entry in distinctCourses) + { +
  • @entry
  • + } +
+ +
+ +
+ + Change Course + + +
+ +
+
+
+ Reporting period +
+
+ @{ + if (Model.TimePeriod == "Custom" && Model.StartDate.HasValue && Model.EndDate.HasValue) + { + @Model.StartDate.Value.ToString("dd MMMM yyyy") to @Model.EndDate.Value.ToString("dd MMMM yyyy") + } + else + { + @Model.TimePeriod days + } + } + +
+ +
+ + Change Reporting period + + +
+ +
+
+ +
+

Displaying @startRow–@endRow of @Model.TotalCount filtered row@(Model.TotalCount > 1 ? "s" : "")

+ + @if (Model.TotalCount != 0) + { +

+ Request to download this report in a spreadsheet (.xls) format.You will be notified + when the report is ready. +

+ @if (Model.ReportHistoryModel != null && Model.ReportHistoryModel.DownloadRequest == null) + { +
+ + +
+ } + else if (Model.ReportHistoryModel != null && Model.ReportHistoryModel.DownloadRequest == true && Model.ReportHistoryModel.ReportStatusId == ((int)Status.Pending)) + { +
+ Information: +

We’re getting your report ready

+

+ You will be notified once it’s ready to download and you will be able to access it in the + Reports section. +

+
+ } + } + + @if (Model.CourseCompletionRecords.Any()) + { + @await Html.PartialAsync("_ReportTable", Model) + } + +
+
+
+ @await Html.PartialAsync("_ReportPaging", Model) +
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml new file mode 100644 index 00000000..f6a7e947 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml @@ -0,0 +1,62 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@using NHSUKViewComponents.Web.ViewModels +@model LearningHub.Nhs.WebUI.Models.Report.ReportCreationCourseSelection; +@{ + ViewData["Title"] = "Select Course"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var errorHasOccurred = !ViewData.ModelState.IsValid; + var routeData = new Dictionary { { "ReturnToConfirmation", Context.Request.Query["returnToConfirmation"] } }; +} + +@section styles { + +} + +
+
+
+ + +
+ +
+ Create a course completion report +
+

+ Select course(s) +

+
+ + @if (errorHasOccurred) + { + + } + +
+
+ @await Component.InvokeAsync("DynamicCheckboxes", new + { + label = "", + checkboxes = Model.AllCources, + required = false, + errorMessage = "Please select at least one course", + selectedValues = Model.Courses, + propertyName = nameof(Model.Courses) + }) +
+
+ + +
+
+ +
+ + +
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml new file mode 100644 index 00000000..745e1e0d --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml @@ -0,0 +1,132 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@using NHSUKViewComponents.Web.ViewModels +@model LearningHub.Nhs.WebUI.Models.Report.ReportCreationDateSelection; +@{ + ViewData["Title"] = "Select Date"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var errorHasOccurred = !ViewData.ModelState.IsValid; + var customId = $"TimePeriod-{Model.PopulateDateRange().Count()}"; + var hintTextLines = new List { $"For example, {Model.StartDay} {Model.StartMonth} {Model.StartYear}" }; + var endHintTextLines = new List { $" " }; +} + +@section styles { + +} + +
+
+
+ + +
+ +
+ Create a course completion report +
+

+ Reporting Period +

+
+ + @if (errorHasOccurred) + { + + } + +
+ +
+
+ +

+ +

+
+
+ For the last: +
+
+ @foreach (var (radio, index) in Model.PopulateDateRange().Select((r, i) => (r, i))) + { + var radioId = $"TimePeriod-{index}"; +
+ + + @if (radio.HintText != null) + { +
+ @radio.HintText +
+ } +
+ + } + +
or
+ +
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ + +
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml new file mode 100644 index 00000000..9145e6be --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -0,0 +1,184 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.Models.Enums.Report +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@model LearningHub.Nhs.WebUI.Models.Report.ReportHistoryViewModel +; +@{ + ViewData["Title"] = "Reports"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var allCourses = this.ViewData["AllCourses"] as List>; +} + +@section styles { + +} + +@section NavBreadcrumbs { + +
+
+
+
+ +
+ Home + +
+ +
+

Reports

+

View and manage your reports

+
+ +
+
+
+
+} +
+
+
+ + +
+
+

+ This page lists all reports you can access or have created. Use the Create a course completion report button to generate a new report. +

+ Create a course completion report +
+ +
+

Previously run reports

+

+ Reports are stored for 30 days from the date they’re generated. If you need to keep a copy, make sure you download it before it expires. +

+
+ @if (Model.ReportHistoryModels.Any()) + { + @foreach (var entry in Model.ReportHistoryModels) + { + var matchedCourseNames = new List(); + if (string.IsNullOrWhiteSpace(entry.CourseFilter)) + { + matchedCourseNames = allCourses.Select(course => course.Value).ToList(); + } + else + { + matchedCourseNames = allCourses + .Where(course => entry.CourseFilter.Contains(course.Key)) + .Select(course => course.Value) + .ToList(); + } + string datePeriod = entry.PeriodDays > 0 ? $"{entry.PeriodDays} days" : $"{entry.StartDate.GetValueOrDefault().ToString("dd MMM yyyy")} to {entry.EndDate.GetValueOrDefault().ToString("dd MMM yyyy")}"; + bool downloadCheck = entry.DownloadRequest != null && (bool)entry.DownloadRequest; + string expiryDate = entry.LastRun.AddDays(30).ToString("dd MMM yyyy"); + +
+ + + Course completion for @matchedCourseNames.FirstOrDefault()?.Normalize() + + @if (downloadCheck) + { + + @if (entry.ReportStatusId == ((int)Status.Ready) && DateTime.Now.Date < entry.LastRun.AddDays(30).Date) + { + + + Expires on @expiryDate + + + Ready to download + } + else if (entry.ReportStatusId == ((int)Status.Pending)) + { + Getting it ready + } + else + { + Expired + } + + } + +
+
+
+
Date requested:
+
@entry.FirstRun.Date.ToString("dd MMM yyyy")
+
+
+
Date period:
+
@datePeriod
+
+
+
Type:
+
Course completions
+
+
+
Reporting on:
+
+
    + @foreach(var item in matchedCourseNames) + { +
  • @item
  • + } +
+
+
+
+ +
    +
  • + + View report + +
  • + + @if (downloadCheck) + { + @if (entry.ReportStatusId == ((int)Status.Ready) && DateTime.Now.Date < entry.LastRun.AddDays(30).Date) + { +
  • + Download report +
  • + } + } +
+
+
+
+ } + + } + else + { +
    +
  • +
    +
    +

    + No reports available yet +

    +

    + You haven’t run any reports yet, so this section is currently empty. + Once you generate a report, it will appear here for you to view or download. +

    +
    +
    +
  • +
+ + } + +
+
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml new file mode 100644 index 00000000..5fe25a28 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml @@ -0,0 +1,76 @@ +@using System.Web; +@using LearningHub.Nhs.WebUI.Models.Learning +@using LearningHub.Nhs.WebUI.Models.Search; +@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel + + +@{ + var pagingModel = Model.ReportPaging; + var showPaging = pagingModel.CurrentPage >= 0 && pagingModel.CurrentPage <= pagingModel.TotalPages - 1; + var previousMessage = $"{pagingModel.CurrentPage} of {pagingModel.TotalPages}"; + int CurrentPageNumber = pagingModel.CurrentPage + 1; + var nextMessage = string.Empty; + if (CurrentPageNumber <= pagingModel.TotalPages) + { + nextMessage = $"{CurrentPageNumber + 1} of {pagingModel.TotalPages}"; + } + else + { + previousMessage = $"{CurrentPageNumber - 1} of {pagingModel.TotalPages}"; + nextMessage = $"{CurrentPageNumber} of {pagingModel.TotalPages}"; + } + + var routeData = new Dictionary(); + routeData["CurrentPageIndex"] = pagingModel.CurrentPage.ToString(); + var nextRouteData = new Dictionary(routeData); + var previousRouteData = new Dictionary(routeData); + nextRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.NextPageChange.ToString(); + previousRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.PreviousPageChange.ToString(); +} + +@if (pagingModel.TotalPages > 1) +{ + +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml new file mode 100644 index 00000000..492abdc0 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml @@ -0,0 +1,145 @@ +@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel + +@{ + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; +} + + +@if (Model.TotalCount > 0) +{ +
+ + + + + + + + + + + + + + + + + + @foreach (var entry in Model.CourseCompletionRecords) + { + + + + + + + + + + + + + + + + } + + +
+ Username + + First Name + + Last Name + + Email Address + + Medical Council No + + Medical Council Name + + Role + + Grade + + Location + + Programme Name + + Course Learning Path Name +
+ Username + + @entry.UserName + + + First Name + @entry.FirstName + + + Last Name + @entry.LastName + + + Email Address + @entry.Email + + + Medical Council No + @entry.MedicalCouncilNo + + + Medical Council Name + @entry.MedicalCouncilName + + + Role + @entry.Role + + + Grade + @entry.Grade + + + Location + @entry.Location + + + Programme Name + @entry.Programme + + + Course Learning Path Name + @entry.Course + +
+
+} diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml new file mode 100644 index 00000000..8099618a --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml @@ -0,0 +1,34 @@ +@using LearningHub.Nhs.WebUI.Models.DynamicCheckbox +@model DynamicCheckboxesViewModel +@{ + var propertyName = ViewData["PropertyName"]?.ToString() ?? "SelectedValues"; +} + +
+
+ + @Model.Label + + +
+ @for (int i = 0; i < Model.Checkboxes.Count; i++) + { + var checkbox = Model.Checkboxes[i]; + var inputId = $"{propertyName}_{i}"; + +
+ + + +
+ } +
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml index 43881e47..d98f7867 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml @@ -50,7 +50,15 @@ } + @if (Model.ShowReports) + { +
  • + + Reports + +
  • + } @if (Model.ShowMyBookmarks) {
  • @@ -61,6 +69,7 @@
  • } + @if (Context.Request.Path.Value != "/Home/Error" && !SystemOffline()) { @if (Model.ShowHelp) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs new file mode 100644 index 00000000..d33e0d71 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs @@ -0,0 +1,60 @@ +namespace LearningHub.Nhs.OpenApi.Models.Configuration +{ + /// + /// DatabricksConfig + /// + public class DatabricksConfig + { + /// + /// Gets or sets the ResourceId for the databricks instance. + /// + public string ResourceId { get; set; } = null!; + + /// + /// Gets or sets the base url for the databricks instance. + /// + public string InstanceUrl { get; set; } = null!; + + /// + /// Gets or sets the warehouse id for databricks. + /// + public string WarehouseId { get; set; } = null!; + + /// + /// Gets or sets the job id for databricks. + /// + public string JobId { get; set; } = null!; + + /// + /// Gets or sets the tenant Id of the service pricncipl. + /// + public string TenantId { get; set; } = null!; + + /// + /// Gets or sets the client Id of the service pricncipl. + /// + public string ClientId { get; set; } = null!; + + /// + /// Gets or sets the client scret of the service pricncipl. + /// + public string clientSecret { get; set; } = null!; + + /// + /// Gets or sets the endpoint to check user permission. + /// + public string UserPermissionEndpoint { get; set; } = null!; + + + /// + /// Gets or sets the endpoint for course completion record. + /// + public string CourseCompletionEndpoint { get; set; } = null!; + + /// + /// Gets or sets the token. + /// + public string Token { get; set; } = null!; + + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs index 81b91aa6..9815f482 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs @@ -67,6 +67,11 @@ public class LearningHubConfig ///
    public string ContentManagementQueueName { get; set; } = null!; + /// + /// Gets or sets . + /// + public string DatabricksProcessingQueueName { get; set; } = null!; + /// /// Gets or sets . /// @@ -142,6 +147,15 @@ public class LearningHubConfig /// public string BrowseCataloguesUrl { get; set; } = null!; + /// + /// Gets or sets . + /// + public string ReportUrl { get; set; } = null!; + + /// + /// Gets or sets the StatMandId. + /// + public int StatMandId { get; set; } /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs index 7b9b7b5b..a3ea9a1b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs @@ -55,5 +55,15 @@ public class NotificationSetting /// Gets or sets the ResourceContributeAccess. /// public string ResourceContributeAccess { get; set; } = null!; + + /// + /// Gets or sets the report title notification content. + /// + public string ReportTitle { get; set; } = null!; + + /// + /// Gets or sets the report notification content. + /// + public string Report { get; set; } = null!; } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj index cecf1b22..2991f677 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -16,7 +16,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/DatabricksNotification.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/DatabricksNotification.cs new file mode 100644 index 00000000..5ce62c6e --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/DatabricksNotification.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace LearningHub.Nhs.OpenApi.Models.ViewModels +{ + /// + /// DatabricksNotification + /// + public class DatabricksNotification + { + /// + /// Gets or sets . + /// + [JsonProperty("event_type")] + public string EventType { get; set; } + + /// + /// Gets or sets . + /// + [JsonProperty("run")] + public RunInfo Run { get; set; } + + /// + /// RunInfo + /// + public class RunInfo + { + /// + /// Gets or sets . + /// + [JsonProperty("run_id")] + public long RunId { get; set; } + + /// + /// Gets or sets . + /// + [JsonProperty("parent_run_id")] + public long ParentRunId { get; set; } + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs index 912d99ca..c7ccc786 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs @@ -75,6 +75,11 @@ public class NavigationModel /// public bool ShowBrowseCatalogues { get; set; } + /// + /// Gets or sets a value indicating whether to show reports. + /// + public bool ShowReports { get; set; } + /// /// Gets or sets a value indicating whether to show home. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index a52fd6bf..e7e92397 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs new file mode 100644 index 00000000..b2aabcb9 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs @@ -0,0 +1,28 @@ +namespace LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities; + using LearningHub.Nhs.Models.Entities.DatabricksReport; + + /// + /// The ProviderRepository interface. + /// + public interface IReportHistoryRepository : IGenericRepository + { + /// + /// The get by id async. + /// + /// The id. + /// The . + Task GetByIdAsync(int id); + + /// + /// The get by user id async. + /// + /// The userId. + /// The . + IQueryable GetByUserIdAsync(int userId); + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs index 1b8fe878..80ec18a5 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs @@ -6,6 +6,7 @@ namespace LearningHub.Nhs.OpenApi.Repositories.EntityFramework using LearningHub.Nhs.Models.Entities; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Content; + using LearningHub.Nhs.Models.Entities.DatabricksReport; using LearningHub.Nhs.Models.Entities.External; using LearningHub.Nhs.Models.Entities.Hierarchy; using LearningHub.Nhs.Models.Entities.Messaging; @@ -750,6 +751,11 @@ public LearningHubDbContextOptions Options /// public virtual DbSet UserProvider { get; set; } + /// + /// Gets or sets Report History. + /// + public virtual DbSet ReportHistory { get; set; } + /// /// Gets or sets Resource Version Provider. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs index a571d177..197f95dc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs @@ -184,6 +184,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // External services.AddSingleton(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index 46722121..0ee6fa49 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,7 +24,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs new file mode 100644 index 00000000..3e398744 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs @@ -0,0 +1,23 @@ +using LearningHub.Nhs.Models.Entities.DatabricksReport; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LearningHub.Nhs.OpenApi.Repositories.Map +{ + /// + /// The ReportHistory Map. + /// + public class ReportHistoryMap : BaseEntityMap + { + /// + /// The internal map. + /// + /// + /// The model builder. + /// + protected override void InternalMap(EntityTypeBuilder modelBuilder) + { + modelBuilder.ToTable("ReportHistory", "Reports"); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs new file mode 100644 index 00000000..32bfd045 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs @@ -0,0 +1,57 @@ +namespace LearningHub.Nhs.OpenApi.Repositories.Repositories +{ + using System.Collections.Generic; + using System.Data; + using System.Linq; + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities; + using LearningHub.Nhs.Models.Entities.DatabricksReport; + using LearningHub.Nhs.Models.Entities.Hierarchy; + using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.OpenApi.Repositories.EntityFramework; + using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; + using Microsoft.EntityFrameworkCore; + + /// + /// The provider repository. + /// + public class ReportHistoryRepository : GenericRepository, IReportHistoryRepository + { + /// + /// Initializes a new instance of the class. + /// + /// The db context. + /// The Timezone offset manager. + public ReportHistoryRepository(LearningHubDbContext dbContext, ITimezoneOffsetManager tzOffsetManager) + : base(dbContext, tzOffsetManager) + { + } + + /// + public async Task GetByIdAsync(int id) + { + return await DbContext.ReportHistory.AsNoTracking().FirstOrDefaultAsync(n => n.Id == id && !n.Deleted); + } + + /// + public IQueryable GetByUserIdAsync(int userId) + { + return DbContext.ReportHistory.AsNoTracking().Where(n => n.CreateUserId == userId && !n.Deleted); + } + + /// + /// The get by user id async. + /// + /// The user id. + /// The . + public IQueryable GetProvidersByUserIdAsync(int userId) + { + return DbContext.Set() + .Include(up => up.Provider) + .Where(up => up.UserId == userId && !up.Deleted).AsNoTracking() + .Select(up => up.Provider); + } + + + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs index 767d35d1..37d56292 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs @@ -56,6 +56,7 @@ private static void AddRepositoryImplementations(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs new file mode 100644 index 00000000..f4226cd8 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs @@ -0,0 +1,26 @@ +namespace LearningHub.Nhs.OpenApi.Services.Interface.HttpClients +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + + /// + /// The Bookmark Http Client interface. + /// + public interface IDatabricksApiHttpClient : IDisposable + { + /// + /// GETs data from Databricks API. + /// + /// The URL to make a get call to. + /// Optional authorization header. + /// A representing the result of the asynchronous operation. + Task GetData(string requestUrl, string? authHeader); + + /// + /// The Get Client method. + /// + /// The . + HttpClient GetClient(); + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj index f6e92a8c..99e4fe96 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs new file mode 100644 index 00000000..10e0f358 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs @@ -0,0 +1,77 @@ +using LearningHub.Nhs.Models.Common; +using LearningHub.Nhs.Models.Databricks; +using LearningHub.Nhs.OpenApi.Models.ViewModels; +using System.Threading.Tasks; + +namespace LearningHub.Nhs.OpenApi.Services.Interface.Services +{ + /// + /// IDatabricks service + /// + public interface IDatabricksService + { + /// + /// IsUserReporter. + /// + /// The userId. + /// A representing the result of the asynchronous operation. + Task IsUserReporter(int userId); + + /// + /// CourseCompletionReport. + /// + /// The userId. + /// The model. + /// A representing the result of the asynchronous operation. + Task CourseCompletionReport(int userId, DatabricksRequestModel model); + + /// + /// CourseCompletionReport. + /// + /// The userId. + /// The page. + /// The pageSize. + /// A representing the result of the asynchronous operation. + Task> GetPagedReportHistory(int userId, int page, int pageSize); + + /// + /// GetPagedReportHistoryById. + /// + /// The userId. + /// The reportHistoryId. + /// A representing the result of the asynchronous operation. + Task GetPagedReportHistoryById(int userId, int reportHistoryId); + + /// + /// QueueReportDownload + /// + /// + /// + /// + Task QueueReportDownload(int userId, int reportHistoryId); + + /// + /// DownloadReport + /// + /// + /// + /// + Task DownloadReport(int userId, int reportHistoryId); + + /// + /// DatabricksJobUpdate. + /// + /// + /// + /// + Task DatabricksJobUpdate(int userId, DatabricksNotification databricksNotification); + + /// + /// DatabricksJobUpdate. + /// + /// userId. + /// databricksUpdateRequest. + /// + Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest databricksUpdateRequest); + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs index 0a0af21e..217a7722 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs @@ -69,5 +69,14 @@ public interface INotificationService /// Error message. /// The . Task CreatePublishFailedNotificationAsync(int userId, string resourceTitle, string errorMessage = ""); + + /// + /// Creates report processed notification. + /// + /// The current user id. + /// Report Name. + /// Report Content. + /// The . + Task CreateReportNotificationAsync(int userId, string reportName, string reportContent); } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs new file mode 100644 index 00000000..b5452240 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs @@ -0,0 +1,63 @@ +namespace LearningHub.Nhs.OpenApi.Services.HttpClients +{ + using System; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading.Tasks; + using IdentityModel.Client; + using LearningHub.Nhs.OpenApi.Models.Configuration; + using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; + using Microsoft.Extensions.Options; + using Microsoft.IdentityModel.Protocols.OpenIdConnect; + + /// + /// Http client for Databricks. + /// + public class DatabricksApiHttpClient : IDatabricksApiHttpClient + { + private readonly HttpClient httpClient; + private readonly IOptions databricksConfig; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration details for the databricks. + public DatabricksApiHttpClient(IOptions databricksConfig) + { + this.databricksConfig = databricksConfig; + this.httpClient = new HttpClient { BaseAddress = new Uri(databricksConfig.Value.InstanceUrl) }; + this.httpClient.DefaultRequestHeaders.Accept.Clear(); + this.httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + } + + /// + public void Dispose() + { + this.httpClient.Dispose(); + } + + /// + /// The Get Client method. + /// + /// The . + public HttpClient GetClient() + { + string accessToken = this.databricksConfig.Value.Token; + this.httpClient.SetBearerToken(accessToken); + return this.httpClient; + } + + /// + public async Task GetData(string requestUrl, string? authHeader) + { + if (!string.IsNullOrEmpty(authHeader)) + { + this.httpClient.SetBearerToken(authHeader); + } + + var message = await this.httpClient.GetAsync(requestUrl).ConfigureAwait(false); + return message; + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj index e2e06473..7c534adf 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -30,7 +30,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs new file mode 100644 index 00000000..88705817 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -0,0 +1,483 @@ +using LearningHub.Nhs.Models.Bookmark; +using LearningHub.Nhs.Models.Entities.Reporting; +using LearningHub.Nhs.OpenApi.Models.Configuration; +using LearningHub.Nhs.OpenApi.Services.HttpClients; +using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; +using LearningHub.Nhs.OpenApi.Services.Interface.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Text; +using System.Threading.Tasks; +using LearningHub.Nhs.Models.Databricks; +using System.Linq; +using System.Net.Http.Headers; +using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; +using LearningHub.Nhs.Models.Entities.DatabricksReport; +using AutoMapper; +using LearningHub.Nhs.Models.Entities.Activity; +using LearningHub.Nhs.Models.Resource.Activity; +using LearningHub.Nhs.Models.Common; +using LearningHub.Nhs.Models.Notification; +using Microsoft.EntityFrameworkCore; +using LearningHub.Nhs.Models.Enums.Report; +using Newtonsoft.Json.Linq; +using LearningHub.Nhs.OpenApi.Models.ViewModels; +using LearningHub.Nhs.Models.Resource; +using LearningHub.Nhs.OpenApi.Repositories.Repositories; +using LearningHub.Nhs.Models.Constants; +using LearningHub.Nhs.Models.Hierarchy; +using LearningHub.Nhs.Models.Enums; +using System.Text.Json; +using LearningHub.Nhs.Models.Entities.Resource; +using LearningHub.Nhs.Models.Entities; + +namespace LearningHub.Nhs.OpenApi.Services.Services +{ + /// + /// DatabricksService + /// + public class DatabricksService : IDatabricksService + { + private const string CacheKey = "DatabricksReporter"; + private readonly IOptions databricksConfig; + private readonly IOptions learningHubConfig; + private readonly IReportHistoryRepository reportHistoryRepository; + private readonly IQueueCommunicatorService queueCommunicatorService; + private readonly ICachingService cachingService; + private readonly INotificationService notificationService; + private readonly IUserNotificationService userNotificationService; + private readonly IMoodleApiService moodleApiService; + private readonly IMapper mapper; + + /// + /// Initializes a new instance of the class. + /// + /// databricksConfig. + /// learningHubConfig. + /// reportHistoryRepository. + /// mapper. + /// queueCommunicatorService. + /// cachingService. + /// notificationService. + /// userNotificationService. + /// moodleApiService. + public DatabricksService(IOptions databricksConfig,IOptions learningHubConfig, IReportHistoryRepository reportHistoryRepository, IMapper mapper, IQueueCommunicatorService queueCommunicatorService, ICachingService cachingService, INotificationService notificationService, IUserNotificationService userNotificationService, IMoodleApiService moodleApiService) + { + this.databricksConfig = databricksConfig; + this.learningHubConfig = learningHubConfig; + this.reportHistoryRepository = reportHistoryRepository; + this.mapper = mapper; + this.queueCommunicatorService = queueCommunicatorService; + this.cachingService = cachingService; + this.notificationService = notificationService; + this.userNotificationService = userNotificationService; + this.moodleApiService = moodleApiService; + } + + /// + public async Task IsUserReporter(int userId) + { + string cacheKey = $"{CacheKey}_{userId}"; + var userReportPermission = await this.cachingService.GetAsync(cacheKey); + if (userReportPermission.ResponseEnum == CacheReadResponseEnum.Found) + { + return userReportPermission.Item; + } + + + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); + + var sqlText = $"CALL {this.databricksConfig.Value.UserPermissionEndpoint}({userId});"; + const string requestUrl = "/api/2.0/sql/statements"; + + var requestPayload = new + { + warehouse_id = this.databricksConfig.Value.WarehouseId, + statement = sqlText, + wait_timeout = "30s", + on_wait_timeout = "CANCEL" + }; + + var jsonBody = JsonConvert.SerializeObject(requestPayload); + using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + var response = await databricksInstance.GetClient().PostAsync(requestUrl, content); + + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + //log failure + return false; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + + responseResult = responseResult.Trim(); + var root = JsonDocument.Parse(responseResult).RootElement; + string data = root.GetProperty("result").GetProperty("data_array")[0][0].GetString(); + bool isReporter = data == "1"; + + await this.cachingService.SetAsync(cacheKey, isReporter); + return isReporter; + } + + /// + public async Task CourseCompletionReport(int userId, DatabricksRequestModel model) + { + userId = 22527; + newEntry: + if (model.ReportHistoryId == 0 && model.Take > 1) + { + + bool timePeriodCheck = int.TryParse(model.TimePeriod, out int timePeriod); + var reportHistory = new ReportHistory { CourseFilter = string.Join(",", model.Courses) ,StartDate = model.StartDate,EndDate =model.EndDate, PeriodDays = timePeriodCheck ? timePeriod : 0 , + FirstRun = DateTimeOffset.Now, LastRun = DateTimeOffset.Now, ReportStatusId = 2}; + model.ReportHistoryId = await AddReportHistory(userId, reportHistory); + } + else if(model.ReportHistoryId > 0 && model.Take > 1) + { + //get the existing values and compare + var reportChecker = await GetPagedReportHistoryById(userId, model.ReportHistoryId); + if (reportChecker != null) + { + if(reportChecker.CourseFilter == "all") { reportChecker.CourseFilter = string.Empty; } + if(reportChecker.CourseFilter != string.Join(",", model.Courses) || reportChecker.StartDate.GetValueOrDefault().Date != model.StartDate.GetValueOrDefault().Date || reportChecker.EndDate.GetValueOrDefault().Date != model.EndDate.GetValueOrDefault().Date) + { + model.ReportHistoryId = 0; + goto newEntry; + } + } + await UpdateReportLastRunTime(userId, model.ReportHistoryId); + } + + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); + + const string requestUrl = "/api/2.0/sql/statements"; + + var sql = $@"CALL {this.databricksConfig.Value.CourseCompletionEndpoint}(:par_adminId, :par_completionFlag, :par_locationId, :par_catalogueId, :par_learnerId, :par_courseId, :par_PageSize, :par_PageNumber, :par_Date_from, :par_Date_to);"; + + + var parameters = new List> + { + new("par_adminId", userId), + new("par_completionFlag", -1), + new("par_locationId", -1), + new("par_catalogueId", -1), + new("par_learnerId", -1), + new("par_courseId", model.Courses.Count < 1 ? string.Empty : string.Join(",", model.Courses)), + new("par_PageSize", model.Take), + new("par_PageNumber", model.Skip), + new("par_Date_from", model.StartDate.HasValue ? model.StartDate.Value.ToString("yyyy-MM-dd"): string.Empty), + new("par_Date_to", model.EndDate.HasValue ? model.EndDate.Value.ToString("yyyy-MM-dd"): string.Empty), + }; + + var formattedParams = parameters.Select(p => new { name = p.Key, value = p.Value }); + + var body = new + { + warehouse_id = this.databricksConfig.Value.WarehouseId, + statement = sql, + parameters = formattedParams, + wait_timeout = "30s", + on_wait_timeout = "CANCEL" + }; + + var json = JsonConvert.SerializeObject(body); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await databricksInstance.GetClient().PostAsync(requestUrl, content); + + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + //log failure + return new DatabricksDetailedViewModel { ReportHistoryId = model.ReportHistoryId }; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(responseResult); + if (result != null && result.Result.DataArray != null) + { + var records = MapDataArrayToCourseCompletionRecords(result.Result.DataArray); + return new DatabricksDetailedViewModel { CourseCompletionRecords = records, ReportHistoryId = model.ReportHistoryId }; + + } + + return new DatabricksDetailedViewModel { CourseCompletionRecords= new List(), ReportHistoryId = model.ReportHistoryId }; + } + + /// + public async Task> GetPagedReportHistory(int userId,int page, int pageSize) + { + userId = 22527; + var result = new PagedResultSet(); + var query = this.reportHistoryRepository.GetByUserIdAsync(userId); + + // Execute async count + result.TotalItemCount = await query.CountAsync(); + try + { + // Execute async paging + var pagedItems = await query + .OrderByDescending(x => x.LastRun) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + result.Items = mapper.Map>(pagedItems); + } + catch(Exception e) + { + + } + + return result; + } + + /// + public async Task GetPagedReportHistoryById(int userId, int reportHistoryId) + { + userId = 22527; + var result = new ReportHistoryModel(); + + var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); + if(reportHistory != null) + { + if(reportHistory.CreateUserId != userId) + { + throw new Exception("Invalid Id"); + } + } + result = mapper.Map(reportHistory); + if(result != null && string.IsNullOrWhiteSpace(result.CourseFilter)) + { + result.CourseFilter = "all"; + } + + return result; + + } + + /// + public async Task QueueReportDownload(int userId, int reportHistoryId) + { + userId = 22527; + var result = new ReportHistoryModel(); + + var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); + if (reportHistory != null && reportHistory.DownloadRequest == null) + { + if (reportHistory.CreateUserId != userId) + { + throw new Exception("Invalid Id"); + } + reportHistory.DownloadRequest = true; + reportHistory.DownloadRequested = DateTimeOffset.Now; + } + else + { + throw new Exception("Invalid Id"); + } + //call the job + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); + + const string requestUrl = "/api/2.1/jobs/run-now"; + + var body = new + { + job_id = this.databricksConfig.Value.JobId, + notebook_params = new + { + par_adminId = userId, + par_completionFlag = -1, + par_locationId = -1, + par_catalogueId = -1, + par_learnerId = -1, + par_courseId = reportHistory.CourseFilter, + par_PageSize = 0, + par_PageNumber = 0, + par_Date_from = reportHistory.StartDate.GetValueOrDefault().ToString("yyyy-MM-dd"), + par_Date_to = reportHistory.EndDate.GetValueOrDefault().ToString("yyyy-MM-dd") + } + }; + + var json = JsonConvert.SerializeObject(body); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + reportHistory.ProcessingMessage = databricksResponse.ReasonPhrase; + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Failed; + await reportHistoryRepository.UpdateAsync(userId, reportHistory); + return false; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + var responseData = JsonConvert.DeserializeObject(responseResult); + if (responseData != null) + { + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Pending; + reportHistory.ParentJobRunId = (long)responseData.run_id; + await reportHistoryRepository.UpdateAsync(userId, reportHistory); + return true; + } + return false; + } + + /// + public async Task DownloadReport(int userId, int reportHistoryId) + { + userId = 22527; + var response = new ReportHistoryModel(); + + var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); + if (reportHistory != null) + { + if (reportHistory.CreateUserId != userId) + { + throw new Exception("Invalid Id"); + } + reportHistory.DownloadedDate = DateTimeOffset.Now; + await reportHistoryRepository.UpdateAsync(userId, reportHistory); + response = mapper.Map(reportHistory); + } + else + { + throw new Exception("Invalid Id"); + } + + return response; + } + + /// + /// DatabricksJobUpdate. + /// + /// + /// + /// + public async Task DatabricksJobUpdate(int userId, DatabricksNotification databricksNotification) + { + var reportHistory = await this.reportHistoryRepository.GetAll().FirstOrDefaultAsync(x=>x.ParentJobRunId == databricksNotification.Run.ParentRunId); + if (reportHistory == null) { return; } + reportHistory.JobRunId = databricksNotification.Run.RunId; + if (!databricksNotification.EventType.Contains("success")) + { + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Failed; + reportHistoryRepository.Update(userId, reportHistory); + return; + } + reportHistoryRepository.Update(userId, reportHistory); + + await this.queueCommunicatorService.SendAsync(this.learningHubConfig.Value.DatabricksProcessingQueueName, databricksNotification.Run.RunId); + return; + } + + + /// + /// DatabricksJobUpdate. + /// + /// userId. + /// databricksUpdateRequest. + /// + public async Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest databricksUpdateRequest) + { + var reportHistory = await this.reportHistoryRepository.GetAll().FirstOrDefaultAsync(x => x.JobRunId == databricksUpdateRequest.RunId); + if (reportHistory == null) { return; } + if(string.IsNullOrWhiteSpace(databricksUpdateRequest.ProcessingMessage)) + { + reportHistory.DownloadReady = DateTimeOffset.Now; + reportHistory.FilePath = databricksUpdateRequest.FilePath; + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Ready; + //send notification + string firstCourse = string.Empty; + + var courses = await moodleApiService.GetCoursesByCategoryIdAsync(learningHubConfig.Value.StatMandId); + + firstCourse = string.IsNullOrWhiteSpace(reportHistory.CourseFilter) + ? courses.Courses.Select(c => c.Displayname).FirstOrDefault() + : courses.Courses + .Where(c => reportHistory.CourseFilter.Contains(c.Id.ToString())) + .Select(c => c.Displayname) + .FirstOrDefault(); + + + var notificationId = await this.notificationService.CreateReportNotificationAsync(userId, "Course Completion", firstCourse); + + if (notificationId > 0) + { + await this.userNotificationService.CreateAsync(userId, new UserNotification { UserId = reportHistory.CreateUserId, NotificationId = notificationId }); + } + } + else + { + reportHistory.ProcessingMessage = databricksUpdateRequest.ProcessingMessage; + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Failed; + } + + reportHistoryRepository.Update(userId, reportHistory); + return; + } + + + private async Task AddReportHistory(int userId,ReportHistory model) + { + return await reportHistoryRepository.CreateAsync(userId, model); + } + + private async Task UpdateReportLastRunTime(int userId, int reportHistoryId) + { + var entry = await reportHistoryRepository.GetByIdAsync(reportHistoryId); + entry.LastRun = DateTime.Now; + await reportHistoryRepository.UpdateAsync(userId, entry); + } + + /// + /// MapDataArrayToCourseCompletionRecords. + /// + /// + /// + public static List MapDataArrayToCourseCompletionRecords(List> dataArray) + { + var records = new List(); + + foreach (var row in dataArray) + { + if (row == null || row.Count < 19) continue; + + var record = new DatabricksDetailedItemViewModel + { + UserName = row[0]?.ToString(), + FirstName = row[1]?.ToString(), + LastName = row[2]?.ToString(), + Email = row[3]?.ToString(), + Programme = row[4]?.ToString(), + Course = row[5]?.ToString(), + CourseStatus = row[6]?.ToString(), + Location = row[7]?.ToString(), + Role = row[8]?.ToString(), + Grade = row[9]?.ToString(), + MedicalCouncilNo = row[10]?.ToString(), + MedicalCouncilName = row[11]?.ToString(), + LastAccess = row[12]?.ToString(), + CourseCompletionDate = row[13]?.ToString(), + ReferenceType = row[14]?.ToString(), + ReferenceValue = row[15]?.ToString(), + PermissionType = row[16]?.ToString(), + MinValidDate = row[17]?.ToString(), + TotalRows = row[18] != null && int.TryParse(row[18].ToString(), out int totalRows) ? totalRows : 0 + }; + + records.Add(record); + } + + return records; + } + + } + +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs index a8c6ceb7..9e28c504 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs @@ -13,16 +13,19 @@ public class NavigationPermissionService : INavigationPermissionService { private readonly IResourceService resourceService; private readonly IUserGroupService userGroupService; + private readonly IDatabricksService databricksService; /// /// Initializes a new instance of the class. /// /// Resource service. /// userGroup service. - public NavigationPermissionService(IResourceService resourceService, IUserGroupService userGroupService) + /// databricksService. + public NavigationPermissionService(IResourceService resourceService, IUserGroupService userGroupService, IDatabricksService databricksService) { this.resourceService = resourceService; this.userGroupService = userGroupService; + this.databricksService = databricksService; } /// @@ -86,6 +89,7 @@ public NavigationModel NotAuthenticated() ShowSignOut = false, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -111,6 +115,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = true, }; } @@ -137,6 +142,7 @@ private async Task AuthenticatedBlueUser(string controllerName, ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = await this.databricksService.IsUserReporter(userId), }; } @@ -161,6 +167,7 @@ private NavigationModel AuthenticatedGuest() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -186,6 +193,7 @@ private async Task AuthenticatedReadOnly(string controllerName, ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = true, + ShowReports = await this.databricksService.IsUserReporter(userId), }; } @@ -210,6 +218,7 @@ private async Task AuthenticatedBasicUserOnly(int userId) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = false, }; } @@ -234,6 +243,7 @@ private NavigationModel InLoginWizard() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs index fe002695..e001da09 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs @@ -195,6 +195,34 @@ public async Task CreateResourcePublishedNotificationAsync(int userId, stri } } + /// + /// Creates report processed notification. + /// + /// The current user id. + /// Report Name. + /// Report Content. + /// The . + public async Task CreateReportNotificationAsync(int userId, string reportName, string reportContent) + { + var title = learningHubConfig.Notifications.ReportTitle.Replace("[ReportName]", reportName).Replace("[ReportContent]", reportContent); + + var message = learningHubConfig.Notifications.Report.Replace("[ReportName]", reportName).Replace("[ReportContent]", reportContent); + + + var notification = await this.CreateAsync(userId, this.UserSpecificNotification( + title, message, NotificationTypeEnum.ReportProcessed, NotificationPriorityEnum.General)); + + if (notification.CreatedId.HasValue) + { + return notification.CreatedId.Value; + } + else + { + return 0; + } + } + + /// /// Creates resource publish failed notification. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs index 87c94f64..870ccba0 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs @@ -24,6 +24,7 @@ public static void AddServices(this IServiceCollection services) { services.AddScoped(); services.AddHttpClient(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -36,6 +37,7 @@ public static void AddServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index 35c78091..3ecb4d1a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs index 7d5a6179..1c5a692e 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs @@ -45,6 +45,11 @@ public static class ConfigurationExtensions /// public const string MoodleSectionName = "Moodle"; + /// + /// The DatabricksSectionName. + /// + public const string DatabricksSectionName = "Databricks"; + /// /// Adds config. /// @@ -65,6 +70,8 @@ public static void AddConfig(this IServiceCollection services, IConfiguration co services.AddOptions().Bind(config.GetSection(AzureSectionName)); services.AddOptions().Bind(config.GetSection(MoodleSectionName)); + + services.AddOptions().Bind(config.GetSection(DatabricksSectionName)); } private static OptionsBuilder RegisterPostConfigure(this OptionsBuilder builder) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs new file mode 100644 index 00000000..faee4af0 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs @@ -0,0 +1,129 @@ +namespace LearningHub.NHS.OpenAPI.Controllers +{ + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.OpenApi.Models.ViewModels; + using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + + /// + /// Report Controller. + /// + [ApiController] + [Authorize(Policy = "AuthorizeOrCallFromLH")] + [Route("Report")] + public class ReportController : OpenApiControllerBase + { + private readonly IDatabricksService databricksService; + + /// + /// Initializes a new instance of the class. + /// + /// The catalogue service. + public ReportController(IDatabricksService databricksService) + { + this.databricksService = databricksService; + } + + /// + /// Get all catalogues. + /// + /// Task. + [HttpGet] + [Route("GetReporterPermission")] + public async Task GetReporterPermission() + { + return await this.databricksService.IsUserReporter(this.CurrentUserId.GetValueOrDefault()); + } + + /// + /// Get CourseCompletionReport from Databricks. + /// + /// requestModel. + /// Task. + [HttpPost] + [Route("GetCourseCompletionReport")] + public async Task CourseCompletionReport(DatabricksRequestModel requestModel) + { + return await this.databricksService.CourseCompletionReport(this.CurrentUserId.GetValueOrDefault(),requestModel); + } + + /// + /// Get CourseCompletionReport from Databricks. + /// + /// request. + /// Task. + [HttpPost] + [Route("GetReportHistory")] + public async Task> GetReportHistory(PagingRequestModel request) + { + return await this.databricksService.GetPagedReportHistory(this.CurrentUserId.GetValueOrDefault(), request.Page, request.PageSize); + } + + /// + /// Get GetReportHistoryById. + /// + /// reportHistoryId. + /// Task. + [HttpGet] + [Route("GetReportHistoryById/{reportHistoryId}")] + public async Task GetReportHistoryById(int reportHistoryId) + { + return await this.databricksService.GetPagedReportHistoryById(this.CurrentUserId.GetValueOrDefault(), reportHistoryId); + } + + /// + /// Get QueueReportDownload. + /// + /// reportHistoryId. + /// Task. + [HttpGet] + [Route("QueueReportDownload/{reportHistoryId}")] + public async Task QueueReportDownload(int reportHistoryId) + { + return await this.databricksService.QueueReportDownload(this.CurrentUserId.GetValueOrDefault(), reportHistoryId); + } + + /// + /// Get DownloadReport. + /// + /// reportHistoryId. + /// Task. + [HttpGet] + [Route("DownloadReport/{reportHistoryId}")] + public async Task DownloadReport(int reportHistoryId) + { + return await this.databricksService.DownloadReport(this.CurrentUserId.GetValueOrDefault(), reportHistoryId); + } + + /// + /// DatabricksJobNotify. + /// + /// databricksNotification. + /// Task. + [HttpPost] + [AllowAnonymous] + [Route("DatabricksJobNotify")] + public async Task DatabricksJobNotify(DatabricksNotification databricksNotification) + { + await this.databricksService.DatabricksJobUpdate(this.CurrentUserId.GetValueOrDefault(), databricksNotification); + return this.Ok(new ApiResponse(true)); + } + + /// + /// UpdateDatabricksReport. + /// + /// databricksUpdateRequest. + /// Task. + [HttpPost] + [Route("UpdateDatabricksReport")] + public async Task UpdateDatabricksReport(DatabricksUpdateRequest databricksUpdateRequest) + { + await this.databricksService.UpdateDatabricksReport(this.CurrentUserId.GetValueOrDefault(), databricksUpdateRequest); + return this.Ok(new ApiResponse(true)); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs index 98590761..54d04308 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs @@ -353,7 +353,6 @@ public async Task>> GetLHUserNavigation() return this.MenuItems(model); } - private List> MenuItems(NavigationModel model) { var menu = new List> @@ -382,6 +381,12 @@ private List> MenuItems(NavigationModel model) { "url", this.learningHubConfig.BrowseCataloguesUrl }, { "visible", model.ShowBrowseCatalogues }, }, + new Dictionary + { + { "title", "Reports" }, + { "url", this.learningHubConfig.ReportUrl }, + { "visible", model.ShowReports }, + }, new Dictionary { { "title", "Admin" }, diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index f23f74ba..cfc4572c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -19,7 +19,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user index 5bdd1216..1d8db97a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user @@ -5,7 +5,7 @@ IIS Local - ApiControllerEmptyScaffolder - root/Common/Api + MvcControllerEmptyScaffolder + root/Common/MVC/Controller \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index bec2f230..4d62b872 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -80,6 +80,7 @@ "ResourcePublishQueueRouteName": "", "HierarchyEditPublishQueueName": "", "ContentManagementQueueName": "", + "DatabricksProcessingQueueName": "", "AuthClientIdentityKey": "", "LHClientIdentityKey": "", "ReportApiClientIdentityKey": "", @@ -93,7 +94,9 @@ "ResourcePublishFailedWithReason": "

    The resource you contributed failed to publish, which means that users cannot access it.

    The error message generated was:
    [ErrorMessage]

    Please contact the support team for more information.

    ", "ResourceAccessTitle": "What you can do in the Learning Hub has changed", "ResourceReadonlyAccess": "

    You can continue to search for and access resources in the Learning Hub, however you cannot contribute to it.

    If you have any questions about this, please contact the support team.

    ", - "ResourceContributeAccess": "

    You can now contribute to the Learning Hub and continue to search for and access resources.

    If you have any questions about this, please contact the support team.

    " + "ResourceContributeAccess": "

    You can now contribute to the Learning Hub and continue to search for and access resources.

    If you have any questions about this, please contact the support team.

    ", + "ReportTitle": "

    [ReportName] report for [ReportContent] is ready

    ", + "Report": "

    Content: Your report [ReportName] report for [ReportContent] is ready. You can view and download the report in the Reports Section section

    " }, "MyContributionsUrl": "/my-contributions", "MyLearningUrl": "/MyLearning", @@ -107,7 +110,8 @@ "RegisterUrl": "/register", "SignOutUrl": "/home/logout", "MyAccountUrl": "/myaccount", - "BrowseCataloguesUrl": "/allcatalogue" + "BrowseCataloguesUrl": "/allcatalogue", + "ReportUrl": "" }, "LearningHubAPIConfig": { "BaseUrl": "https://learninghub.nhs.uk/api" @@ -126,5 +130,13 @@ "ApiBaseUrl": "", "ApiWsRestFormat": "json", "ApiWsToken": "" + }, + "Databricks": { + "InstanceUrl": "", + "Token": "", + "WarehouseId": "", + "JobId": "", + "UserPermissionEndpoint": "", + "CourseCompletionEndpoint": "" } } diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index a88a4bcf..6a1492b6 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -103,6 +103,7 @@ +
    @@ -577,7 +578,6 @@ - @@ -622,7 +622,6 @@ - @@ -680,7 +679,6 @@ - @@ -691,6 +689,10 @@ + + + + @@ -761,4 +763,4 @@ - \ No newline at end of file + diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6148-ADFtableData.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6148-ADFtableData.sql index e748cbf8..d9e8acd3 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6148-ADFtableData.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6148-ADFtableData.sql @@ -42,4 +42,5 @@ VALUES ('ELFHtoLH', 'JobRoleTbl', '1900-01-01'), ('ELFHtoLH', 'userTermsAndConditionsTBL', '1900-01-01'), ('ELFHtoLH', 'medicalCouncilTBL', '1900-01-01'), -('ELFHtoLH', 'staffGroupTBL', '1900-01-01') +('ELFHtoLH', 'staffGroupTBL', '1900-01-01'), +('ELFHtoLH', 'usergroupreportertbl', '1900-01-01') diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6625-AddtionalTable-CDC.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6625-AddtionalTable-CDC.sql new file mode 100644 index 00000000..f87485a2 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/TD-6625-AddtionalTable-CDC.sql @@ -0,0 +1,10 @@ +-- DROP NodeEditor table +DROP TABLE elfh.userHistoryAttributeTBL + +--Enable CDC +EXEC sys.sp_cdc_enable_table + @source_schema = N'elfh', + @source_name = N'userReportingUserTBL', + @role_name = NULL, + @supports_net_changes = 0; +GO diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql index 28e48027..5128603f 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql @@ -7,44 +7,56 @@ -- -- 04-11-2025 Sarathlal Initial Revision ------------------------------------------------------------------------------- -CREATE PROCEDURE [AdfMergeUserAdminLocation] +CREATE PROCEDURE [dbo].[AdfMergeUserAdminLocation] @UserAdminLocationList dbo.UserAdminLocationType READONLY AS BEGIN SET NOCOUNT ON; - MERGE [elfh].[userAdminLocationTBL] AS target - USING @UserAdminLocationList AS source - ON target.[userId] = source.[userId] - AND target.[adminLocationId] = source.[adminLocationId] -- composite key match + -- Deduplicate source first + ;WITH DedupedSource AS ( + SELECT *, + ROW_NUMBER() OVER ( + PARTITION BY userId, adminLocationId, deleted + ORDER BY amendDate DESC, createdDate DESC + ) AS rn + FROM @UserAdminLocationList + ), + CleanSource AS ( + SELECT * FROM DedupedSource WHERE rn = 1 + ) + + MERGE elfh.userAdminLocationTBL AS target + USING CleanSource AS source + ON target.userId = source.userId + AND target.adminLocationId = source.adminLocationId + AND target.deleted = source.deleted -- IMPORTANT! WHEN MATCHED THEN UPDATE SET - target.[deleted] = source.[deleted], - target.[amendUserId] = source.[amendUserId], - target.[amendDate] = source.[amendDate], - target.[createdUserId] = source.[createdUserId], - target.[createdDate] = source.[createdDate] + target.amendUserId = source.amendUserId, + target.amendDate = source.amendDate, + target.createdUserId = source.createdUserId, + target.createdDate = source.createdDate WHEN NOT MATCHED BY TARGET THEN INSERT ( - [userId], - [adminLocationId], - [deleted], - [amendUserId], - [amendDate], - [createdUserId], - [createdDate] + userId, + adminLocationId, + deleted, + amendUserId, + amendDate, + createdUserId, + createdDate ) VALUES ( - source.[userId], - source.[adminLocationId], - source.[deleted], - source.[amendUserId], - source.[amendDate], - source.[createdUserId], - source.[createdDate] + source.userId, + source.adminLocationId, + source.deleted, + source.amendUserId, + source.amendDate, + source.createdUserId, + source.createdDate ); - END GO diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserEmploymentReference.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserEmploymentReference.sql index 5a5aa29a..e6f0d2ad 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserEmploymentReference.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserEmploymentReference.sql @@ -7,13 +7,14 @@ -- -- 04-11-2025 Sarathlal Initial Revision ------------------------------------------------------------------------------- -CREATE PROCEDURE [AdfMergeUserEmploymentReference] +CREATE PROCEDURE [dbo].[AdfMergeUserEmploymentReference] @UserEmploymentReferenceList dbo.UserEmploymentReferenceType READONLY AS BEGIN SET NOCOUNT ON; SET IDENTITY_INSERT [elfh].[userEmploymentReferenceTBL] ON; + ALTER TABLE [elfh].[userEmploymentReferenceTBL] NOCHECK CONSTRAINT ALL; MERGE [elfh].[userEmploymentReferenceTBL] AS target USING @UserEmploymentReferenceList AS source ON target.[userEmploymentReferenceId] = source.[userEmploymentReferenceId] @@ -47,5 +48,6 @@ BEGIN source.[amendDate] ); SET IDENTITY_INSERT [elfh].[userEmploymentReferenceTBL] OFF; + ALTER TABLE [elfh].[userEmploymentReferenceTBL] NOCHECK CONSTRAINT ALL; END GO diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserGroupReporter.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserGroupReporter.sql new file mode 100644 index 00000000..d1817a38 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserGroupReporter.sql @@ -0,0 +1,50 @@ +------------------------------------------------------------------------------- +-- Author Sarathlal +-- Created 25-11-2025 +-- Purpose ELFH-LH Data sync +-- +-- Modification History +-- +-- 25-11-2025 Sarathlal Initial Revision +------------------------------------------------------------------------------- +CREATE PROCEDURE [dbo].[AdfMergeUserGroupReporter] + @UserGroupReporterList [dbo].[UserGroupReporterType] READONLY +AS +BEGIN + SET NOCOUNT ON; + SET IDENTITY_INSERT [elfh].[userGroupReporterTBL] ON; + ALTER TABLE elfh.userGroupReporterTBL NOCHECK CONSTRAINT FK_userGroupReporterTBL_userGroupTBL; + MERGE [elfh].[userGroupReporterTBL] AS target + USING @UserGroupReporterList AS source + ON target.[userGroupReporterId] = source.[userGroupReporterId] + + WHEN MATCHED THEN + UPDATE SET + target.[userId] = source.[userId], + target.[userGroupId] = source.[userGroupId], + target.[deleted] = source.[deleted], + target.[amendUserId] = source.[amendUserId], + target.[amendDate] = source.[amendDate] + + WHEN NOT MATCHED BY TARGET THEN + INSERT ( + [userGroupReporterId], + [userId], + [userGroupId], + [deleted], + [amendUserId], + [amendDate] + ) + VALUES ( + source.[userGroupReporterId], + source.[userId], + source.[userGroupId], + source.[deleted], + source.[amendUserId], + source.[amendDate] + ); + SET IDENTITY_INSERT [elfh].[userGroupReporterTBL] OFF; + ALTER TABLE elfh.userGroupReporterTBL CHECK CONSTRAINT FK_userGroupReporterTBL_userGroupTBL; +END +GO + diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserHistoryAttribute.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserHistoryAttribute.sql deleted file mode 100644 index cc17b95f..00000000 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserHistoryAttribute.sql +++ /dev/null @@ -1,62 +0,0 @@ -------------------------------------------------------------------------------- --- Author Sarathlal --- Created 04-11-2025 --- Purpose ELFH-LH Data sync --- --- Modification History --- --- 04-11-2025 Sarathlal Initial Revision -------------------------------------------------------------------------------- -CREATE PROCEDURE [AdfMergeUserHistoryAttribute] - @UserHistoryAttributeList dbo.UserHistoryAttributeType READONLY -AS -BEGIN - SET NOCOUNT ON; - - ALTER TABLE [elfh].[userHistoryAttributeTBL] NOCHECK CONSTRAINT ALL; - SET IDENTITY_INSERT [elfh].[userHistoryAttributeTBL] ON; - MERGE [elfh].[userHistoryAttributeTBL] AS target - USING @UserHistoryAttributeList AS source - ON target.[userHistoryAttributeId] = source.[userHistoryAttributeId] - - WHEN MATCHED THEN - UPDATE SET - target.[userHistoryId] = source.[userHistoryId], - target.[attributeId] = source.[attributeId], - target.[intValue] = source.[intValue], - target.[textValue] = source.[textValue], - target.[booleanValue] = source.[booleanValue], - target.[dateValue] = source.[dateValue], - target.[deleted] = source.[deleted], - target.[amendUserId] = source.[amendUserId], - target.[amendDate] = source.[amendDate] - - WHEN NOT MATCHED BY TARGET THEN - INSERT ( - [userHistoryAttributeId], - [userHistoryId], - [attributeId], - [intValue], - [textValue], - [booleanValue], - [dateValue], - [deleted], - [amendUserId], - [amendDate] - ) - VALUES ( - source.[userHistoryAttributeId], - source.[userHistoryId], - source.[attributeId], - source.[intValue], - source.[textValue], - source.[booleanValue], - source.[dateValue], - source.[deleted], - source.[amendUserId], - source.[amendDate] - ); - SET IDENTITY_INSERT [elfh].[userHistoryAttributeTBL] OFF; - ALTER TABLE [elfh].[userHistoryAttributeTBL] CHECK CONSTRAINT ALL; -END -GO diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserPasswordValidationToke.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserPasswordValidationToke.sql index d9c65fd6..c37cbd56 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserPasswordValidationToke.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserPasswordValidationToke.sql @@ -15,6 +15,8 @@ BEGIN -- Enable identity insert if userPasswordValidationTokenId is an IDENTITY column SET IDENTITY_INSERT [elfh].[userPasswordValidationTokenTBL] ON; + ALTER TABLE [elfh].[userPasswordValidationTokenTBL] NOCHECK CONSTRAINT ALL; + ALTER TABLE [hub].[User] NOCHECK CONSTRAINT ALL; MERGE [elfh].[userPasswordValidationTokenTBL] AS target USING @userPasswordValidationTokenList AS source ON target.userPasswordValidationTokenId = source.userPasswordValidationTokenId @@ -56,5 +58,7 @@ BEGIN -- Disable identity insert SET IDENTITY_INSERT [elfh].[userPasswordValidationTokenTBL] OFF; + ALTER TABLE [hub].[User] CHECK CONSTRAINT ALL; + ALTER TABLE [elfh].[userPasswordValidationTokenTBL] CHECK CONSTRAINT ALL; END GO diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeattribute.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeattribute.sql index d6851b39..33969535 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeattribute.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeattribute.sql @@ -32,20 +32,6 @@ CREATE TYPE dbo.UserReportingUserType AS TABLE [AmendDate] DATETIMEOFFSET ); GO -CREATE TYPE UserHistoryAttributeType AS TABLE -( - [userHistoryAttributeId] INT, - [userHistoryId] INT, - [attributeId] INT, - [intValue] INT NULL, - [textValue] NVARCHAR(1000) NULL, - [booleanValue] BIT NULL, - [dateValue] DATETIMEOFFSET NULL, - [deleted] BIT, - [amendUserId] INT, - [amendDate] DATETIMEOFFSET -); -GO CREATE TYPE UserEmploymentResponsibilityType AS TABLE ( [userEmploymentResponsibilityId] INT, @@ -584,6 +570,16 @@ CREATE TYPE dbo.MedicalCouncil AS TABLE amendDate datetimeoffset(7) ); GO +CREATE TYPE [dbo].[UserGroupReporterType] AS TABLE +( + [userGroupReporterId] INT, + [userId] INT, + [userGroupId] INT, + [deleted] BIT, + [amendUserId] INT, + [amendDate] DATETIMEOFFSET(7) +); +GO CREATE PROCEDURE [dbo].[AdfMergeattribute] @attributeList dbo.Attribute READONLY -- Table-valued parameter AS diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeuserEmployment.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeuserEmployment.sql index 7e4dc75e..5be94cd1 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeuserEmployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeuserEmployment.sql @@ -14,6 +14,8 @@ BEGIN SET NOCOUNT ON; SET IDENTITY_INSERT [elfh].[userEmploymentTBL] ON; + ALTER TABLE [elfh].[userEmploymentTBL] NOCHECK CONSTRAINT ALL; + ALTER TABLE [hub].[User] NOCHECK CONSTRAINT ALL; MERGE [elfh].[userEmploymentTBL] AS target USING @userEmploymentList AS source ON target.userEmploymentId = source.userEmploymentId @@ -73,5 +75,7 @@ BEGIN -- Disable identity insert SET IDENTITY_INSERT [elfh].[userEmploymentTBL] OFF; + ALTER TABLE [elfh].[userEmploymentTBL] NOCHECK CONSTRAINT ALL; + ALTER TABLE [hub].[User] NOCHECK CONSTRAINT ALL; END GO diff --git a/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql b/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql new file mode 100644 index 00000000..61c2cec7 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql @@ -0,0 +1,25 @@ +CREATE TABLE [reports].[ReportHistory] +( + [Id] INT NOT NULL IDENTITY (1, 1), + [CourseFilter] NVARCHAR(512) NULL, + [FirstRun] DATETIMEOFFSET(7) NOT NULL, + [LastRun] DATETIMEOFFSET(7) NOT NULL, + [PeriodDays] INT NOT NULL, + [StartDate][datetimeoffset](7) NULL, + [EndDate][datetimeoffset](7) NULL, + [DownloadRequest] BIT NULL, + [DownloadRequested][datetimeoffset](7) NULL, + [DownloadReady][datetimeoffset](7) NULL, + [ReportStatusId] INT NULL, + [FilePath] NVARCHAR(1024) NULL, + [DownloadedDate] [datetimeoffset](7) NULL, + [ParentJobRunId] INT NULL, + [JobRunId] INT NULL, + [ProcessingMessage] NVARCHAR(1024) NULL, + [Deleted] [bit] NOT NULL, + [CreateUserId] INT NOT NULL, + [CreateDate] [datetimeoffset](7) NOT NULL, + [AmendUserId] INT NOT NULL, + [AmendDate] [datetimeoffset](7) NOT NULL, + CONSTRAINT [PK_Reports_ReportHistory] PRIMARY KEY CLUSTERED ([Id] ASC) ON [PRIMARY] +); \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Tables/Elfh/UserGroupReporterTBL.sql b/WebAPI/LearningHub.Nhs.Database/Tables/Elfh/UserGroupReporterTBL.sql new file mode 100644 index 00000000..8794a0a6 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Tables/Elfh/UserGroupReporterTBL.sql @@ -0,0 +1,30 @@ +CREATE TABLE [elfh].[userGroupReporterTBL]( + [userGroupReporterId] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL, + [userId] [int] NOT NULL, + [userGroupId] [int] NOT NULL, + [deleted] [bit] NOT NULL, + [amendUserId] [int] NOT NULL, + [amendDate] [datetimeoffset](7) NOT NULL, + CONSTRAINT [PK_userGroupReporterTBL] PRIMARY KEY CLUSTERED +( + [userGroupReporterId] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY] +) ON [PRIMARY] +GO + +ALTER TABLE [elfh].[userGroupReporterTBL] ADD DEFAULT (sysdatetimeoffset()) FOR [amendDate] +GO + +ALTER TABLE [elfh].[userGroupReporterTBL] WITH CHECK ADD CONSTRAINT [FK_userGroupReporterTBL_userGroupTBL] FOREIGN KEY([userGroupId]) +REFERENCES [hub].[userGroup] ([Id]) +GO + +ALTER TABLE [elfh].[userGroupReporterTBL] CHECK CONSTRAINT [FK_userGroupReporterTBL_userGroupTBL] +GO + +ALTER TABLE [elfh].[userGroupReporterTBL] WITH CHECK ADD CONSTRAINT [FK_userGroupReporterTBL_userTBL] FOREIGN KEY([userId]) +REFERENCES [hub].[user] ([Id]) +GO + +ALTER TABLE [elfh].[userGroupReporterTBL] CHECK CONSTRAINT [FK_userGroupReporterTBL_userTBL] +GO \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Tables/Elfh/UserHistoryAttributeTBL.sql b/WebAPI/LearningHub.Nhs.Database/Tables/Elfh/UserHistoryAttributeTBL.sql deleted file mode 100644 index 0ab66132..00000000 --- a/WebAPI/LearningHub.Nhs.Database/Tables/Elfh/UserHistoryAttributeTBL.sql +++ /dev/null @@ -1,39 +0,0 @@ -CREATE TABLE [elfh].[userHistoryAttributeTBL]( - [userHistoryAttributeId] [int] IDENTITY(1,1) NOT NULL, - [userHistoryId] [int] NOT NULL, - [attributeId] [int] NOT NULL, - [intValue] [int] NULL, - [textValue] [nvarchar](1000) NULL, - [booleanValue] [bit] NULL, - [dateValue] [datetimeoffset](7) NULL, - [deleted] [bit] NOT NULL, - [amendUserId] [int] NOT NULL, - [amendDate] [datetimeoffset](7) NOT NULL, - CONSTRAINT [PK_userHistoryAttributeTBL] PRIMARY KEY CLUSTERED -( - [userHistoryAttributeId] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO - -ALTER TABLE [elfh].[userHistoryAttributeTBL] ADD CONSTRAINT [DF_userHistoryAttributeTBL_deleted] DEFAULT ((0)) FOR [deleted] -GO - -ALTER TABLE [elfh].[userHistoryAttributeTBL] ADD CONSTRAINT [DF_userHistoryAttributeTBL_amendDate] DEFAULT (sysdatetimeoffset()) FOR [amendDate] -GO - -ALTER TABLE [elfh].[userHistoryAttributeTBL] WITH CHECK ADD CONSTRAINT [FK_userHistoryAttributeTBL_attributeId] FOREIGN KEY([attributeId]) -REFERENCES [elfh].[attributeTBL] ([attributeId]) -GO - -ALTER TABLE [elfh].[userHistoryAttributeTBL] CHECK CONSTRAINT [FK_userHistoryAttributeTBL_attributeId] -GO - -ALTER TABLE [elfh].[userHistoryAttributeTBL] WITH CHECK ADD CONSTRAINT [FK_userHistoryAttributeTBL_userHistoryId] FOREIGN KEY([userHistoryId]) -REFERENCES [elfh].[userHistoryTBL] ([userHistoryId]) -GO - -ALTER TABLE [elfh].[userHistoryAttributeTBL] CHECK CONSTRAINT [FK_userHistoryAttributeTBL_userHistoryId] -GO - - diff --git a/replacements.txt b/replacements.txt new file mode 100644 index 00000000..4819f42b --- /dev/null +++ b/replacements.txt @@ -0,0 +1 @@ +Databricks_API_Token==>REMOVED \ No newline at end of file