Type:
- @UtilityHelper.GetPrettifiedResourceTypeNameMoodle(UtilityHelper.ToEnum(item.ResourceType), 0)
+ @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0)
@if (item.ResourceType != "moodle")
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/MoodleConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/MoodleConfig.cs
new file mode 100644
index 000000000..20e9b1904
--- /dev/null
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/MoodleConfig.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LearningHub.Nhs.OpenApi.Models.Configuration
+{
+ ///
+ /// The Moodle Settings.
+ ///
+ public class MoodleConfig
+ {
+ ///
+ /// Gets or sets the base url for the Moodle service.
+ ///
+ public string ApiBaseUrl { get; set; } = null!;
+
+ ///
+ /// Gets or sets the Web service Rest Format.
+ ///
+ public string ApiWsRestFormat { get; set; } = null!;
+
+ ///
+ /// Gets or sets the token.
+ ///
+ public string ApiWsToken { get; set; } = null!;
+
+ ///
+ /// Gets or sets the token.
+ ///
+ public string ApiPath { get; set; } = "webservice/rest/server.php";
+
+ ///
+ /// Gets or sets the token.
+ ///
+ public string CoursePath { get; set; } = "course/view.php";
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Interfaces/IMoodleHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IMoodleHttpClient.cs
similarity index 79%
rename from LearningHub.Nhs.WebUI/Interfaces/IMoodleHttpClient.cs
rename to OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IMoodleHttpClient.cs
index 3348a20a9..4a98b4ebf 100644
--- a/LearningHub.Nhs.WebUI/Interfaces/IMoodleHttpClient.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IMoodleHttpClient.cs
@@ -1,8 +1,8 @@
-namespace LearningHub.Nhs.Services.Interface
-{
- using System.Net.Http;
- using System.Threading.Tasks;
+using System.Net.Http;
+using System.Threading.Tasks;
+namespace LearningHub.Nhs.OpenApi.Services.Interface.HttpClients
+{
///
/// The Moodle Http Client interface.
///
@@ -20,4 +20,4 @@ public interface IMoodleHttpClient
///
defaultParameters.
string GetDefaultParameters();
}
-}
\ No newline at end of file
+}
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 aa019e18c..6fe3b2dfe 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/IMoodleApiService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs
new file mode 100644
index 000000000..ee573d43b
--- /dev/null
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs
@@ -0,0 +1,36 @@
+using LearningHub.Nhs.Models.Moodle.API;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LearningHub.Nhs.OpenApi.Services.Interface.Services
+{
+ ///
+ /// IMoodleApiService.
+ ///
+ public interface IMoodleApiService
+ {
+ ///
+ /// GetResourcesAsync.
+ ///
+ /// The current LH User Id.
+ /// A representing the result of the asynchronous operation.
+ Task GetMoodleUserIdByUsernameAsync(int currentUserId);
+
+ ///
+ /// GetEnrolledCoursesAsync.
+ ///
+ /// Moodle user id.
+ /// pageNumber.
+ /// List of MoodleCourseResponseModel.
+ Task> GetEnrolledCoursesAsync(int currentUserId, int pageNumber);
+
+ ///
+ /// GetEnrolledCoursesAsync.
+ ///
+ /// Moodle user id.
+ /// Moodle course id.
+ /// pageNumber.
+ /// List of MoodleCourseResponseModel.
+ Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber);
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Services/MoodleHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/MoodleHttpClient.cs
similarity index 61%
rename from LearningHub.Nhs.WebUI/Services/MoodleHttpClient.cs
rename to OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/MoodleHttpClient.cs
index 79f44027a..2c9fc410a 100644
--- a/LearningHub.Nhs.WebUI/Services/MoodleHttpClient.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/MoodleHttpClient.cs
@@ -1,34 +1,42 @@
-namespace LearningHub.Nhs.Services
-{
- using System;
- using System.Net.Http;
- using System.Net.Http.Headers;
- using System.Threading.Tasks;
- using LearningHub.Nhs.Services.Interface;
- using Microsoft.Extensions.Configuration;
+using LearningHub.Nhs.OpenApi.Models.Configuration;
+using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+namespace LearningHub.Nhs.OpenApi.Services.HttpClients
+{
///
/// The Moodle Http Client.
///
public class MoodleHttpClient : IMoodleHttpClient, IDisposable
{
- private readonly HttpClient httpClient = new ();
+ private readonly MoodleConfig moodleConfig;
+ private readonly HttpClient httpClient = new();
+
private bool initialised = false;
- private string moodleAPIBaseUrl;
- private string moodleAPIMoodleWSRestFormat;
- private string moodleAPIWSToken;
+ private string moodleApiUrl;
+ private string moodleApiMoodleWsRestFormat;
+ private string moodleApiWsToken;
///
/// Initializes a new instance of the class.
///
/// httpClient.
- /// config.
- public MoodleHttpClient(HttpClient httpClient, IConfiguration config)
+ /// config.
+ public MoodleHttpClient(HttpClient httpClient, IOptions moodleConfig)
{
+ this.moodleConfig = moodleConfig.Value;
this.httpClient = httpClient;
- this.moodleAPIBaseUrl = config["MoodleAPIConfig:BaseUrl"] + "webservice/rest/server.php";
- this.moodleAPIMoodleWSRestFormat = config["MoodleAPIConfig:MoodleWSRestFormat"];
- this.moodleAPIWSToken = config["MoodleAPIConfig:WSToken"];
+
+ this.moodleApiUrl = this.moodleConfig.ApiBaseUrl + this.moodleConfig.ApiPath;
+ this.moodleApiMoodleWsRestFormat = this.moodleConfig.ApiWsRestFormat;
+ this.moodleApiWsToken = this.moodleConfig.ApiWsToken;
}
///
@@ -37,7 +45,7 @@ public MoodleHttpClient(HttpClient httpClient, IConfiguration config)
/// The .
public async Task GetClient()
{
- this.Initialise(this.moodleAPIBaseUrl);
+ this.Initialise(this.moodleApiUrl);
return this.httpClient;
}
@@ -47,8 +55,8 @@ public async Task GetClient()
/// defaultParameters.
public string GetDefaultParameters()
{
- string defaultParameters = $"wstoken={this.moodleAPIWSToken}"
- + $"&moodlewsrestformat={this.moodleAPIMoodleWSRestFormat}";
+ string defaultParameters = $"wstoken={this.moodleApiWsToken}"
+ + $"&moodlewsrestformat={this.moodleApiMoodleWsRestFormat}";
return defaultParameters;
}
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 9002a0237..bf7b23c23 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/MoodleApiService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs
new file mode 100644
index 000000000..08786db8d
--- /dev/null
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs
@@ -0,0 +1,192 @@
+namespace LearningHub.Nhs.OpenApi.Services.Services
+{
+ using LearningHub.Nhs.Models.Moodle.API;
+ using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients;
+ using LearningHub.Nhs.OpenApi.Services.Interface.Services;
+ using Microsoft.Extensions.Logging;
+ using Newtonsoft.Json;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Http;
+ using System.Text;
+ using System.Text.Json;
+ using System.Threading.Tasks;
+
+ ///
+ /// MoodleApiService.
+ ///
+ public class MoodleApiService : IMoodleApiService
+ {
+ private readonly IMoodleHttpClient moodleHttpClient;
+ private readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// moodleHttpClient.
+ /// logger.
+ public MoodleApiService(IMoodleHttpClient moodleHttpClient, ILogger logger)
+ {
+ this.moodleHttpClient = moodleHttpClient;
+ this.logger = logger;
+ }
+
+ ///
+ /// GetMoodleUserIdByUsernameAsync.
+ ///
+ /// current User Id.
+ /// UserId from Moodle.
+ public async Task GetMoodleUserIdByUsernameAsync(int currentUserId)
+ {
+ var parameters = new Dictionary
+ {
+ { "criteria[0][key]", "username" },
+ { "criteria[0][value]", currentUserId.ToString() }
+ };
+
+ var response = await GetCallMoodleApiAsync("core_user_get_users", parameters);
+
+ var user = response?.Users?.FirstOrDefault(u => u.Username == currentUserId.ToString());
+ return user?.Id ?? 0;
+ }
+
+
+ ///
+ /// GetEnrolledCoursesAsync.
+ ///
+ /// Moodle user id.
+ /// The page Number.
+ /// A representing the result of the asynchronous operation.
+ public async Task> GetEnrolledCoursesAsync(int userId, int pageNumber)
+ {
+ var parameters = new Dictionary
+ {
+ { "userid", userId.ToString() }
+ };
+
+ // Fetch enrolled courses
+ var enrolledCourses = await GetCallMoodleApiAsync>(
+ "core_enrol_get_users_courses",
+ parameters
+ );
+
+ if (enrolledCourses == null || enrolledCourses.Count == 0)
+ return new List();
+
+ // Load course completion info in parallel
+ var completionTasks = enrolledCourses
+ .Where(c => c.Id.HasValue)
+ .Select(async course =>
+ {
+ try
+ {
+ course.CourseCompletionViewModel = await GetCourseCompletionAsync(userId, course.Id.Value, pageNumber);
+ }
+ catch (Exception ex)
+ {
+ course.CourseCompletionViewModel = new MoodleCourseCompletionModel
+ {
+ CompletionStatus = null,
+ };
+ }
+
+ return course;
+ });
+
+ var enrichedCourses = await Task.WhenAll(completionTasks);
+
+ return enrichedCourses.ToList();
+
+ }
+
+ ///
+ /// GetEnrolledCoursesAsync.
+ ///
+ /// Moodle user id.
+ /// Moodle course id.
+ /// pageNumber.
+ /// List of MoodleCourseResponseModel.
+ public async Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber)
+ {
+ var parameters = new Dictionary
+ {
+ { "userid", userId.ToString() },
+ { "courseid", courseId.ToString() }
+ };
+
+ // Call Moodle API and parse response
+ var result = await GetCallMoodleApiAsync("core_completion_get_course_completion_status", parameters);
+
+ // If Moodle did not return an exception, return parsed completion data
+ if (result.Warnings.Count == 0)
+ {
+ // Optionally map/convert if needed
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(result));
+ }
+
+ return new MoodleCourseCompletionModel(); // Return empty model or null as fallback
+ }
+
+ private async Task GetCallMoodleApiAsync(string wsFunction, Dictionary parameters)
+ {
+ var client = await this.moodleHttpClient.GetClient();
+ string defaultParameters = this.moodleHttpClient.GetDefaultParameters();
+
+ // Build URL query string
+ var queryBuilder = new StringBuilder($"&wsfunction={wsFunction}");
+ foreach (var param in parameters)
+ {
+ queryBuilder.Append($"&{param.Key}={Uri.EscapeDataString(param.Value)}");
+ }
+
+ string fullUrl = "?" + defaultParameters + queryBuilder.ToString();
+
+ HttpResponseMessage response = await client.GetAsync(fullUrl);
+ string result = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode)
+ {
+ // Moodle may still return an error with 200 OK
+ try
+ {
+ using var document = JsonDocument.Parse(result);
+ var root = document.RootElement;
+
+ if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("exception", out var exceptionProp))
+ {
+ string? message = root.TryGetProperty("message", out var messageProp)
+ ? messageProp.GetString()
+ : "Unknown error";
+
+ this.logger.LogError($"Moodle returned an exception: {exceptionProp.GetString()}, Message: {message}");
+ throw new Exception($"Moodle API Error: {exceptionProp.GetString()}, Message: {message}");
+ }
+ }
+ catch (System.Text.Json.JsonException ex)
+ {
+ this.logger.LogError(ex, "Failed to parse Moodle API response as JSON.");
+ throw;
+ }
+
+ var deserialized = JsonConvert.DeserializeObject(result);
+
+ return deserialized == null
+ ? throw new Exception($"Failed to deserialize Moodle API response into type {typeof(T).Name}. Raw response: {result}")
+ : deserialized;
+ }
+ else if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ this.logger.LogError($"Moodle API access denied. Status Code: {response.StatusCode}");
+ throw new Exception("AccessDenied to MoodleApi");
+ }
+ else
+ {
+ this.logger.LogError($"Moodle API error. Status Code: {response.StatusCode}, Response: {result}");
+ throw new Exception("Error with MoodleApi");
+ }
+ }
+
+ }
+}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs
index fb9098bde..f00f13253 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs
@@ -23,6 +23,7 @@ public static class Startup
public static void AddServices(this IServiceCollection services)
{
services.AddScoped();
+ services.AddHttpClient();
services.AddScoped();
services.AddScoped();
services.AddScoped();
@@ -33,6 +34,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/Configuration/ConfigurationExtensions.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs
index 63b61f042..7d5a61795 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs
@@ -40,6 +40,11 @@ public static class ConfigurationExtensions
///
public const string AzureSectionName = "Azure";
+ ///
+ /// The FindwiseSectionName.
+ ///
+ public const string MoodleSectionName = "Moodle";
+
///
/// Adds config.
///
@@ -58,6 +63,8 @@ public static void AddConfig(this IServiceCollection services, IConfiguration co
services.AddOptions().Bind(config.GetSection(LearningHubApiSectionName));
services.AddOptions().Bind(config.GetSection(AzureSectionName));
+
+ services.AddOptions().Bind(config.GetSection(MoodleSectionName));
}
private static OptionsBuilder RegisterPostConfigure(this OptionsBuilder builder)
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MoodleController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MoodleController.cs
new file mode 100644
index 000000000..f2639d005
--- /dev/null
+++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MoodleController.cs
@@ -0,0 +1,70 @@
+namespace LearningHub.NHS.OpenAPI.Controllers
+{
+ using LearningHub.Nhs.OpenApi.Models.Configuration;
+ using LearningHub.Nhs.OpenApi.Services.Interface.Services;
+ using Microsoft.AspNetCore.Authorization;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.Extensions.Options;
+ using System.Threading.Tasks;
+
+ ///
+ /// Moodle operations.
+ ///
+ [Route("Moodle")]
+ [ApiController]
+ [Authorize]
+ public class MoodleController : Controller
+ {
+ private readonly IMoodleApiService moodleService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The moodle service.
+ public MoodleController(IMoodleApiService moodleService)
+ {
+ this.moodleService = moodleService;
+ }
+
+ ///
+ /// The GetMoodleUserId.
+ ///
+ /// The LH user id.
+ /// The .
+ [HttpGet]
+ [Route("GetMoodleUserId/{currentUserId?}")]
+ public async Task GetMoodleUserId(int? currentUserId)
+ {
+ if (currentUserId.HasValue)
+ {
+ var moodleUser = await this.moodleService.GetMoodleUserIdByUsernameAsync(currentUserId.Value);
+ return this.Ok(moodleUser);
+ }
+ else
+ {
+ return this.Ok(0);
+ }
+ }
+
+ ///
+ /// GetEnrolledCoursesAsync.
+ ///
+ /// Moodle user id.
+ /// The page Number.
+ /// A representing the result of the asynchronous operation.
+ [HttpGet]
+ [Route("GetEnrolledCourses/{currentUserId?}")]
+ public async Task GetEnrolledCoursesAsync(int? currentUserId, int pageNumber = 0)
+ {
+ if (currentUserId.HasValue)
+ {
+ var entrolledCourses = await this.moodleService.GetEnrolledCoursesAsync(currentUserId.Value, pageNumber);
+ return this.Ok(entrolledCourses);
+ }
+ else
+ {
+ return this.Ok(0);
+ }
+ }
+ }
+}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json
index 401183bf6..181c8eaf8 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json
+++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json
@@ -119,5 +119,10 @@
"ResponseType": "code id_token",
"AuthSecret": "",
"AuthTimeout": 20
+ },
+ "Moodle": {
+ "ApiBaseUrl": "",
+ "ApiWsRestFormat": "json",
+ "ApiWsToken": ""
}
}