diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index c3c94084a..56a3e4aac 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using AutoMapper; +using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.Models; using HwProj.AuthService.Client; using HwProj.CoursesService.Client; @@ -22,16 +23,19 @@ namespace HwProj.APIGateway.API.Controllers; public class CoursesController : AggregationController { private readonly ICoursesServiceClient _coursesClient; + private readonly ILtiToolService _ltiToolService; private readonly IMapper _mapper; private readonly IStudentsInformationProvider _studentsInfo; public CoursesController( ICoursesServiceClient coursesClient, + ILtiToolService ltiToolService, IAuthServiceClient authServiceClient, IMapper mapper, IStudentsInformationProvider studentsInfo) : base(authServiceClient) { _coursesClient = coursesClient; + _ltiToolService = ltiToolService; _mapper = mapper; _studentsInfo = studentsInfo; } @@ -103,6 +107,40 @@ public async Task GetProgramNames() [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] public async Task CreateCourse(CreateCourseViewModel model) { + AccountDataDto? ltiBot = null; + + if (!string.IsNullOrWhiteSpace(model.LtiToolName)) + { + var ltiTool = _ltiToolService.GetByName(model.LtiToolName); + if (ltiTool == null) + { + return BadRequest($"LTI-инструмент '{model.LtiToolName}' не найден"); + } + + var botResult = await AuthServiceClient.GetOrCreateLtiBot(ltiTool.ClientId); + if (!botResult.Succeeded) + { + return StatusCode( + (int)HttpStatusCode.ServiceUnavailable, + botResult.Errors); + } + + ltiBot = botResult.Value; + if (ltiBot == null) + { + return StatusCode( + (int)HttpStatusCode.InternalServerError, + "LTI-бот был создан, но его данные не найдены"); + } + + if (ltiBot.Role != Roles.ExpertRole) + { + return StatusCode( + (int)HttpStatusCode.InternalServerError, + "LTI-бот не имеет роли Expert"); + } + } + if (model.GroupNames.Any() && model.FetchStudents) { var studentCandidates = new List(); @@ -138,9 +176,33 @@ public async Task CreateCourse(CreateCourseViewModel model) } var result = await _coursesClient.CreateCourse(model); - return result.Succeeded - ? Ok(result.Value) - : BadRequest(result.Errors); + if (!result.Succeeded) + { + return BadRequest(result.Errors); + } + + if (ltiBot != null) + { + var addExpertResult = await _coursesClient.AcceptLecturer( + result.Value, + ltiBot.Email, + ltiBot.UserId, + sendNotification: false); + + if (!addExpertResult.Succeeded) + { + return StatusCode( + (int)HttpStatusCode.InternalServerError, + new + { + CourseId = result.Value, + Errors = addExpertResult.Errors, + Message = "Курс создан, но LTI-бот не был добавлен как эксперт" + }); + } + } + + return Ok(result.Value); } [HttpPost("update/{courseId}")] @@ -310,7 +372,8 @@ private async Task ToCourseViewModel(CourseDTO course) Homeworks = course.Homeworks, Groups = course.Groups, IsCompleted = course.IsCompleted, - IsOpen = course.IsOpen + IsOpen = course.IsOpen, + LtiToolName = course.LtiToolName, }; } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 51ca4e4f5..392bc317f 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -138,6 +138,7 @@ public async Task GetStudentSolution(long taskId, string studentI return Ok(new UserTaskSolutionsPageData { CourseId = course.Id, + LtiToolName = course.LtiToolName, CourseMates = accounts, TaskSolutions = taskSolutions }); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index 8f29323ea..33776c3f4 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -21,6 +21,7 @@ + diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiPlatformConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiPlatformConfig.cs new file mode 100644 index 000000000..3eb3af1db --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiPlatformConfig.cs @@ -0,0 +1,19 @@ +namespace HwProj.APIGateway.API.Lti.Configuration; + +public class LtiPlatformConfig +{ + public string Issuer { get; set; } + public string OidcAuthorizationEndpoint { get; set; } + public string DeepLinkReturnUrl { get; set; } + public string ResourceLinkReturnUrl { get; set; } + public string AssignmentsGradesEndpoint { get; set; } + public string AccessTokenUrl { get; set; } + public string JwksEndpoint { get; set; } + public LtiSigningKeyConfig SigningKey { get; set; } +} + +public class LtiSigningKeyConfig +{ + public string KeyId { get; set; } + public string PrivateKeyPem { get; set; } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiToolConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiToolConfig.cs new file mode 100644 index 000000000..1be3f2390 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiToolConfig.cs @@ -0,0 +1,13 @@ +namespace HwProj.APIGateway.API.Lti.Configuration +{ + public class LtiToolConfig + { + public string Name { get; set; } + public string Issuer { get; set; } + public string ClientId { get; set; } + public string JwksEndpoint { get; set; } + public string InitiateLoginUri { get; set; } + public string LaunchUrl { get; set; } + public string DeepLink { get; set; } + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs new file mode 100644 index 000000000..cf5685043 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs @@ -0,0 +1,57 @@ +using System.Security.Cryptography; +using HwProj.APIGateway.API.Lti.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +public class JwksController(IOptions options) : ControllerBase +{ + private readonly LtiPlatformConfig _config = options.Value; + + [HttpGet("jwks")] + [AllowAnonymous] + public IActionResult GetJwks() + { + var keyConfig = _config.SigningKey; + + if (string.IsNullOrEmpty(keyConfig?.PrivateKeyPem)) + { + return StatusCode(500, "Signing key is not configured."); + } + + using var rsa = RSA.Create(); + try + { + rsa.ImportFromPem(keyConfig.PrivateKeyPem); + } + catch (CryptographicException) + { + return StatusCode(500, "Invalid Private Key format in configuration."); + } + + var publicParams = rsa.ExportParameters(false); + + var jwks = new + { + keys = new[] + { + new + { + kty = "RSA", + e = Base64UrlEncoder.Encode(publicParams.Exponent), + n = Base64UrlEncoder.Encode(publicParams.Modulus), + kid = keyConfig.KeyId, + alg = "RS256", + use = "sig" + } + } + }; + + return Ok(jwks); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs new file mode 100644 index 000000000..ac8ebc70d --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs @@ -0,0 +1,113 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.Services; +using HwProj.APIGateway.API.LTI.Services; +using HwProj.AuthService.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +public class LtiAccessTokenController( + IOptions options, + ILtiToolService toolService, + ILtiKeyService ltiKeyService, + ILtiTokenService tokenService, + IAuthServiceClient authServiceClient + ) : ControllerBase +{ + [HttpPost("token")] + [AllowAnonymous] + public async Task GetTokenAsync([FromForm] IFormCollection form) + { + if (!form.TryGetValue("grant_type", out var grantType) || grantType != "client_credentials") + { + return BadRequest(new { error = "unsupported_grant_type", error_description = "Only 'client_credentials' is supported." }); + } + + if (!form.TryGetValue("client_assertion_type", out var assertionType) || + assertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + { + return BadRequest(new { error = "invalid_request", error_description = "Invalid client_assertion_type." }); + } + + if (!form.TryGetValue("client_assertion", out var clientAssertion)) + { + return BadRequest(new { error = "invalid_request", error_description = "Missing client_assertion." }); + } + + var handler = new JwtSecurityTokenHandler(); + if (!handler.CanReadToken(clientAssertion)) + { + return BadRequest(new { error = "invalid_client", error_description = "Invalid JWT structure." }); + } + + var unverifiedToken = handler.ReadJwtToken(clientAssertion); + + var clientId = unverifiedToken.Issuer; + + var tool = toolService.GetByClientId(clientId); + if (tool == null) + { + return Unauthorized(new { error = "invalid_client", error_description = $"Unknown clientId: {clientId}" }); + } + + var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); + + try + { + var tokenEndpointUrl = options.Value.AccessTokenUrl; + + handler.ValidateToken(clientAssertion, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = tool.ClientId, + + ValidateAudience = true, + ValidAudience = tokenEndpointUrl, + + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + + ValidateIssuerSigningKey = true, + IssuerSigningKeys = signingKeys + }, out _); + } + catch (Exception ex) + { + return Unauthorized(new { error = "invalid_client", error_description = $"Token validation failed: {ex.Message}" }); + } + + const string scope = "https://purl.imsglobal.org/spec/lti-ags/scope/score"; + + var botResult = await authServiceClient.GetOrCreateLtiBot(tool.ClientId); + if (!botResult.Succeeded || botResult.Value == null) + { + return StatusCode(StatusCodes.Status500InternalServerError, new + { + error = "server_error", + error_description = "Failed to resolve the LTI bot account." + }); + } + + var accessToken = tokenService.GenerateAccessTokenForLti( + tool.ClientId, + botResult.Value.UserId, + scope); + + return Ok(new + { + access_token = accessToken, + token_type = "Bearer", + expires_in = 3600, + scope + }); + } +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs new file mode 100644 index 000000000..f6fda03a6 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Services; +using HwProj.CoursesService.Client; +using HwProj.Exceptions; +using HwProj.Models.SolutionsService; +using HwProj.SolutionsService.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using LtiAdvantage.AssignmentGradeServices; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +[Authorize(AuthenticationSchemes = "LtiScheme")] +public class LtiAssignmentsGradesControllers( + ICoursesServiceClient coursesServiceClient, + ISolutionsServiceClient solutionsClient, + ILtiToolService toolService) + : ControllerBase +{ + [HttpPost("lineItem/{taskId}/scores")] + [Consumes("application/vnd.ims.lti-ags.v1.score+json")] + public async Task UpdateTaskScore(long taskId, [FromBody] Score score) + { + var scopeClaim = User.FindFirst("scope")?.Value; + if (string.IsNullOrEmpty(scopeClaim) || !scopeClaim.Contains("https://purl.imsglobal.org/spec/lti-ags/scope/score")) + { + return Forbid(); + } + + var toolClientId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? User.FindFirst("sub")?.Value; + + if (string.IsNullOrEmpty(toolClientId)) + { + return Unauthorized("Unknown tool client id."); + } + + var botId = User.FindFirst("_id")?.Value; + if (string.IsNullOrEmpty(botId)) + { + return Unauthorized("LTI bot id is missing."); + } + + var tool = toolService.GetByClientId(toolClientId); + if (tool == null) + { + return BadRequest("Tool not found."); + } + + var course = await coursesServiceClient.GetCourseByTask(taskId); + if (course == null) + { + return BadRequest("The task does not belong to any course."); + } + + if (!course.MentorIds.Contains(botId)) + { + return Forbid(); + } + + if (course.LtiToolName != tool.Name) + { + return BadRequest("This tool does not apply to this course."); + } + + if (string.IsNullOrEmpty(score.UserId) || + course.AcceptedStudents.All( + student => student.StudentId != score.UserId)) + { + return BadRequest("The student does not belong to this course."); + } + + var task = await coursesServiceClient.GetTask(taskId); + if (task.LtiLaunchData == null) + { + return BadRequest("This task is not linked to an LTI tool."); + } + + if (score.ScoreGiven < 0 || score.ScoreGiven > score.ScoreMaximum) + { + return BadRequest("ScoreGiven must be between 0 and ScoreMaximum."); + } + + try + { + await solutionsClient.PostSolutionWithRate( + taskId, + new PostSolutionModel + { + StudentId = score.UserId, + Rating = (int)Math.Round(score.ScoreGiven), + LecturerComment = $"Результат: {score.ScoreGiven}/{score.ScoreMaximum}\n\n" + score.Comment + }, + false); + + return Ok(new { message = "Score updated successfully" }); + } + catch (KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (ForbiddenException) + { + return Forbid(); + } + catch (Exception) + { + return StatusCode(500, "Internal Server Error"); + } + } +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs new file mode 100644 index 000000000..3ce938bfd --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -0,0 +1,281 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.DTOs; +using HwProj.APIGateway.API.Lti.Services; +using HwProj.APIGateway.API.LTI.Services; +using HwProj.AuthService.Client; +using HwProj.CoursesService.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +public class LtiAuthController( + ICoursesServiceClient coursesServiceClient, + IAuthServiceClient authServiceClient, + IOptions ltiPlatformOptions, + ILtiToolService toolService, + ILtiTokenService tokenService, + IDataProtectionProvider provider + ) + : ControllerBase +{ + private readonly IDataProtector protector = provider.CreateProtector("LtiPlatform.MessageHint.v1"); + + // Tool редиректит сюда браузер (шаг "redirect browser to Platform for Auth") + [HttpGet("authorize")] + [AllowAnonymous] + public async Task AuthorizeLti( + [FromQuery(Name = "client_id")] string clientId, + [FromQuery(Name = "redirect_uri")] string redirectUri, + [FromQuery(Name = "state")] string state, + [FromQuery(Name = "nonce")] string nonce, + [FromQuery(Name = "lti_message_hint")] string ltiMessageHint) + { + LtiHintPayload? payload; + try + { + var json = this.protector.Unprotect(ltiMessageHint); + payload = JsonSerializer.Deserialize(json); + } + catch + { + return BadRequest("Invalid or expired lti_message_hint"); + } + + var targetLinkUri = payload?.TargetLinkUri; + if (payload?.ToolName == null || + payload.CourseId == null || + string.IsNullOrWhiteSpace(targetLinkUri)) + { + return BadRequest("Invalid or expired lti_message_hint"); + } + + var tool = toolService.GetByName(payload.ToolName); + if (tool == null) + { + return BadRequest("Tool not found"); + } + + if (tool.ClientId != clientId) + { + return BadRequest($"Invalid clientId. Expected: {tool.ClientId}, Got: {clientId}"); + } + + var botResult = await authServiceClient.GetOrCreateLtiBot(tool.ClientId); + if (!botResult.Succeeded || botResult.Value == null) + { + return StatusCode(500, "Failed to resolve the LTI bot account."); + } + + var course = await coursesServiceClient.GetCourseById( + long.Parse(payload.CourseId), + botResult.Value.UserId); + if (course == null) + { + return NotFound("Course not found"); + } + + if (course.LtiToolName != tool.Name) + { + return BadRequest("The data is incorrect: the id of the instrument linked to the exchange rate does not match"); + } + + if (!course.MentorIds.Contains(botResult.Value.UserId)) + { + return Forbid(); + } + + string idToken; + switch (payload.Type) + { + case "DeepLinking": + idToken = tokenService.CreateDeepLinkingToken( + clientId: clientId, + courseId: payload.CourseId, + targetLinkUri: targetLinkUri, + userId: payload.UserId, + nonce: nonce + ); + break; + case "ResourceLink": + idToken = tokenService.CreateResourceLinkToken( + clientId: clientId, + courseId: payload.CourseId, + targetLinkUri: targetLinkUri, + ltiCustomParams: payload.Custom, + userId: payload.UserId, + nonce: nonce, + resourceLinkId: payload.ResourceLinkId!); + break; + default: + return BadRequest("Invalid or expired lti_message_hint"); + } + + var html = $""" + + + +
+ + +
+ + + """; + + return Content(html, "text/html"); + } + + [HttpGet("start")] + [Authorize] + public async Task StartLti( + [FromQuery] string? resourceLinkId, + [FromQuery] string? courseId, + [FromQuery] string? toolName, + [FromQuery] string? ltiLaunchUrl, + [FromQuery] string? ltiCustomParams, + [FromQuery] bool isDeepLink = false) + { + var userId = User.FindFirstValue("_id"); + if (userId == null) + { + return Unauthorized("User ID not found"); + } + + string targetUrl; + LtiHintPayload payload; + + if (courseId == null || toolName == null) + { + return BadRequest("For Deep Linking, courseId and toolId are required."); + } + + var tool = toolService.GetByName(toolName); + if (tool == null) + { + return NotFound("Tool not found"); + } + + var course = await coursesServiceClient.GetCourseById(long.Parse(courseId)); + if (course == null) + { + return NotFound("Course not found"); + } + + if (course.LtiToolName != toolName) + { + return BadRequest("The data is incorrect: the id of the instrument linked to the exchange rate does not match"); + } + + if (isDeepLink) + { + targetUrl = !string.IsNullOrEmpty(tool.DeepLink) + ? tool.DeepLink + : tool.LaunchUrl; + + payload = new LtiHintPayload + { + Type = "DeepLinking", + UserId = userId, + CourseId = courseId, + ToolName = toolName + }; + } + else if (!string.IsNullOrEmpty(resourceLinkId) && !string.IsNullOrEmpty(ltiLaunchUrl)) + { + targetUrl = ltiLaunchUrl; + + payload = new LtiHintPayload + { + Type = "ResourceLink", + UserId = userId, + CourseId = courseId, + ToolName = toolName, + ResourceLinkId = resourceLinkId, + Custom = ltiCustomParams + }; + } + else + { + return BadRequest("Either resourceLinkId OR (isDeepLink + courseId + toolId) must be provided."); + } + + payload.TargetLinkUri = targetUrl; + + var json = JsonSerializer.Serialize(payload); + var messageHint = this.protector.Protect(json); + + var dto = new AuthorizePostFormDto( + tool.InitiateLoginUri, + "POST", + new Dictionary + { + ["iss"] = ltiPlatformOptions.Value.Issuer, + ["login_hint"] = userId, + ["target_link_uri"] = targetUrl, + ["lti_message_hint"] = messageHint, + ["client_id"] = tool.ClientId, + }); + + return Ok(dto); + } + + [HttpGet("closeLtiSession")] + public IActionResult CloseLtiSession() + { + const string htmlContent = @" + + + + + Сессия завершена + + + + +
+

Работа с инструментом завершена

+

Вкладка должна закрыться автоматически, а страница задачи обновиться.

+

Если этого не произошло, нажмите кнопку ниже:

+ +
+ + "; + + return Content(htmlContent, "text/html"); + } + + private class LtiHintPayload + { + public string Type { get; set; } + public string UserId { get; set; } + public string? ResourceLinkId { get; set; } + public string? CourseId { get; set; } + public string? ToolName { get; set; } + public string? Custom { get; set; } + public string? TargetLinkUri { get; set; } + } +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs new file mode 100644 index 000000000..b3bbdaf05 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IdentityModel.Tokens.Jwt; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Unicode; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +public class LtiDeepLinkingReturnController( + IOptions ltiPlatformOptions, + ILtiToolService toolService, + ILtiKeyService ltiKeyService + ) : ControllerBase +{ + private static readonly JwtSecurityTokenHandler Handler = new(); + + [HttpPost("deepLinkReturn")] + [AllowAnonymous] + public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollection form) + { + if (!form.TryGetValue("JWT", out var jwtValue)) + { + return BadRequest("Missing JWT parameter"); + } + + var tokenString = jwtValue.ToString(); + + if (!Handler.CanReadToken(tokenString)) + { + return BadRequest("Invalid JWT structure"); + } + + var unverifiedToken = Handler.ReadJwtToken(tokenString); + var clientId = unverifiedToken.Issuer; + + var tool = toolService.GetByClientId(clientId); + if (tool == null) + { + return Unauthorized($"Unknown tool clientId: {clientId}"); + } + + if (string.IsNullOrWhiteSpace(tool.LaunchUrl)) + { + return BadRequest("Tool launch URL is not configured"); + } + + var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); + JwtSecurityToken validatedToken; + + try + { + Handler.ValidateToken(tokenString, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = tool.ClientId, + ValidateAudience = true, + ValidAudience = ltiPlatformOptions.Value.Issuer, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidateIssuerSigningKey = true, + IssuerSigningKeys = signingKeys + }, out var secToken); + + validatedToken = (JwtSecurityToken)secToken; + } + catch (Exception ex) + { + return BadRequest($"Token signature validation failed: {ex.Message}"); + } + + const string itemsClaimName = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items"; + + var itemsClaims = validatedToken.Claims + .Where(c => c.Type == itemsClaimName) + .Select(c => c.Value) + .ToList(); + + if (itemsClaims.Count == 0) + { + return Content("", "text/html"); + } + + var options = new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) + }; + + var contentItems = new List(); + try + { + foreach (var itemsClaim in itemsClaims) + { + var parsedClaim = JsonNode.Parse(itemsClaim); + if (parsedClaim is JsonArray itemsArray) + { + foreach (var item in itemsArray) + { + contentItems.Add(ApplyLaunchUrlFallback(item, tool.LaunchUrl)); + } + } + else + { + contentItems.Add(ApplyLaunchUrlFallback(parsedClaim, tool.LaunchUrl)); + } + } + } + catch (JsonException) + { + return BadRequest("Invalid content_items claim"); + } + + var jsonPayload = JsonSerializer.Serialize(contentItems, options); + + // language=html + var htmlResponse = $@" + + + Processing LTI Return... + + + + + + "; + + return Content(htmlResponse, "text/html"); + } + + private static JsonNode? ApplyLaunchUrlFallback(JsonNode? contentItem, string launchUrl) + { + if (contentItem is not JsonObject contentItemObject) + { + return contentItem; + } + + var isLtiResourceLink = contentItemObject["type"] is JsonValue typeValue && + typeValue.TryGetValue(out var type) && + string.Equals(type, "ltiResourceLink", StringComparison.Ordinal); + + if (!isLtiResourceLink) + { + return contentItemObject; + } + + var hasLaunchUrl = contentItemObject["url"] is JsonValue urlValue && + urlValue.TryGetValue(out var url) && + !string.IsNullOrWhiteSpace(url); + + if (!hasLaunchUrl) + { + contentItemObject["url"] = launchUrl; + } + + return contentItemObject; + } +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs new file mode 100644 index 000000000..d73563940 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.DTOs; +using HwProj.APIGateway.API.Lti.Services; +using Microsoft.AspNetCore.Mvc; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti/tools")] +[ApiController] +public class LtiToolsController(ILtiToolService toolService) : ControllerBase +{ + [HttpGet] + [ProducesResponseType(typeof(IReadOnlyList), (int)HttpStatusCode.OK)] + public ActionResult> GetAll() + { + var tools = toolService.GetAll(); + return Ok(tools); + } + + [HttpGet("{id:long}")] + [ProducesResponseType(typeof(LtiToolDto), (int)HttpStatusCode.OK)] + public ActionResult Get(string name) + { + var tool = toolService.GetByName(name); + if (tool == null) + { + return NotFound(); + } + + return Ok(tool); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs new file mode 100644 index 000000000..75ad47143 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -0,0 +1,387 @@ +#if DEBUG +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using LtiAdvantage.AssignmentGradeServices; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/mocktool")] +[ApiController] +public class MockToolController(IHttpClientFactory httpClientFactory) : ControllerBase +{ + private static readonly RsaSecurityKey SigningKey; + private static readonly ConcurrentDictionary LoginStates = new(); + + private const string ToolIss = "Local Mock Tool"; + private const string ToolNameId = "mock-tool-client-id"; + private const string TargetLinkUriClaim = "https://purl.imsglobal.org/spec/lti/claim/target_link_uri"; + + private record MockTask(string Id, string Title, string Description, int Score); + private static readonly List AvailableTasks = + [ + new MockTask("1", "Integrals (Mock)", "Calculate definite integral", 10), + new MockTask("2", "Derivatives (Mock)", "Find the derivative of a complex function", 5), + new MockTask("3", "Limits (Mock)", "Calculate sequence limit", 8), + new MockTask("4", "Series (Mock)", "Investigate series for convergence", 12), + new MockTask("5", "Diff. Eqs (Mock)", "Solve linear equation", 15) + ]; + + static MockToolController() + { + var rsa = RSA.Create(2048); + const string keyId = "mock-tool-key-id"; + SigningKey = new RsaSecurityKey(rsa) { KeyId = keyId }; + } + + [HttpGet("jwks")] + public IActionResult GetJwks() + { + var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(SigningKey); + return Ok(new { keys = new[] { jwk } }); + } + + [HttpPost("login")] + public IActionResult Login( + [FromForm] string iss, + [FromForm] string login_hint, + [FromForm] string lti_message_hint, + [FromForm(Name = "target_link_uri")] string targetLinkUri) + { + if (string.IsNullOrWhiteSpace(targetLinkUri)) + { + return BadRequest("target_link_uri is required"); + } + + var state = Guid.NewGuid().ToString(); + LoginStates[state] = targetLinkUri; + + var queryParameters = new Dictionary + { + ["client_id"] = ToolNameId, + ["response_type"] = "id_token", + ["redirect_uri"] = "http://localhost:5000/api/mocktool/callback", + ["login_hint"] = login_hint, + ["lti_message_hint"] = lti_message_hint, + ["scope"] = "openid", + ["state"] = state, + ["nonce"] = Guid.NewGuid().ToString() + }; + + var queryString = string.Join("&", queryParameters.Select(parameter => + $"{Uri.EscapeDataString(parameter.Key)}={Uri.EscapeDataString(parameter.Value)}")); + var callbackUrl = $"{iss.TrimEnd('/')}/api/lti/authorize?{queryString}"; + + return Redirect(callbackUrl); + } + + [HttpPost("callback")] + public async Task Callback( + [FromForm] string id_token, + [FromForm] string state) + { + if (!LoginStates.TryRemove(state, out var expectedTargetLinkUri)) + { + return BadRequest("Unknown or already used state"); + } + + var handler = new JwtSecurityTokenHandler(); + if (!handler.CanReadToken(id_token)) return BadRequest("Invalid Token"); + var unverifiedToken = handler.ReadJwtToken(id_token); + + var issuer = unverifiedToken.Issuer; + var platformJwksUrl = $"{issuer}/api/lti/jwks"; + + var client = httpClientFactory.CreateClient(); + string jwksJson; + try { + jwksJson = await client.GetStringAsync(platformJwksUrl); + } catch { + return BadRequest($"Failed to download HwProj keys from {platformJwksUrl}"); + } + + var platformKeySet = new JsonWebKeySet(jwksJson); + + SecurityToken validatedToken; + try { + handler.ValidateToken(id_token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = ToolNameId, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = platformKeySet.Keys + }, out validatedToken); + } catch (Exception ex) { + return Unauthorized($"HwProj signature validation error: {ex.Message}"); + } + + var token = (JwtSecurityToken)validatedToken; + var actualTargetLinkUri = token.Claims.FirstOrDefault(c => c.Type == TargetLinkUriClaim)?.Value; + if (!string.Equals(actualTargetLinkUri, expectedTargetLinkUri, StringComparison.Ordinal)) + { + return Unauthorized("The target_link_uri claim does not match the initial login request"); + } + + var messageType = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/message_type")?.Value; + + return messageType switch + { + "LtiDeepLinkingRequest" => RenderDeepLinkingSelectionUI(token), + "LtiResourceLinkRequest" => HandleResourceLink(token), + _ => BadRequest($"Unknown message type: {messageType}") + }; + } + + private IActionResult RenderDeepLinkingSelectionUI(JwtSecurityToken token) + { + var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); + if (settingsClaim == null) return BadRequest("No deep linking settings found"); + + var settings = JsonDocument.Parse(settingsClaim.Value); + var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); + var dataPayload = settings.RootElement.TryGetProperty("data", out var dataEl) ? dataEl.GetString() : ""; + + var tasksHtml = string.Join("", AvailableTasks.Select(t => $@" +
+ +
")); + + var html = $@" + + +

Select Tasks for HwProj

+
+ + + + {tasksHtml} +
+
+ + "; + + return Content(html, "text/html"); + } + + [HttpPost("submit-selection")] + public IActionResult SubmitDeepLinkingSelection( + [FromForm] List selectedIds, + [FromForm] string returnUrl, + [FromForm] string? data, + [FromForm] string platformIssuer) + { + var selectedTasks = AvailableTasks.Where(t => selectedIds.Contains(t.Id)).ToList(); + + var contentItems = selectedTasks.Select(t => new Dictionary + { + ["type"] = "ltiResourceLink", + ["title"] = t.Title, + ["text"] = t.Description, + + ["lineItem"] = new Dictionary + { + ["scoreMaximum"] = t.Score, + ["label"] = t.Title + }, + + ["custom"] = new Dictionary + { + { "internal_task_id", t.Id } + } + + }).ToList(); + + var payload = new JwtPayload + { + { "iss", ToolNameId }, + { "aud", platformIssuer }, + { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, + { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, + { "nonce", Guid.NewGuid().ToString() }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } + }; + + if (!string.IsNullOrEmpty(data)) + payload.Add("https://purl.imsglobal.org/spec/lti-dl/claim/data", data); + + var credentials = new SigningCredentials(SigningKey, SecurityAlgorithms.RsaSha256); + var header = new JwtHeader(credentials); + var responseToken = new JwtSecurityToken(header, payload); + var responseString = new JwtSecurityTokenHandler().WriteToken(responseToken); + + var html = $@" + + +
+ +
+ + "; + + return Content(html, "text/html"); + } + + private IActionResult HandleResourceLink(JwtSecurityToken token) + { + var presentationClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"); + var presentationJson = JsonDocument.Parse(presentationClaim?.Value ?? "{}"); + var returnUrl = presentationJson.RootElement.TryGetProperty("return_url", out var rProp) ? rProp.GetString() : ""; + + var customClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/custom"); + var customJson = JsonDocument.Parse(customClaim?.Value ?? "{}"); + + string toolTaskId = null; + if (customJson.RootElement.TryGetProperty("internal_task_id", out var idProp)) + { + toolTaskId = idProp.GetString(); + } + + if (string.IsNullOrEmpty(toolTaskId)) + { + var resourceLinkClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/resource_link"); + toolTaskId = JsonDocument.Parse(resourceLinkClaim?.Value ?? "{}").RootElement.GetProperty("id").GetString(); + } + + var currentTask = AvailableTasks.FirstOrDefault(t => t.Id == toolTaskId); + + var scoreToDisplay = currentTask?.Score ?? 0; + var titleToDisplay = currentTask?.Title ?? $"Task ID: {toolTaskId} (Not Found)"; + var descToDisplay = currentTask?.Description ?? "Description not available"; + + var agsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); + var lineItemUrl = JsonDocument.Parse(agsClaim?.Value ?? "{}").RootElement.GetProperty("lineitem").GetString(); + + var html = $@" + + +

Performing: {titleToDisplay}

+

{descToDisplay}

+
+ + + + + + + + +
+ + "; + + return Content(html, "text/html"); + } + + [HttpPost("send-score")] + public async Task SendScore( + [FromForm] string lineItemUrl, [FromForm] string userId, + [FromForm] string platformIss, [FromForm] string taskId, [FromForm] string returnUrl) + { + var currentTask = AvailableTasks.FirstOrDefault(t => t.Id == taskId); + + if (currentTask == null) + { + return BadRequest($"Task with internal ID '{taskId}' not found in the tool database. (Check if DeepLinking passed custom params correctly)"); + } + + var client = httpClientFactory.CreateClient(); + var clientAssertion = CreateClientAssertion(platformIss); + + var tokenRequest = new Dictionary { + ["grant_type"] = "client_credentials", + ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + ["client_assertion"] = clientAssertion, + ["scope"] = "https://purl.imsglobal.org/spec/lti-ags/scope/score" + }; + + var tokenResponse = await client.PostAsync($"{platformIss}/api/lti/token", new FormUrlEncodedContent(tokenRequest)); + if (!tokenResponse.IsSuccessStatusCode) return BadRequest($"Error retrieving token from {platformIss}"); + + var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); + var accessToken = JsonDocument.Parse(tokenContent).RootElement.GetProperty("access_token").GetString(); + + var scoreObj = new Score { + UserId = userId, + ScoreGiven = currentTask.Score, + ScoreMaximum = currentTask.Score, + Comment = $"Excellent! Task '{currentTask.Title}' completed.", + GradingProgress = GradingProgress.FullyGraded, + ActivityProgress = ActivityProgress.Completed, + TimeStamp = DateTime.UtcNow + }; + + var scoreRequest = new HttpRequestMessage(HttpMethod.Post, $"{lineItemUrl}/scores") { + Content = new StringContent(JsonSerializer.Serialize(scoreObj), System.Text.Encoding.UTF8, "application/vnd.ims.lti-ags.v1.score+json") + }; + scoreRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var scoreResponse = await client.SendAsync(scoreRequest); + + var statusColor = scoreResponse.IsSuccessStatusCode ? "green" : "red"; + var statusText = scoreResponse.IsSuccessStatusCode + ? $"Score of {currentTask.Score} successfully submitted!" + : $"Error submitting score: {scoreResponse.StatusCode}"; + + var html = $@" + + + + + + +
+

{statusText}

+

You will be redirected back to HwProj in 3 seconds...

+ Return Now +
+ + "; + + return Content(html, "text/html"); + } + + private static string CreateClientAssertion(string platformIssuer) + { + var claims = new List { + new(JwtRegisteredClaimNames.Iss, ToolNameId), + new(JwtRegisteredClaimNames.Aud, $"{platformIssuer}/api/lti/token"), + new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var jwt = new JwtSecurityToken( + header: new JwtHeader(new SigningCredentials(SigningKey, SecurityAlgorithms.RsaSha256)), + payload: new JwtPayload(claims) + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } +} +#endif diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs new file mode 100644 index 000000000..738d66eaa --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace HwProj.APIGateway.API.Lti.DTOs; + +public record AuthorizePostFormDto( + string ActionUrl, + string Method, + Dictionary Fields); \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs new file mode 100644 index 000000000..02e384b1e --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs @@ -0,0 +1,10 @@ +namespace HwProj.APIGateway.API.Lti.DTOs; + +public record LtiToolDto( + string Name, + string issuer, + string ClientId, + string JwksEndpoint, + string InitiateLoginUri, + string LaunchUrl, + string DeepLink); \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs new file mode 100644 index 000000000..43d64d397 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs @@ -0,0 +1,20 @@ +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.DTOs; + +namespace HwProj.APIGateway.API.Lti.Mappings; + +public static class LtiToolMapper +{ + public static LtiToolDto LtiToolConfigToDto(this LtiToolConfig t) + { + return new LtiToolDto( + t.Name, + t.Issuer, + t.ClientId, + t.JwksEndpoint, + t.InitiateLoginUri, + t.LaunchUrl, + t.DeepLink + ); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs new file mode 100644 index 000000000..2570abbe7 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Services; + +public interface ILtiKeyService +{ + Task?> GetKeysAsync(string jwksUrl); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs new file mode 100644 index 000000000..6c89bcc61 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -0,0 +1,28 @@ +using System.Security.Claims; +using System.Threading.Tasks; + +namespace HwProj.APIGateway.API.LTI.Services; + +public interface ILtiTokenService +{ + public string CreateDeepLinkingToken( + string clientId, + string courseId, + string targetLinkUri, + string userId, + string nonce); + + public string CreateResourceLinkToken( + string clientId, + string courseId, + string targetLinkUri, + string? ltiCustomParams, + string userId, + string nonce, + string resourceLinkId); + + public string GenerateAccessTokenForLti( + string clientId, + string botId, + string scope); +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs new file mode 100644 index 000000000..a5e79f979 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.DTOs; + +namespace HwProj.APIGateway.API.Lti.Services; + +public interface ILtiToolService +{ + IReadOnlyList GetAll(); + LtiToolDto? GetByName(string name); + LtiToolDto? GetByIssuer(string issuer); + LtiToolDto? GetByClientId(string clientId); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs new file mode 100644 index 000000000..ba9a9d5f7 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Caching.Memory; +using System.Net.Http; +using System.Threading.Tasks; +using System.Collections.Generic; +using HwProj.APIGateway.API.Lti.Services; + +public class LtiKeyService(IHttpClientFactory httpClientFactory, IMemoryCache keycMemoryCache) : ILtiKeyService +{ + + private const int ageByDefault = 24; + + public async Task?> GetKeysAsync(string jwksUrl) + { + if (string.IsNullOrEmpty(jwksUrl)) + { + return null; + } + + if (keycMemoryCache.TryGetValue(jwksUrl, out JsonWebKeySet? keySet)) + { + return keySet?.Keys; + } + + try + { + var client = httpClientFactory.CreateClient(); + client.Timeout = TimeSpan.FromSeconds(30); + + using var response = await client.GetAsync(jwksUrl); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + keySet = new JsonWebKeySet(json); + + var cacheControl = response.Headers.CacheControl; + if (cacheControl?.NoCache == true || + cacheControl?.NoStore == true || + cacheControl?.Private == true) + { + return keySet.Keys; + } + + var cacheDuration = TimeSpan.FromHours(ageByDefault); + + if (cacheControl?.MaxAge.HasValue == true) + { + cacheDuration = cacheControl.MaxAge.Value; + } + + var cacheOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(cacheDuration) + .SetPriority(CacheItemPriority.High); + + keycMemoryCache.Set(jwksUrl, keySet, cacheOptions); + + return keySet.Keys; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs new file mode 100644 index 000000000..1f02c83e5 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Json; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.LTI.Services; +using LtiAdvantage.DeepLinking; +using LtiAdvantage.Lti; +using LtiAdvantage.AssignmentGradeServices; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Services; + +public class LtiTokenService(IOptions options) : ILtiTokenService +{ + private readonly LtiPlatformConfig _options = options.Value; + + public string CreateDeepLinkingToken( + string clientId, + string courseId, + string targetLinkUri, + string userId, + string nonce) + { + var request = new LtiDeepLinkingRequest + { + DeploymentId = clientId, + Nonce = nonce, + UserId = userId, + TargetLinkUri = targetLinkUri, + Roles = [Role.ContextInstructor, Role.InstitutionInstructor], + + Context = new ContextClaimValueType + { + Id = courseId + }, + + DeepLinkingSettings = new DeepLinkingSettingsClaimValueType + { + AutoCreate = true, + AcceptMultiple = true, + AcceptTypes = ["ltiResourceLink"], + AcceptPresentationDocumentTargets = [DocumentTarget.Window], + + DeepLinkReturnUrl = this._options.DeepLinkReturnUrl, + } + }; + + return this.CreateJwt(clientId, request); + } + + public string CreateResourceLinkToken( + string clientId, + string courseId, + string targetLinkUri, + string? ltiCustomParams, + string userId, + string nonce, + string resourceLinkId) + { + var request = new LtiResourceLinkRequest + { + DeploymentId = clientId, + Nonce = nonce, + UserId = userId, + TargetLinkUri = targetLinkUri, + + Roles = [Role.ContextLearner, Role.InstitutionStudent], + + Context = new ContextClaimValueType + { + Id = courseId + }, + + ResourceLink = new ResourceLinkClaimValueType + { + Id = resourceLinkId + }, + + LaunchPresentation = new LaunchPresentationClaimValueType + { + DocumentTarget = DocumentTarget.Window, + ReturnUrl = _options.ResourceLinkReturnUrl, + }, + + AssignmentGradeServices = new AssignmentGradeServicesClaimValueType + { + Scope = ["https://purl.imsglobal.org/spec/lti-ags/scope/score"], + LineItemUrl = _options.AssignmentsGradesEndpoint + "/" + resourceLinkId, + } + }; + + if (string.IsNullOrEmpty(ltiCustomParams)) + { + request.Custom = new Dictionary(); + return this.CreateJwt(clientId, request); + } + + try + { + request.Custom = JsonSerializer.Deserialize>(ltiCustomParams); + } + catch (JsonException) + { + request.Custom = new Dictionary(); + } + + return this.CreateJwt(clientId, request); + } + + public string GenerateAccessTokenForLti(string clientId, string botId, string scope) + { + var now = DateTime.UtcNow; + + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, clientId), + + new Claim("_id", botId), + + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + + new Claim("scope", scope) + }; + + var jwt = new JwtSecurityToken( + issuer: _options.Issuer, + audience: _options.Issuer, + claims: claims, + notBefore: now, + expires: now.AddHours(1), + signingCredentials: GetSigningCredentials() + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } + + + private SigningCredentials GetSigningCredentials() + { + var keyConfig = _options.SigningKey; + + var rsa = RSA.Create(); + + rsa.ImportFromPem(keyConfig.PrivateKeyPem); + + var securityKey = new RsaSecurityKey(rsa) + { + KeyId = keyConfig.KeyId + }; + + return new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256); + } + + private string CreateJwt(string clientId, LtiRequest request) + { + var now = DateTime.UtcNow; + var jwt = new JwtSecurityToken( + issuer: this._options.Issuer, + audience: clientId, + claims: request.IssuedClaims, + notBefore: now, + expires: now.AddMinutes(5), + signingCredentials: GetSigningCredentials() + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } +} diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs new file mode 100644 index 000000000..b1c1c8354 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.DTOs; +using HwProj.APIGateway.API.Lti.Mappings; +using Microsoft.Extensions.Options; + +namespace HwProj.APIGateway.API.Lti.Services; + +public class LtiToolService(IOptions> options) : ILtiToolService +{ + private readonly IReadOnlyList _tools = (options.Value ?? []).AsReadOnly(); + + public IReadOnlyList GetAll() + => _tools + .Select(LtiToolMapper.LtiToolConfigToDto) + .ToList() + .AsReadOnly(); + + public LtiToolDto? GetByName(string name) + => _tools.FirstOrDefault(t => t.Name == name)?.LtiToolConfigToDto(); + + public LtiToolDto? GetByIssuer(string issuer) + => _tools.FirstOrDefault(t => t.Issuer == issuer)?.LtiToolConfigToDto(); + + public LtiToolDto? GetByClientId(string clientId) + => _tools.FirstOrDefault(t => t.ClientId == clientId)?.LtiToolConfigToDto(); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs index 7f79d2cd2..a964901d6 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs @@ -32,6 +32,7 @@ public class TaskSolutionStatisticsPageData public class UserTaskSolutionsPageData { public long CourseId { get; set; } + public string? LtiToolName { get; set; } public AccountDataDto[] CourseMates { get; set; } public HomeworksGroupUserTaskSolutions[] TaskSolutions { get; set; } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index ef7c604ad..5ebde98ae 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -1,7 +1,11 @@ using System.Collections.Generic; +using System.Security.Cryptography; using System.Text; using System.Text.Json.Serialization; using HwProj.APIGateway.API.Filters; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.Services; +using HwProj.APIGateway.API.LTI.Services; using HwProj.AuthService.Client; using HwProj.ContentService.Client; using HwProj.CoursesService.Client; @@ -68,8 +72,30 @@ public void ConfigureServices(IServiceCollection services) new SymmetricSecurityKey(Encoding.ASCII.GetBytes(appSettings["SecurityKey"])), ValidateIssuerSigningKey = true }; + }) + .AddJwtBearer("LtiScheme", options => + { + var ltiConfig = Configuration.GetSection("LtiPlatform").Get(); + if (ltiConfig == null) return; + + var rsa = RSA.Create(); + + rsa.ImportFromPem(ltiConfig.SigningKey.PrivateKeyPem); + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = ltiConfig.Issuer, + ValidateAudience = true, + ValidAudience = ltiConfig.Issuer, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + IssuerSigningKeys = [new RsaSecurityKey(rsa)] + }; }); + services.AddMemoryCache(); services.AddHttpClient(); services.AddHttpContextAccessor(); @@ -79,6 +105,12 @@ public void ConfigureServices(IServiceCollection services) services.AddNotificationsServiceClient(); services.AddContentServiceClient(); + services.Configure(Configuration.GetSection("LtiPlatform")); + services.Configure>(Configuration.GetSection("LtiTools")); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json index b3e146a09..ece890536 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json +++ b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json @@ -15,5 +15,29 @@ "LdapHost": "ad.pu.ru", "LdapPort": 389, "SearchBase": "DC=ad,DC=pu,DC=ru" - } + }, + "LtiPlatform": { + "Issuer": "http://localhost:5000", + "OidcAuthorizationEndpoint": "http://localhost:5000/api/lti/authorize", + "DeepLinkReturnUrl": "http://localhost:5000/api/lti/deepLinkReturn", + "JwksEndpoint": "http://localhost:5000/api/lti/jwks", + "ResourceLinkReturnUrl": "http://localhost:5000/api/lti/closeLtiSession", + "AssignmentsGradesEndpoint": "http://localhost:5000/api/lti/lineItem", + "AccessTokenUrl": "http://localhost:5000/api/lti/token", + "SigningKey": { + "KeyId": "", + "PrivateKeyPem": "" + } + }, + "LtiTools": [ + { + "name": "Local Mock Tool", + "Issuer": "Local Mock Tool", + "clientId": "mock-tool-client-id", + "JwksEndpoint": "http://localhost:5000/api/mocktool/jwks", + "initiateLoginUri": "http://localhost:5000/api/mocktool/login", + "launchUrl": "http://localhost:5000/api/mocktool/callback", + "deepLinking": "http://localhost:5000/api/mocktool/callback" + } + ] } diff --git a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs index 06e2bf26f..c46b74375 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs @@ -139,6 +139,14 @@ public async Task GetAllLecturers() return Ok(result); } + [HttpPost("getOrCreateLtiBot")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task GetOrCreateLtiBot([FromBody] string toolClientId) + { + var result = await _accountService.GetOrCreateLtiBot(toolClientId); + return Ok(result); + } + [HttpPost("requestPasswordRecovery")] public async Task RequestPasswordRecovery(RequestPasswordRecoveryViewModel model) { diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs index 706886b30..e7befc6ae 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using System.Linq; using System.Net.Http; +using System.Security.Cryptography; +using System.Text; using System.Web; using AutoMapper; using HwProj.AuthService.API.Extensions; @@ -225,6 +227,76 @@ public async Task> GetUsersInRole(string role) return await _userManager.GetUsersInRoleAsync(role); } + public async Task> GetOrCreateLtiBot(string toolClientId) + { + if (string.IsNullOrWhiteSpace(toolClientId)) + { + return Result.Failed("ClientId LTI-инструмента не указан"); + } + + var botId = GetLtiBotId(toolClientId); + var botEmail = $"{botId}@system.local"; + var bot = await _aspUserManager.FindByIdAsync(botId); + + if (bot != null) + { + if (!string.Equals(bot.Email, botEmail, StringComparison.OrdinalIgnoreCase)) + { + return Result.Failed( + $"Идентификатор LTI-бота {botId} уже занят другим пользователем"); + } + } + else + { + var userWithBotEmail = await _aspUserManager.FindByEmailAsync(botEmail); + if (userWithBotEmail != null) + { + return Result.Failed( + $"Почта LTI-бота {botEmail} уже принадлежит пользователю с другим идентификатором"); + } + + bot = new User + { + Id = botId, + UserName = botEmail, + Email = botEmail, + EmailConfirmed = true, + Name = "LTI Bot", + Surname = "", + MiddleName = "", + IsExternalAuth = false, + }; + + var createResult = await _aspUserManager.CreateAsync(bot); + if (!createResult.Succeeded) + { + var concurrentlyCreatedBot = await _aspUserManager.FindByIdAsync(botId); + if (concurrentlyCreatedBot == null || + !string.Equals(concurrentlyCreatedBot.Email, botEmail, + StringComparison.OrdinalIgnoreCase)) + { + return Result.Failed( + createResult.Errors.Select(error => error.Description).ToArray()); + } + + bot = concurrentlyCreatedBot; + } + } + + var isExpert = await _aspUserManager.IsInRoleAsync(bot, Roles.ExpertRole); + if (!isExpert) + { + var roleResult = await _userManager.AddToRoleAsync(bot, Roles.ExpertRole); + if (roleResult.Succeeded || await _aspUserManager.IsInRoleAsync(bot, Roles.ExpertRole)) + return Result.Success(bot.ToAccountDataDto(Roles.ExpertRole)); + + return Result.Failed( + roleResult.Errors.Select(error => error.Description).ToArray()); + } + + return Result.Success(bot.ToAccountDataDto(Roles.ExpertRole)); + } + public async Task RequestPasswordRecovery(RequestPasswordRecoveryViewModel model) { var user = await _aspUserManager.FindByEmailAsync(model.Email); @@ -377,5 +449,24 @@ private async Task> GetToken(User user) { return Result.Success(await _tokenService.GetTokenAsync(user).ConfigureAwait(false)); } + + private static string GetLtiBotId(string toolClientId) + { + if (string.IsNullOrWhiteSpace(toolClientId)) + { + throw new ArgumentException( + "ClientId LTI-инструмента не указан", + nameof(toolClientId)); + } + + var source = $"hwproj:lti-bot:{toolClientId.Trim()}"; + var bytes = Encoding.UTF8.GetBytes(source); + var hash = SHA256.HashData(bytes); + var hashString = Convert + .ToHexString(hash) + .ToLowerInvariant(); + + return $"lti-bot-{hashString}"; + } } } diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs index ca7d3c1d4..dd93b43ad 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs @@ -19,6 +19,7 @@ public interface IAccountService Task> RefreshToken(string userId); Task InviteNewLecturer(string emailOfInvitedUser); Task> GetUsersInRole(string role); + Task> GetOrCreateLtiBot(string toolClientId); Task RequestPasswordRecovery(RequestPasswordRecoveryViewModel model); Task ResetPassword(ResetPasswordViewModel model); Task AuthorizeGithub(string code, string userId); diff --git a/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs b/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs index bf2d29112..9b3b515d9 100644 --- a/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs +++ b/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs @@ -187,6 +187,25 @@ public async Task GetAllLecturers() return await response.DeserializeAsync().ConfigureAwait(false); } + public async Task> GetOrCreateLtiBot(string toolClientId) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + _authServiceUri + "api/account/getOrCreateLtiBot"); + httpRequest.Content = new StringContent( + JsonConvert.SerializeObject(toolClientId), + Encoding.UTF8, + "application/json"); + + var response = await _httpClient.SendAsync(httpRequest); + if (!response.IsSuccessStatusCode) + { + return Result.Failed(response.ReasonPhrase); + } + + return await response.DeserializeAsync>(); + } + public async Task Ping() { try diff --git a/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs b/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs index 63d678991..fb52ec3b7 100644 --- a/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs +++ b/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs @@ -19,6 +19,7 @@ public interface IAuthServiceClient Task FindByEmailAsync(string email); Task GetAllStudents(); Task GetAllLecturers(); + Task> GetOrCreateLtiBot(string toolClientId); Task Ping(); Task RequestPasswordRecovery(RequestPasswordRecoveryViewModel model); Task ResetPassword(ResetPasswordViewModel model); diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs index d61937109..0551d5adb 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs @@ -18,6 +18,7 @@ public class CreateCourseViewModel public bool FetchStudents { get; set; } [Required] public bool IsOpen { get; set; } public long? BaseCourseId { get; set; } + public string? LtiToolName { get; set; } } public class UpdateCourseViewModel @@ -31,6 +32,7 @@ public class UpdateCourseViewModel [Required] public bool IsOpen { get; set; } public bool IsCompleted { get; set; } + public string? LtiToolName { get; set; } } public class CourseDTO : CoursePreview @@ -42,6 +44,7 @@ public class CourseDTO : CoursePreview public GroupViewModel[] Groups { get; set; } = Array.Empty(); public IEnumerable AcceptedStudents => CourseMates.Where(t => t.IsAccepted); public IEnumerable NewStudents => CourseMates.Where(t => !t.IsAccepted); + public string? LtiToolName { get; set; } } public class CourseViewModel @@ -51,6 +54,7 @@ public class CourseViewModel public string GroupName { get; set; } public bool IsOpen { get; set; } public bool IsCompleted { get; set; } + public string? LtiToolName { get; set; } public GroupViewModel[] Groups { get; set; } public AccountDataDto[] Mentors { get; set; } diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs index f2cc40854..9e94729d6 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs @@ -31,6 +31,8 @@ public class HomeworkTaskViewModel public bool DeadlineDateNotSet { get; set; } + public long CourseId { get; set; } + public long HomeworkId { get; set; } public bool IsGroupWork { get; set; } @@ -38,6 +40,9 @@ public class HomeworkTaskViewModel public bool IsDeferred { get; set; } public List? Criteria { get; set; } = new List(); + + [JsonProperty] + public LtiLaunchData? LtiLaunchData { get; set; } } public class HomeworkTaskForEditingViewModel @@ -70,5 +75,13 @@ public class PostTaskViewModel public ActionOptions? ActionOptions { get; set; } public List Criteria { get; set; } + + public LtiLaunchData? LtiLaunchData { get; set; } + } + + public class LtiLaunchData + { + public string LtiLaunchUrl { get; set; } + public string? CustomParams { get; set; } } } diff --git a/HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json b/HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json new file mode 100644 index 000000000..97e58f831 --- /dev/null +++ b/HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "LocalStorageConfiguration": { + "Path": "ContentFiles" + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index 97b6eef64..43f80e196 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -89,7 +89,7 @@ public async Task GetAllData(long courseId) public async Task GetByTask(long taskId) { var userId = Request.GetUserIdFromHeader(); - var course = await _coursesService.GetByTaskAsync(taskId, userId); + var course = await _coursesService.GetByTaskAsync(taskId, userId!); if (course == null) return NotFound(); return Ok(course); @@ -121,7 +121,8 @@ public async Task UpdateCourse(long courseId, [FromBody] UpdateCo Name = courseViewModel.Name, GroupName = courseViewModel.GroupName, IsCompleted = courseViewModel.IsCompleted, - IsOpen = courseViewModel.IsOpen + IsOpen = courseViewModel.IsOpen, + LtiToolName = courseViewModel.LtiToolName }); return Ok(); @@ -177,9 +178,9 @@ public async Task GetUserCourses(string role) [HttpGet("acceptLecturer/{courseId}")] [ServiceFilter(typeof(CourseMentorOnlyAttribute))] public async Task AcceptLecturer(long courseId, [FromQuery] string lecturerEmail, - [FromQuery] string lecturerId) + [FromQuery] string lecturerId, [FromQuery] bool sendNotification = true) { - await _coursesService.AcceptLecturerAsync(courseId, lecturerEmail, lecturerId); + await _coursesService.AcceptLecturerAsync(courseId, lecturerEmail, lecturerId, sendNotification); return Ok(); } @@ -256,5 +257,6 @@ public async Task GetMentorsToAssignedStudents(long courseId) var mentorsToAssignedStudents = await _courseFilterService.GetAssignedStudentsIds(courseId, mentorIds); return Ok(mentorsToAssignedStudents); } + } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs index dbbdb1ba6..fbcb6ff68 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs @@ -27,26 +27,19 @@ public async Task AddHomework(long courseId, var validationResult = Validator.ValidateHomework(homeworkViewModel); if (validationResult.Any()) return BadRequest(validationResult); - var newHomework = await _homeworksService.AddHomeworkAsync(courseId, homeworkViewModel); - return Ok(newHomework.ToHomeworkViewModel()); + var responseViewModel = await _homeworksService.AddHomeworkAsync(courseId, homeworkViewModel); + + return Ok(responseViewModel); } [HttpGet("get/{homeworkId}")] public async Task GetHomework(long homeworkId) - { - var homeworkFromDb = await _homeworksService.GetHomeworkAsync(homeworkId); - var homework = homeworkFromDb.ToHomeworkViewModel(); - return homework; - } + => await _homeworksService.GetHomeworkAsync(homeworkId); [HttpGet("getForEditing/{homeworkId}")] [ServiceFilter(typeof(CourseMentorOnlyAttribute))] public async Task GetForEditingHomework(long homeworkId) - { - var homeworkFromDb = await _homeworksService.GetForEditingHomeworkAsync(homeworkId); - var homework = homeworkFromDb.ToHomeworkViewModel(); - return homework; - } + => await _homeworksService.GetForEditingHomeworkAsync(homeworkId); [HttpDelete("delete/{homeworkId}")] [ServiceFilter(typeof(CourseMentorOnlyAttribute))] @@ -61,11 +54,13 @@ public async Task UpdateHomework(long homeworkId, [FromBody] CreateHomeworkViewModel homeworkViewModel) { var homework = await _homeworksService.GetForEditingHomeworkAsync(homeworkId); - var validationResult = Validator.ValidateHomework(homeworkViewModel, homework); + var validationResult = Validator.ValidateHomework(homeworkViewModel, + homework); if (validationResult.Any()) return BadRequest(validationResult); var updatedHomework = await _homeworksService.UpdateHomeworkAsync(homeworkId, homeworkViewModel); - return Ok(updatedHomework.ToHomeworkViewModel()); + + return Ok(updatedHomework); } } -} +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs index 6d59f1b13..847437515 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs @@ -44,7 +44,7 @@ public async Task GetTask(long taskId, [FromQuery] bool withCrite var lecturers = await _coursesService.GetCourseLecturers(homework.CourseId); if (!lecturers.Contains(userId)) return BadRequest(); } - return Ok(task.ToHomeworkTaskViewModel()); + return Ok(task); } [HttpGet("getForEditing/{taskId}")] @@ -94,7 +94,7 @@ public async Task UpdateTask(long taskId, [FromBody] PostTaskView var updatedTask = await _tasksService.UpdateTaskAsync(taskId, taskViewModel, taskViewModel.ActionOptions ?? ActionOptions.Default); - return Ok(updatedTask.ToHomeworkTaskViewModel()); + return Ok(updatedTask); } [HttpPost("addQuestion")] @@ -107,7 +107,7 @@ public async Task AddQuestionForTask([FromBody] AddTaskQuestionDt if (string.IsNullOrEmpty(question.Text)) return BadRequest("Текст вопроса пуст"); - if (!await _coursesService.HasStudent(task.Homework.CourseId, studentId)) + if (!await _coursesService.HasStudent(task.CourseId, studentId)) return Forbid(); await _taskQuestionsService.AddQuestionAsync(new TaskQuestion @@ -117,6 +117,7 @@ await _taskQuestionsService.AddQuestionAsync(new TaskQuestion Text = question.Text, IsPrivate = question.IsPrivate, }); + return Ok(); } @@ -127,7 +128,7 @@ public async Task GetQuestionsForTask(long taskId) var task = await _tasksService.GetTaskAsync(taskId); if (userId == null || task == null) return NotFound(); - var courseId = task.Homework.CourseId; + var courseId = task.CourseId; var isLecturer = (await _coursesService.GetCourseLecturers(courseId)).Contains(userId); var isStudent = await _coursesService.HasStudent(courseId, userId); if (!isLecturer && !isStudent) @@ -146,6 +147,7 @@ public async Task GetQuestionsForTask(long taskId) IsPrivate = x.IsPrivate, LecturerId = x.LecturerId }); + return Ok(result); } @@ -196,7 +198,7 @@ public async Task AddAnswerForQuestion(AddAnswerForQuestionDto an var task = await _tasksService.GetTaskAsync(question.TaskId); if (task == null) return NotFound(); - var courseId = task.Homework.CourseId; + var courseId = task.CourseId; var isLecturer = (await _coursesService.GetCourseLecturers(courseId)).Contains(lecturerId); if (!isLecturer) return Forbid(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index 48ce7a8ea..3a0443405 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -57,6 +57,7 @@ public static HomeworkTaskViewModel ToHomeworkTaskViewModel(this HomeworkTask ta DeadlineDateNotSet = task.DeadlineDate == null || task.DeadlineDate == DateToOverride, IsDeferred = DateTime.UtcNow < evaluatedPublicationDate, IsGroupWork = tags.Contains(HomeworkTags.GroupWork), + CourseId = task.Homework.CourseId, HomeworkId = task.HomeworkId, Tags = tags, Criteria = task.Criteria.Select(c => new CriterionViewModel @@ -104,6 +105,7 @@ public static CourseDTO ToCourseDto(this Course course) InviteCode = course.InviteCode, CourseMates = course.CourseMates.Select(cm => cm.ToCourseMateViewModel()).ToArray(), Homeworks = course.Homeworks.Select(h => h.ToHomeworkViewModel()).ToArray(), + LtiToolName = course.LtiToolName, }; public static CoursePreview ToCoursePreview(this Course course) @@ -159,6 +161,7 @@ public static CourseTemplate ToCourseTemplate(this CreateCourseViewModel createC Name = createCourseViewModel.Name, GroupName = string.Join(", ", createCourseViewModel.GroupNames), IsOpen = createCourseViewModel.IsOpen, + LtiToolName = createCourseViewModel.LtiToolName, }; public static CourseTemplate ToCourseTemplate(this Course course) @@ -168,6 +171,7 @@ public static CourseTemplate ToCourseTemplate(this Course course) GroupName = course.GroupName, IsOpen = course.IsOpen, Homeworks = course.Homeworks.Select(h => h.ToHomeworkTemplate()).ToList(), + LtiToolName = course.LtiToolName, }; public static HomeworkTemplate ToHomeworkTemplate(this Homework homework) @@ -191,7 +195,7 @@ public static HomeworkTaskTemplate ToHomeworkTaskTemplate(this HomeworkTask task IsDeadlineStrict = task.IsDeadlineStrict, HasSpecialPublicationDate = task.PublicationDate != null, HasSpecialDeadlineDate = task.DeadlineDate != null, - IsBonusExplicit = task.IsBonusExplicit + IsBonusExplicit = task.IsBonusExplicit, }; public static Course ToCourse(this CourseTemplate courseTemplate) @@ -200,6 +204,7 @@ public static Course ToCourse(this CourseTemplate courseTemplate) Name = courseTemplate.Name, GroupName = courseTemplate.GroupName, IsOpen = courseTemplate.IsOpen, + LtiToolName = courseTemplate.LtiToolName, }; public static Homework ToHomework(this HomeworkTemplate homeworkTemplate, long courseId) @@ -227,5 +232,23 @@ public static HomeworkTask ToHomeworkTask(this HomeworkTaskTemplate taskTemplate DeadlineDate = taskTemplate.HasSpecialDeadlineDate ? DateToOverride : (DateTime?)null, IsBonusExplicit = taskTemplate.IsBonusExplicit, }; + + public static LtiLaunchData? ToLtiLaunchData( + this HwProj.Models.CoursesService.ViewModels.LtiLaunchData? ltiLaunchData) + => ltiLaunchData == null ? null : + new LtiLaunchData + { + LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, + CustomParams = ltiLaunchData.CustomParams + }; + + public static HwProj.Models.CoursesService.ViewModels.LtiLaunchData? ToLtiLaunchData( + this LtiLaunchData? ltiLaunchData) + => ltiLaunchData == null ? null : + new HwProj.Models.CoursesService.ViewModels.LtiLaunchData + { + LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, + CustomParams = ltiLaunchData.CustomParams + }; } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs index 6cade85ea..b64305bb6 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs @@ -8,7 +8,7 @@ namespace HwProj.CoursesService.API.Domains { public static class Validator { - public static List ValidateTask(PostTaskViewModel task, Homework homework, + public static List ValidateTask(PostTaskViewModel task, HomeworkViewModel homework, HomeworkTask? previousState = null) { var errors = new List(); @@ -72,11 +72,19 @@ public static List ValidateTask(PostTaskViewModel task, Homework homewor return errors; } - public static List ValidateHomework(CreateHomeworkViewModel homework, Homework? previousState = null) + public static List ValidateHomework(CreateHomeworkViewModel homework, HomeworkViewModel? previousState = null) { var errors = new List(); - homework.Tasks.ForEach(task => errors.AddRange(ValidateTask(task, homework.ToHomework()))); + var homeworkContext = new HomeworkViewModel() + { + PublicationDate = homework.PublicationDate, + DeadlineDate = homework.DeadlineDate, + HasDeadline = homework.HasDeadline, + IsDeadlineStrict = homework.IsDeadlineStrict + }; + + homework.Tasks.ForEach(task => errors.AddRange(ValidateTask(task, homeworkContext))); if (homework.HasDeadline == false && homework.DeadlineDate != null) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseMentorOnlyAttribute.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseMentorOnlyAttribute.cs index 4e6e7254b..94331c78b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseMentorOnlyAttribute.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseMentorOnlyAttribute.cs @@ -44,7 +44,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context else if (routeData.Values.TryGetValue("taskId", out var taskId)) { var task = await _taskService.GetTaskAsync(long.Parse(taskId.ToString())); - mentorIds = await _coursesService.GetCourseLecturers(task.Homework.CourseId); + mentorIds = await _coursesService.GetCourseLecturers(task.CourseId); } if (mentorIds != null && !mentorIds.Contains(userId.ToString())) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs deleted file mode 100644 index 94831cdd7..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs +++ /dev/null @@ -1,214 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240104183735_HomeworkCommonProperties")] - partial class HomeworkCommonProperties - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs deleted file mode 100644 index eec4c39bd..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class HomeworkCommonProperties : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "Date", - table: "Homeworks", - newName: "PublicationDate"); - - migrationBuilder.AlterColumn( - name: "PublicationDate", - table: "Tasks", - nullable: true, - oldClrType: typeof(DateTime)); - - migrationBuilder.AlterColumn( - name: "IsDeadlineStrict", - table: "Tasks", - nullable: true, - oldClrType: typeof(bool)); - - migrationBuilder.AlterColumn( - name: "HasDeadline", - table: "Tasks", - nullable: true, - oldClrType: typeof(bool)); - - migrationBuilder.AddColumn( - name: "DeadlineDate", - table: "Homeworks", - nullable: true); - - migrationBuilder.AddColumn( - name: "HasDeadline", - table: "Homeworks", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "IsDeadlineStrict", - table: "Homeworks", - nullable: false, - defaultValue: false); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs deleted file mode 100644 index 9ad925339..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs +++ /dev/null @@ -1,241 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240108203028_Assignments")] - partial class Assignments - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs deleted file mode 100644 index 5ae583675..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class Assignments : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Assignments", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - CourseId = table.Column(nullable: false), - MentorId = table.Column(nullable: false), - StudentId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Assignments", x => x.Id); - table.ForeignKey( - name: "FK_Assignments_Courses_CourseId", - column: x => x.CourseId, - principalTable: "Courses", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Assignments_CourseId", - table: "Assignments", - column: "CourseId"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs deleted file mode 100644 index 5fd43694a..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs +++ /dev/null @@ -1,243 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240209220217_IsGroupWork")] - partial class IsGroupWork - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("IsGroupWork"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs deleted file mode 100644 index c698d63d6..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class IsGroupWork : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsGroupWork", - table: "Homeworks", - nullable: false, - defaultValue: false); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs deleted file mode 100644 index 00c1bbf88..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs +++ /dev/null @@ -1,245 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240408124740_AddTagsToHomework")] - partial class AddTagsToHomework - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("IsGroupWork"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs deleted file mode 100644 index dac7516bd..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class AddTagsToHomework : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Tags", - table: "Homeworks", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Tags", - table: "Homeworks"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs deleted file mode 100644 index 1d2a123cc..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs +++ /dev/null @@ -1,243 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240413143547_DeleteIsGroupWork")] - partial class DeleteIsGroupWork - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs deleted file mode 100644 index f09bdd8e5..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class DeleteIsGroupWork : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("UPDATE Homeworks SET Tags = Tags + ';Командная работа' WHERE IsGroupWork = 1 AND Tags != null"); - migrationBuilder.Sql("UPDATE Homeworks SET Tags = 'Командная работа' WHERE IsGroupWork = 1 AND Tags = null"); - - migrationBuilder.DropColumn( - name: "IsGroupWork", - table: "Homeworks"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsGroupWork", - table: "Homeworks", - nullable: false, - defaultValue: false); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs deleted file mode 100644 index 4fff1b41c..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs +++ /dev/null @@ -1,279 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240911013242_CreateCourseFilterTables")] - partial class CreateCourseFilterTables - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs deleted file mode 100644 index be351c013..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class CreateCourseFilterTables : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CourseFilters", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - FilterJson = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CourseFilters", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "UserToCourseFilters", - columns: table => new - { - CourseId = table.Column(nullable: false), - UserId = table.Column(nullable: false), - CourseFilterId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserToCourseFilters", x => new { x.CourseId, x.UserId }); - table.ForeignKey( - name: "FK_UserToCourseFilters_CourseFilters_CourseFilterId", - column: x => x.CourseFilterId, - principalTable: "CourseFilters", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_UserToCourseFilters_CourseFilterId", - table: "UserToCourseFilters", - column: "CourseFilterId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "UserToCourseFilters"); - - migrationBuilder.DropTable( - name: "CourseFilters"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs deleted file mode 100644 index 25de66053..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs +++ /dev/null @@ -1,306 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20241110212839_TaskQuestions")] - partial class TaskQuestions - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs deleted file mode 100644 index 1f7c21fe1..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class TaskQuestions : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Questions", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - TaskId = table.Column(nullable: false), - StudentId = table.Column(nullable: true), - Text = table.Column(maxLength: 1000, nullable: true), - IsPrivate = table.Column(nullable: false), - LecturerId = table.Column(nullable: true), - Answer = table.Column(maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Questions", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Questions_TaskId", - table: "Questions", - column: "TaskId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Questions"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs deleted file mode 100644 index 66c42ced8..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs +++ /dev/null @@ -1,327 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20250420184715_'StudentCharacteristics'")] - partial class StudentCharacteristics - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs deleted file mode 100644 index 2ae866cbf..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class StudentCharacteristics : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "StudentCharacteristics", - columns: table => new - { - CourseMateId = table.Column(nullable: false), - Tags = table.Column(nullable: true), - Description = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_StudentCharacteristics", x => x.CourseMateId); - table.ForeignKey( - name: "FK_StudentCharacteristics_CourseMates_CourseMateId", - column: x => x.CourseMateId, - principalTable: "CourseMates", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "StudentCharacteristics"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs deleted file mode 100644 index 2e1d1382d..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs +++ /dev/null @@ -1,356 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20251230213439_Criteria")] - partial class Criteria - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("MaxPoints"); - - b.Property("Name"); - - b.Property("TaskId"); - - b.Property("Type"); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Criteria"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") - .WithMany("Criteria") - .HasForeignKey("TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs deleted file mode 100644 index 8ec08eac2..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class Criteria : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Criteria", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - TaskId = table.Column(nullable: false), - Type = table.Column(nullable: false), - Name = table.Column(nullable: true), - MaxPoints = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Criteria", x => x.Id); - table.ForeignKey( - name: "FK_Criteria_Tasks_TaskId", - column: x => x.TaskId, - principalTable: "Tasks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Criteria_TaskId", - table: "Criteria", - column: "TaskId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Criteria"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs deleted file mode 100644 index ddc6b7d83..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class BonusTaskExplicit : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsBonusExplicit", - table: "Tasks", - nullable: false, - defaultValue: false); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsBonusExplicit", - table: "Tasks"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.Designer.cs similarity index 92% rename from HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs rename to HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.Designer.cs index 297f015d8..a96a714f2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.Designer.cs @@ -10,8 +10,8 @@ namespace HwProj.CoursesService.API.Migrations { [DbContext(typeof(CourseContext))] - [Migration("20260221230655_BonusTaskExplicit")] - partial class BonusTaskExplicit + [Migration("20260322063832_InitialCreate")] + partial class InitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -54,6 +54,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("IsOpen"); + b.Property("LtiToolName"); + b.Property("MentorIds"); b.Property("Name"); @@ -209,6 +211,20 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Tasks"); }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => + { + b.Property("HomeworkTaskId"); + + b.Property("CustomParams"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.HasKey("HomeworkTaskId"); + + b.ToTable("TaskLtiData"); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.Property("CourseMateId"); @@ -329,6 +345,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "HomeworkTask") + .WithMany() + .HasForeignKey("HomeworkTaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.HasOne("HwProj.CoursesService.API.Models.CourseMate") diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.cs new file mode 100644 index 000000000..11a37c098 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.cs @@ -0,0 +1,376 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CourseFilters", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + FilterJson = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseFilters", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Courses", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Name = table.Column(nullable: true), + GroupName = table.Column(nullable: true), + IsOpen = table.Column(nullable: false), + InviteCode = table.Column(nullable: true), + IsCompleted = table.Column(nullable: false), + MentorIds = table.Column(nullable: true), + LtiToolName = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Courses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CourseId = table.Column(nullable: false), + Name = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Questions", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + TaskId = table.Column(nullable: false), + StudentId = table.Column(nullable: true), + Text = table.Column(maxLength: 1000, nullable: true), + IsPrivate = table.Column(nullable: false), + LecturerId = table.Column(nullable: true), + Answer = table.Column(maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Questions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserToCourseFilters", + columns: table => new + { + CourseId = table.Column(nullable: false), + UserId = table.Column(nullable: false), + CourseFilterId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserToCourseFilters", x => new { x.CourseId, x.UserId }); + table.ForeignKey( + name: "FK_UserToCourseFilters_CourseFilters_CourseFilterId", + column: x => x.CourseFilterId, + principalTable: "CourseFilters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Assignments", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CourseId = table.Column(nullable: false), + MentorId = table.Column(nullable: true), + StudentId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Assignments", x => x.Id); + table.ForeignKey( + name: "FK_Assignments_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CourseMates", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CourseId = table.Column(nullable: false), + StudentId = table.Column(nullable: true), + IsAccepted = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseMates", x => x.Id); + table.ForeignKey( + name: "FK_CourseMates_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Homeworks", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Title = table.Column(nullable: true), + Description = table.Column(nullable: true), + HasDeadline = table.Column(nullable: false), + DeadlineDate = table.Column(nullable: true), + IsDeadlineStrict = table.Column(nullable: false), + PublicationDate = table.Column(nullable: false), + Tags = table.Column(nullable: true), + CourseId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Homeworks", x => x.Id); + table.ForeignKey( + name: "FK_Homeworks_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "GroupMates", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + GroupId = table.Column(nullable: false), + StudentId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GroupMates", x => x.Id); + table.UniqueConstraint("AK_GroupMates_GroupId_StudentId", x => new { x.GroupId, x.StudentId }); + table.ForeignKey( + name: "FK_GroupMates_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TasksModels", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + TaskId = table.Column(nullable: false), + GroupId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TasksModels", x => x.Id); + table.ForeignKey( + name: "FK_TasksModels_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "StudentCharacteristics", + columns: table => new + { + CourseMateId = table.Column(nullable: false), + Tags = table.Column(nullable: true), + Description = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StudentCharacteristics", x => x.CourseMateId); + table.ForeignKey( + name: "FK_StudentCharacteristics_CourseMates_CourseMateId", + column: x => x.CourseMateId, + principalTable: "CourseMates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tasks", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Title = table.Column(nullable: true), + Description = table.Column(nullable: true), + MaxRating = table.Column(nullable: false), + HasDeadline = table.Column(nullable: true), + DeadlineDate = table.Column(nullable: true), + IsDeadlineStrict = table.Column(nullable: true), + PublicationDate = table.Column(nullable: true), + IsBonusExplicit = table.Column(nullable: false), + HomeworkId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tasks", x => x.Id); + table.ForeignKey( + name: "FK_Tasks_Homeworks_HomeworkId", + column: x => x.HomeworkId, + principalTable: "Homeworks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Criteria", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + TaskId = table.Column(nullable: false), + Type = table.Column(nullable: false), + Name = table.Column(nullable: true), + MaxPoints = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Criteria", x => x.Id); + table.ForeignKey( + name: "FK_Criteria_Tasks_TaskId", + column: x => x.TaskId, + principalTable: "Tasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TaskLtiData", + columns: table => new + { + HomeworkTaskId = table.Column(nullable: false), + LtiLaunchUrl = table.Column(nullable: false), + CustomParams = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskLtiData", x => x.HomeworkTaskId); + table.ForeignKey( + name: "FK_TaskLtiData_Tasks_HomeworkTaskId", + column: x => x.HomeworkTaskId, + principalTable: "Tasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Assignments_CourseId", + table: "Assignments", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_CourseMates_CourseId", + table: "CourseMates", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_Criteria_TaskId", + table: "Criteria", + column: "TaskId"); + + migrationBuilder.CreateIndex( + name: "IX_Homeworks_CourseId", + table: "Homeworks", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_Questions_TaskId", + table: "Questions", + column: "TaskId"); + + migrationBuilder.CreateIndex( + name: "IX_Tasks_HomeworkId", + table: "Tasks", + column: "HomeworkId"); + + migrationBuilder.CreateIndex( + name: "IX_TasksModels_GroupId", + table: "TasksModels", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_UserToCourseFilters_CourseFilterId", + table: "UserToCourseFilters", + column: "CourseFilterId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Assignments"); + + migrationBuilder.DropTable( + name: "Criteria"); + + migrationBuilder.DropTable( + name: "GroupMates"); + + migrationBuilder.DropTable( + name: "Questions"); + + migrationBuilder.DropTable( + name: "StudentCharacteristics"); + + migrationBuilder.DropTable( + name: "TaskLtiData"); + + migrationBuilder.DropTable( + name: "TasksModels"); + + migrationBuilder.DropTable( + name: "UserToCourseFilters"); + + migrationBuilder.DropTable( + name: "CourseMates"); + + migrationBuilder.DropTable( + name: "Tasks"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropTable( + name: "CourseFilters"); + + migrationBuilder.DropTable( + name: "Homeworks"); + + migrationBuilder.DropTable( + name: "Courses"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 199917cf8..62a3e2815 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -66,6 +66,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsOpen") .HasColumnType("bit"); + b.Property("LtiToolName") + .HasColumnType("nvarchar(max)"); + b.Property("MentorIds") .HasColumnType("nvarchar(max)"); @@ -272,6 +275,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tasks"); }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => + { + b.Property("HomeworkTaskId"); + + b.Property("CustomParams"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.HasKey("HomeworkTaskId"); + + b.ToTable("TaskLtiUrls"); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.Property("CourseMateId") @@ -420,6 +437,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Homework"); }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "HomeworkTask") + .WithMany() + .HasForeignKey("HomeworkTaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.HasOne("HwProj.CoursesService.API.Models.CourseMate", null) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs index 255241668..36127f5cf 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs @@ -16,5 +16,6 @@ public class Course : IEntity public List CourseMates { get; set; } = new List(); public List Homeworks { get; set; } = new List(); public List Assignments { get; set; } = new List(); + public string? LtiToolName { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs index c32220254..8d7be6cb2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -18,6 +18,7 @@ public sealed class CourseContext : DbContext public DbSet UserToCourseFilters { get; set; } public DbSet Questions { get; set; } public DbSet Criteria { get; set; } + public DbSet TaskLtiData { get; set; } public CourseContext(DbContextOptions options) : base(options) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs index 35e66b44e..afcdd5358 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs @@ -11,5 +11,6 @@ public class CourseTemplate public bool IsOpen { get; set; } public List Homeworks { get; set; } = new List(); + public string? LtiToolName { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs new file mode 100644 index 000000000..a96104290 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace HwProj.CoursesService.API.Models +{ + public class HomeworkTaskLtiLaunchData + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public long HomeworkTaskId { get; set; } + + [Required] + public string LtiLaunchUrl { get; set; } + + /// JSON + public string? CustomParams { get; set; } + + [ForeignKey(nameof(HomeworkTaskId))] + public HomeworkTask HomeworkTask { get; set; } + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs index d79456add..592424279 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs @@ -17,5 +17,7 @@ public class HomeworkTaskTemplate public bool HasSpecialDeadlineDate { get; set; } public bool IsBonusExplicit { get; set; } + + public LtiLaunchData? LtiLaunchData { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs new file mode 100644 index 000000000..d353d1461 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs @@ -0,0 +1,5 @@ +public class LtiLaunchData +{ + public string LtiLaunchUrl { get; set; } + public string? CustomParams { get; set; } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs index e6fe97e7d..26301ee26 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs @@ -9,6 +9,10 @@ namespace HwProj.CoursesService.API.Repositories { public interface ITasksRepository : ICrudRepository { + Task AddOrUpdateLtiLaunchDataAsync(long taskId, LtiLaunchData ltiLaunchData); + Task AddRangeLtiLaunchDataAsync(IEnumerable ltiLaunchData); + Task GetLtiDataAsync(long taskId); + Task> GetLtiDataForTasksAsync(IEnumerable taskIds); Task GetWithHomeworkAsync(long id); Task UpdateAsync(long id, Expression> updateFunc, List criteria); Task GetWithHomeworkAndCriteriaAsync(long id); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index b8405e9ee..87e41ebdf 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -54,6 +54,65 @@ public Task GetWithHomeworkAsync(long id) .FirstOrDefaultAsync(x => x.Id == id); } + public async Task AddOrUpdateLtiLaunchDataAsync(long taskId, LtiLaunchData ltiLaunchData) + { + var existingRecord = await Context.Set().FindAsync(taskId); + + if (existingRecord != null) + { + existingRecord.LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl; + existingRecord.CustomParams = ltiLaunchData.CustomParams; + Context.Set().Update(existingRecord); + } + else + { + var ltiRecord = new HomeworkTaskLtiLaunchData + { + HomeworkTaskId = taskId, + LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, + CustomParams = ltiLaunchData.CustomParams + }; + await Context.Set().AddAsync(ltiRecord); + } + + await Context.SaveChangesAsync(); + } + + public async Task AddRangeLtiLaunchDataAsync(IEnumerable ltiLaunchData) + { + var ltiLaunchDataList = ltiLaunchData as HomeworkTaskLtiLaunchData[] ?? ltiLaunchData.ToArray(); + if (!ltiLaunchDataList.Any()) + { + return; + } + + await Context.Set().AddRangeAsync(ltiLaunchDataList); + + await Context.SaveChangesAsync(); + } + + public async Task GetLtiDataAsync(long taskId) + { + var record = await Context.Set().FindAsync(taskId); + + return record == null ? null : new LtiLaunchData + { + LtiLaunchUrl = record.LtiLaunchUrl, + CustomParams = record.CustomParams + }; + } + + public async Task> GetLtiDataForTasksAsync(IEnumerable taskIds) + { + return await Context.Set() + .Where(t => taskIds.Contains(t.HomeworkTaskId)) + .ToDictionaryAsync(t => t.HomeworkTaskId, t => new LtiLaunchData + { + LtiLaunchUrl = t.LtiLaunchUrl, + CustomParams = t.CustomParams + }); + } + public async Task UpdateAsync(long id, Expression> updateFunc, List criteria) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index dca5492ec..914e075d4 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -76,6 +76,9 @@ public async Task GetAllAsync() var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToListAsync(); var courseDto = course.ToCourseDto(); + + await FillNecessaryLtiDataForCourseDtos(courseDto); + courseDto.Groups = groups.Select(g => new GroupViewModel { @@ -84,7 +87,7 @@ public async Task GetAllAsync() StudentsIds = g.GroupMates.Select(t => t.StudentId).ToArray() }).ToArray(); - var result = userId == string.Empty ? courseDto : await _courseFilterService.ApplyFilter(courseDto, userId); + var result = string.IsNullOrEmpty(userId) ? courseDto : await _courseFilterService.ApplyFilter(courseDto, userId); return result; } @@ -112,6 +115,28 @@ public async Task AddAsync(CreateCourseViewModel courseViewModel, string m baseCourse?.Homeworks.Select(h => h.ToHomeworkTemplate()).ToList() ?? new List(); + if (baseCourse?.LtiToolName != null) + { + var allTaskIds = baseCourse.Homeworks + .SelectMany(h => h.Tasks.Select(t => t.Id)); + + var ltiDataDict = await _tasksRepository.GetLtiDataForTasksAsync(allTaskIds); + + for (var homeworkIndex = 0; homeworkIndex < baseCourse.Homeworks.Count; homeworkIndex++) + { + var homework = baseCourse.Homeworks[homeworkIndex]; + var homeworkTemplate = courseTemplate.Homeworks[homeworkIndex]; + + for (var i = 0; i < homeworkTemplate.Tasks.Count; i++) + { + if (ltiDataDict.TryGetValue(homework.Tasks[i].Id, out var ltiData)) + { + homeworkTemplate.Tasks[i].LtiLaunchData = ltiData; + } + } + } + } + using var transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var newCourse = await AddFromTemplateAsync(courseTemplate, courseViewModel.StudentIDs, mentorId); @@ -154,13 +179,42 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L course.MentorIds = mentorId; course.InviteCode = Guid.NewGuid().ToString(); var courseId = await _coursesRepository.AddAsync(course); + course.LtiToolName = courseTemplate.LtiToolName; var homeworks = courseTemplate.Homeworks.Select(hwTemplate => hwTemplate.ToHomework(courseId)); var homeworkIds = await _homeworksRepository.AddRangeAsync(homeworks); - var tasks = courseTemplate.Homeworks.SelectMany((hwTemplate, i) => - hwTemplate.Tasks.Select(taskTemplate => taskTemplate.ToHomeworkTask(homeworkIds[i]))); - await _tasksRepository.AddRangeAsync(tasks); + var taskPairs = courseTemplate.Homeworks + .SelectMany((hwTemplate, i) => + hwTemplate.Tasks.Select(taskTemplate => new + { + Template = taskTemplate, + NewEntity = taskTemplate.ToHomeworkTask(homeworkIds[i]) + })) + .ToList(); + + var tasksToSave = taskPairs.Select(x => x.NewEntity); + await _tasksRepository.AddRangeAsync(tasksToSave); + + var ltiDataToSave = new List(); + + foreach (var pair in taskPairs) + { + if (pair.Template.LtiLaunchData != null) + { + ltiDataToSave.Add(new HomeworkTaskLtiLaunchData + { + HomeworkTaskId = pair.NewEntity.Id, + LtiLaunchUrl = pair.Template.LtiLaunchData.LtiLaunchUrl, + CustomParams = pair.Template.LtiLaunchData.CustomParams + }); + } + } + + if (ltiDataToSave.Any()) + { + await _tasksRepository.AddRangeLtiLaunchDataAsync(ltiDataToSave); + } if (studentIds.Any()) { @@ -203,7 +257,8 @@ public async Task UpdateAsync(long courseId, Course updated) Name = updated.Name, GroupName = updated.GroupName, IsCompleted = updated.IsCompleted, - IsOpen = updated.IsOpen + IsOpen = updated.IsOpen, + LtiToolName = updated.LtiToolName, }); } @@ -294,6 +349,8 @@ public async Task GetUserCoursesAsync(string userId, string role) var result = await _courseFilterService.ApplyFiltersToCourses( userId, coursesWithValues.Select(c => c.ToCourseDto()).ToArray()); + await FillNecessaryLtiDataForCourseDtos(result); + if (role == Roles.ExpertRole) { foreach (var courseDto in result) @@ -307,7 +364,8 @@ public async Task GetUserCoursesAsync(string userId, string role) return result; } - public async Task AcceptLecturerAsync(long courseId, string lecturerEmail, string lecturerId) + public async Task AcceptLecturerAsync(long courseId, string lecturerEmail, string lecturerId, + bool sendNotification = true) { var course = await _coursesRepository.GetAsync(courseId); if (course == null) return false; @@ -319,13 +377,17 @@ public async Task AcceptLecturerAsync(long courseId, string lecturerEmail, MentorIds = newMentors, }); - _eventBus.Publish(new LecturerInvitedToCourseEvent + if (sendNotification) { - CourseId = courseId, - CourseName = course.Name, - MentorId = lecturerId, - MentorEmail = lecturerEmail - }); + _eventBus.Publish(new LecturerInvitedToCourseEvent + { + CourseId = courseId, + CourseName = course.Name, + MentorId = lecturerId, + MentorEmail = lecturerEmail + }); + } + //TODO: remove await RejectCourseMateAsync(courseId, lecturerId); } @@ -373,5 +435,31 @@ await _courseMatesRepository.FindAll(x => x.CourseId == courseId && x.StudentId await _context.SaveChangesAsync(); return true; } + + private async Task FillNecessaryLtiDataForCourseDtos(params CourseDTO[] courses) + { + var ltiCourses = courses.Where(c => c.LtiToolName != null).ToArray(); + if (!ltiCourses.Any()) + { + return; + } + + var allTasks = ltiCourses.SelectMany(c => c.Homeworks).SelectMany(h => h.Tasks).ToList(); + + if (allTasks.Any()) + { + var taskIds = allTasks.Select(t => t.Id).ToArray(); + + var ltiUrls = await _tasksRepository.GetLtiDataForTasksAsync(taskIds); + + foreach (var taskDto in allTasks) + { + if (ltiUrls.TryGetValue(taskDto.Id, out var ltiLaunchData)) + { + taskDto.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + } + } + } + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index a6816f1bd..0ea11c505 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -18,19 +18,24 @@ public class HomeworksService : IHomeworksService private readonly ICoursesRepository _coursesRepository; private readonly IGroupsService _groupsService; private readonly ICourseFilterService _courseFilterService; + private readonly ITasksRepository _tasksRepository; + private readonly ITasksService _tasksService; public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, ICoursesRepository coursesRepository, - IGroupsService groupsService, ICourseFilterService courseFilterService) + IGroupsService groupsService, ICourseFilterService courseFilterService, + ITasksRepository tasksRepository, ITasksService tasksService) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; _groupsService = groupsService; _courseFilterService = courseFilterService; + _tasksRepository = tasksRepository; + _tasksService = tasksService; } - public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) + public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) { homeworkViewModel.Tags = homeworkViewModel.Tags.Where(t => !string.IsNullOrWhiteSpace(t)).ToList(); var homework = homeworkViewModel.ToHomework(); @@ -41,6 +46,7 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); await _homeworksRepository.AddAsync(homework); + var savedHomework = await GetHomeworkAsync(homework.Id, withCriteria: true); if (homework.GroupId is { } groupId) { @@ -57,21 +63,43 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.DeadlineDate)); } - return await GetHomeworkAsync(homework.Id, withCriteria: true); + if (homeworkViewModel.Tasks == null || homework.Tasks == null) return savedHomework; + + var createdTasks = homework.Tasks.ToList(); + + for (var i = 0; i < createdTasks.Count && i < homeworkViewModel.Tasks.Count; i++) + { + var taskModel = homeworkViewModel.Tasks[i]; + var ltiLaunchData = taskModel.LtiLaunchData.ToLtiLaunchData(); + if (ltiLaunchData == null) + { + continue; + } + + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(createdTasks[i].Id, ltiLaunchData); + savedHomework.Tasks[i].LtiLaunchData = taskModel.LtiLaunchData; + } + + return savedHomework; } - public async Task GetHomeworkAsync(long homeworkId, bool withCriteria = false) + public async Task GetHomeworkAsync(long homeworkId, bool withCriteria = false) { var homework = await _homeworksRepository.GetWithTasksAsync(homeworkId, withCriteria); CourseDomain.FillTasksInHomework(homework); - return homework; + var resultViewModelHomework = homework.ToHomeworkViewModel(); + await _tasksService.FillLtiLaunchDataForTasks(resultViewModelHomework); + + return resultViewModelHomework; } - public async Task GetForEditingHomeworkAsync(long homeworkId) + public async Task GetForEditingHomeworkAsync(long homeworkId) { - var result = await _homeworksRepository.GetWithTasksAsync(homeworkId); + var homework = await _homeworksRepository.GetWithTasksAsync(homeworkId); + var result = homework.ToHomeworkViewModel(); + await _tasksService.FillLtiLaunchDataForTasks(result); return result; } @@ -80,7 +108,7 @@ public async Task DeleteHomeworkAsync(long homeworkId) await _homeworksRepository.DeleteAsync(homeworkId); } - public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkViewModel homeworkViewModel) + public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkViewModel homeworkViewModel) { homeworkViewModel.Tags = homeworkViewModel.Tags.Where(t => !string.IsNullOrWhiteSpace(t)).ToList(); var update = homeworkViewModel.ToHomework(); @@ -116,7 +144,11 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV var updatedHomework = await _homeworksRepository.GetWithTasksAsync(homeworkId); CourseDomain.FillTasksInHomework(updatedHomework); - return updatedHomework; + + var updatedHomeworkViewModel = updatedHomework.ToHomeworkViewModel(); + await _tasksService.FillLtiLaunchDataForTasks(updatedHomeworkViewModel); + + return updatedHomeworkViewModel; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs index 6b52df25b..77c5ebeb5 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs @@ -18,7 +18,8 @@ public interface ICoursesService Task AcceptCourseMateAsync(long courseId, string studentId); Task RejectCourseMateAsync(long courseId, string studentId); Task GetUserCoursesAsync(string userId, string role); - Task AcceptLecturerAsync(long courseId, string lecturerEmail, string lecturerId); + Task AcceptLecturerAsync(long courseId, string lecturerEmail, string lecturerId, + bool sendNotification = true); Task GetCourseLecturers(long courseId); Task HasStudent(long courseId, string studentId); Task UpdateStudentCharacteristics(long courseId, string studentId, StudentCharacteristicsDto characteristics); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs index 2b2460c37..45d768eca 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs @@ -6,14 +6,14 @@ namespace HwProj.CoursesService.API.Services { public interface IHomeworksService { - Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel); + Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel); - Task GetHomeworkAsync(long homeworkId, bool withCriteria = false); + Task GetHomeworkAsync(long homeworkId, bool withCriteria = false); - Task GetForEditingHomeworkAsync(long homeworkId); + Task GetForEditingHomeworkAsync(long homeworkId); Task DeleteHomeworkAsync(long homeworkId); - Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkViewModel homeworkViewModel); + Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkViewModel homeworkViewModel); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index 0f6301fa5..e1f957f25 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -1,16 +1,18 @@ -using HwProj.CoursesService.API.Models; +using System.Collections.Generic; +using System.Threading.Tasks; +using HwProj.CoursesService.API.Models; using HwProj.Models; using HwProj.Models.CoursesService.ViewModels; -using System.Threading.Tasks; namespace HwProj.CoursesService.API.Services { public interface ITasksService { - Task GetTaskAsync(long taskId, bool withCriteria = false); + Task GetTaskAsync(long taskId, bool withCriteria = false); Task GetForEditingTaskAsync(long taskId); - Task AddTaskAsync(long homeworkId, PostTaskViewModel taskViewModel); + Task AddTaskAsync(long homeworkId, PostTaskViewModel taskViewModel); Task DeleteTaskAsync(long taskId); - Task UpdateTaskAsync(long taskId, PostTaskViewModel taskViewModel, ActionOptions options); + Task UpdateTaskAsync(long taskId, PostTaskViewModel taskViewModel, ActionOptions options); + Task FillLtiLaunchDataForTasks(HomeworkViewModel viewModel); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index 2d4d7cb6e..5861c94bd 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using HwProj.CoursesService.API.Domains; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories; @@ -5,9 +9,6 @@ using HwProj.Models; using HwProj.Models.CoursesService.ViewModels; using HwProj.NotificationService.Events.CoursesService; -using System; -using System.Linq; -using System.Threading.Tasks; namespace HwProj.CoursesService.API.Services { @@ -27,15 +28,17 @@ public TasksService(ITasksRepository tasksRepository, IEventBus eventBus, ICours _coursesRepository = coursesRepository; } - public async Task GetTaskAsync(long taskId, bool withCriteria = false) + public async Task GetTaskAsync(long taskId, bool withCriteria = false) { var taskFromDb = withCriteria ? await _tasksRepository.GetWithHomeworkAndCriteriaAsync(taskId) : await _tasksRepository.GetWithHomeworkAsync(taskId); CourseDomain.FillTask(taskFromDb.Homework, taskFromDb); + var taskViewModel = taskFromDb.ToHomeworkTaskViewModel(); + await this.FillTaskViewModelWithLtiLaunchDataAsync(taskViewModel, taskId); - return taskFromDb; + return taskViewModel; } public async Task GetForEditingTaskAsync(long taskId) @@ -43,7 +46,9 @@ public async Task GetForEditingTaskAsync(long taskId) return await _tasksRepository.GetWithHomeworkAndCriteriaAsync(taskId); } - public async Task AddTaskAsync(long homeworkId, PostTaskViewModel taskViewModel) + public async Task AddTaskAsync( + long homeworkId, + PostTaskViewModel taskViewModel) { var task = taskViewModel.ToHomeworkTask(); task.HomeworkId = homeworkId; @@ -52,6 +57,12 @@ public async Task AddTaskAsync(long homeworkId, PostTaskViewModel var course = await _coursesRepository.GetWithCourseMatesAndHomeworksAsync(homework.CourseId); var taskId = await _tasksRepository.AddAsync(task); + + if (taskViewModel.LtiLaunchData != null && !string.IsNullOrEmpty(taskViewModel.LtiLaunchData.LtiLaunchUrl)) + { + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(taskId, taskViewModel.LtiLaunchData.ToLtiLaunchData()!); + } + var deadlineDate = task.DeadlineDate ?? homework.DeadlineDate; var studentIds = course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); @@ -67,7 +78,9 @@ public async Task DeleteTaskAsync(long taskId) await _tasksRepository.DeleteAsync(taskId); } - public async Task UpdateTaskAsync(long taskId, PostTaskViewModel taskViewModel, + public async Task UpdateTaskAsync( + long taskId, + PostTaskViewModel taskViewModel, ActionOptions options) { var update = taskViewModel.ToHomeworkTask(); @@ -94,7 +107,45 @@ public async Task UpdateTaskAsync(long taskId, PostTaskViewModel t IsBonusExplicit = update.IsBonusExplicit, }, update.Criteria); + if (taskViewModel.LtiLaunchData != null && !string.IsNullOrEmpty(taskViewModel.LtiLaunchData.LtiLaunchUrl)) + { + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(taskId, taskViewModel.LtiLaunchData.ToLtiLaunchData()!); + } + return await GetTaskAsync(taskId, true); } + + public async Task FillTaskViewModelWithLtiLaunchDataAsync(HomeworkTaskViewModel taskViewModel, long taskId) + { + var ltiLaunchData = await this.GetTaskLtiDataAsync(taskId); + taskViewModel.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + } + + public async Task FillLtiLaunchDataForTasks(HomeworkViewModel viewModel) + { + if (viewModel.Tasks != null && viewModel.Tasks.Any()) + { + var taskIds = viewModel.Tasks.Select(t => t.Id).ToArray(); + var ltiLaunchMultipleData = await this.GetLtiDataForTasksAsync(taskIds); + + foreach (var task in viewModel.Tasks) + { + if (ltiLaunchMultipleData.TryGetValue(task.Id, out var ltiLaunchData)) + { + task.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + } + } + } + } + + private async Task GetTaskLtiDataAsync(long taskId) + { + return await _tasksRepository.GetLtiDataAsync(taskId); + } + + private async Task> GetLtiDataForTasksAsync(long[] taskIds) + { + return await _tasksRepository.GetLtiDataForTasksAsync(taskIds); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json b/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json index b8f8a2600..0cec9984e 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json +++ b/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=CoursesServiceDB;Trusted_Connection=True;TrustServerCertificate=true;", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=CoursesServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=CoursesServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=true;" }, "Logging": { "LogLevel": { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/global.json b/HwProj.CoursesService/HwProj.CoursesService.API/global.json new file mode 100644 index 000000000..e4d8f6d06 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/global.json @@ -0,0 +1 @@ +{ "sdk": { "version": "2.2.207" } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index c82dcdcbe..50951aaa4 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -61,13 +61,21 @@ public async Task GetAllCourses() return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; } - public async Task GetCourseById(long courseId) + public async Task GetCourseById(long courseId, string? userId = null) { using var httpRequest = new HttpRequestMessage( HttpMethod.Get, _coursesServiceUri + $"api/Courses/{courseId}"); - httpRequest.TryAddUserId(_httpContextAccessor); + if (string.IsNullOrEmpty(userId)) + { + httpRequest.TryAddUserId(_httpContextAccessor); + } + else + { + httpRequest.Headers.Add("UserId", userId); + } + var response = await _httpClient.SendAsync(httpRequest); return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; } @@ -509,12 +517,14 @@ public async Task GetGroupTasks(long groupId) return await response.DeserializeAsync(); } - public async Task AcceptLecturer(long courseId, string lecturerEmail, string lecturerId) + public async Task AcceptLecturer(long courseId, string lecturerEmail, string lecturerId, + bool sendNotification = true) { using var httpRequest = new HttpRequestMessage( HttpMethod.Get, _coursesServiceUri + - $"api/Courses/acceptLecturer/{courseId}?lecturerEmail={lecturerEmail}&lecturerId={lecturerId}"); + $"api/Courses/acceptLecturer/{courseId}?lecturerEmail={lecturerEmail}&lecturerId={lecturerId}" + + $"&sendNotification={sendNotification}"); httpRequest.TryAddUserId(_httpContextAccessor); var response = await _httpClient.SendAsync(httpRequest); @@ -641,5 +651,6 @@ public async Task Ping() return false; } } + } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index 9bcb031c1..44d461a57 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -9,7 +9,7 @@ public interface ICoursesServiceClient { Task GetAllCourses(); Task GetCourseView(long courseId); - Task GetCourseById(long courseId); + Task GetCourseById(long courseId, string? userId = null); Task> GetCourseByIdForMentor(long courseId, string mentorId); /// Получить полную информацию о курсе без учетов фильтров для преподавателей Task> GetCourseDataRaw(long courseId); @@ -44,7 +44,8 @@ Task UpdateStudentCharacteristics(long courseId, string studentId, Task AddStudentInGroup(long courseId, long groupId, string userId); Task GetGroupsById(params long[] groupIds); Task GetGroupTasks(long groupId); - Task AcceptLecturer(long courseId, string lecturerEmail, string lecturerId); + Task AcceptLecturer(long courseId, string lecturerEmail, string lecturerId, + bool sendNotification = true); Task GetCourseLecturersIds(long courseId); Task> GetAllTagsForCourse(long courseId); Task> CreateOrUpdateCourseFilter(long courseId, CreateCourseFilterDTO model); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index 9c4e0554b..04efec942 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -21,7 +21,7 @@ namespace HwProj.SolutionsService.API.Controllers { [Route("api/[controller]")] - [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] + // [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] [ApiController] public class SolutionsController : Controller { @@ -85,10 +85,16 @@ public async Task RateSolution(long solutionId, { var solution = await _solutionsService.GetSolutionAsync(solutionId); var task = await _coursesClient.GetTask(solution.TaskId); - var homework = await _coursesClient.GetHomework(task.HomeworkId); - var course = await _coursesClient.GetCourseById(homework.CourseId); + + if (rateSolutionModel.Rating < 0 || rateSolutionModel.Rating > task.MaxRating) + { + return BadRequest($"Rating must be between 0 and {task.MaxRating}."); + } + + var course = await _coursesClient.GetCourseByTask(solution.TaskId); var lecturerId = Request.GetUserIdFromHeader(); + if (course != null && lecturerId != null && course.MentorIds.Contains(lecturerId)) { await _solutionsService.RateSolutionAsync(solutionId, lecturerId, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); @@ -98,6 +104,43 @@ public async Task RateSolution(long solutionId, return Forbid(); } + [HttpPost("postSolutionWithRate/{taskId}")] + public async Task PostSolutionWithRate( + long taskId, + [FromBody] PostSolutionModel solutionModel, + [FromQuery] bool sendNotification = true) + { + var task = await _coursesClient.GetTask(taskId); + if (!task.CanSendSolution) + return BadRequest(); + + if (!solutionModel.Rating.HasValue || + solutionModel.Rating.Value < 0 || + solutionModel.Rating.Value > task.MaxRating) + { + return BadRequest($"Rating must be between 0 and {task.MaxRating}."); + } + + var rating = solutionModel.Rating.Value; + + var course = await _coursesClient.GetCourseByTask(taskId); + var lecturerId = Request.GetUserIdFromHeader(); + + if (course == null || lecturerId == null || !course.MentorIds.Contains(lecturerId)) + return Forbid(); + + var solution = _mapper.Map(solutionModel); + var solutionId = await _solutionsService.PostOrUpdateWithRateAsync( + taskId, + solution, + lecturerId, + rating, + solutionModel.LecturerComment, + sendNotification); + + return Ok(solutionId); + } + [HttpPost("rateEmptySolution/{taskId}")] public async Task PostEmptySolutionWithRate(long taskId, [FromBody] SolutionViewModel solutionViewModel) @@ -326,4 +369,4 @@ public async Task GetSolutionActuality(long solutionId) return await _solutionsService.GetSolutionActuality(solutionId); } } -} \ No newline at end of file +} diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs index 9f284b30f..5e18d7a1f 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs @@ -16,7 +16,9 @@ public interface ISolutionsService Task GetTaskSolutionsFromGroupAsync(long taskId, long groupId); - Task PostOrUpdateAsync(long taskId, Solution solution); + Task PostOrUpdateAsync(long taskId, Solution solution, bool sendNotification = true); + Task PostOrUpdateWithRateAsync(long taskId, Solution solution, string lecturerId, int rating, + string? lecturerComment, bool sendNotification = true); Task PostEmptySolutionWithRateAsync(long task, Solution solution); Task RateSolutionAsync(long solutionId, string lecturerId, int newRating, string lecturerComment); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs index 402c68944..7c53c6c52 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs @@ -60,6 +60,7 @@ public Task GetSolutionAsync(long solutionId) public async Task GetTaskSolutionsFromStudentAsync(long taskId, string studentId) { var course = await _coursesServiceClient.GetCourseByTask(taskId); + if (course == null) return Array.Empty(); var studentGroupsIds = course.Groups @@ -102,50 +103,6 @@ public async Task GetTaskSolutionsFromStudentAsync(long taskId, stri return taskIds.Select(t => solutions.FirstOrDefault(s => s?.TaskId == t)).ToArray(); } - public async Task PostOrUpdateAsync(long taskId, Solution solution) - { - solution.PublicationDate = DateTime.UtcNow; - solution.TaskId = taskId; - - var task = await _coursesServiceClient.GetTask(solution.TaskId); - - var lastSolution = - await _solutionsRepository - .FindAll(s => s.TaskId == taskId && s.StudentId == solution.StudentId) - .OrderByDescending(t => t.PublicationDate) - .FirstOrDefaultAsync(); - - long? solutionId; - - if (lastSolution != null && lastSolution.State == SolutionState.Posted) - { - var isModified = lastSolution.GithubUrl != solution.GithubUrl || lastSolution.Comment != solution.Comment; - await _solutionsRepository.UpdateAsync(lastSolution.Id, x => new Solution - { - GithubUrl = solution.GithubUrl, - Comment = solution.Comment, - GroupId = solution.GroupId, - IsModified = isModified, - State = SolutionState.Posted, - }); - solutionId = lastSolution.Id; - } - else - { - solutionId = await _solutionsRepository.AddAsync(solution); - - var solutionModel = _mapper.Map(solution); - var course = await _coursesServiceClient.GetCourseByTask(solution.TaskId); - var student = await _authServiceClient.GetAccountData(solutionModel.StudentId); - var studentModel = _mapper.Map(student); - _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); - } - - if (task.Tags.Contains(HomeworkTags.Test)) - await TrySaveSolutionCommitsInfo(solutionId.Value, solution.GithubUrl); - return solutionId.Value; - } - public async Task PostEmptySolutionWithRateAsync(long taskId, Solution solution) { var hasSolution = await _solutionsRepository @@ -351,6 +308,128 @@ await client.PullRequest.Commits(pullRequest.Owner, pullRequest.RepoName, pullRe return solutionsActuality; } + public async Task PostOrUpdateAsync( + long taskId, Solution solution, bool sendNotification = true) + { + solution.PublicationDate = DateTime.UtcNow; + solution.TaskId = taskId; + + var task = await _coursesServiceClient.GetTask(solution.TaskId); + + var lastSolution = + await _solutionsRepository + .FindAll(s => s.TaskId == taskId && s.StudentId == solution.StudentId) + .OrderByDescending(t => t.PublicationDate) + .FirstOrDefaultAsync(); + + long? solutionId; + + if (lastSolution != null && lastSolution.State == SolutionState.Posted) + { + var isModified = lastSolution.GithubUrl != solution.GithubUrl || + lastSolution.Comment != solution.Comment; + await _solutionsRepository.UpdateAsync(lastSolution.Id, x => new Solution + { + GithubUrl = solution.GithubUrl, + Comment = solution.Comment, + GroupId = solution.GroupId, + IsModified = isModified, + State = SolutionState.Posted, + }); + solutionId = lastSolution.Id; + } + else + { + solutionId = await _solutionsRepository.AddAsync(solution); + + var solutionModel = _mapper.Map(solution); + var course = await _coursesServiceClient.GetCourseByTask(solution.TaskId); + var student = await _authServiceClient.GetAccountData(solutionModel.StudentId); + var studentModel = _mapper.Map(student); + + if (sendNotification) + { + _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); + } + } + + if (task.Tags.Contains(HomeworkTags.Test)) + await TrySaveSolutionCommitsInfo(solutionId.Value, solution.GithubUrl); + return solutionId.Value; + } + + public async Task PostOrUpdateWithRateAsync( + long taskId, + Solution solution, + string lecturerId, + int rating, + string? lecturerComment, + bool sendNotification = true) + { + var currentTime = DateTime.UtcNow; + var task = await _coursesServiceClient.GetTask(taskId); + + solution.TaskId = taskId; + solution.PublicationDate = currentTime; + solution.RatingDate = currentTime; + solution.Rating = rating; + solution.LecturerId = lecturerId; + solution.LecturerComment = lecturerComment ?? string.Empty; + solution.State = rating >= task.MaxRating + ? SolutionState.Final + : SolutionState.Rated; + + var lastSolution = await _solutionsRepository + .FindAll(s => s.TaskId == taskId && s.StudentId == solution.StudentId) + .OrderByDescending(s => s.PublicationDate) + .FirstOrDefaultAsync(); + + long solutionId; + + if (lastSolution != null && lastSolution.State == SolutionState.Posted) + { + var isModified = lastSolution.GithubUrl != solution.GithubUrl || + lastSolution.Comment != solution.Comment; + + await _solutionsRepository.UpdateAsync(lastSolution.Id, _ => new Solution + { + GithubUrl = solution.GithubUrl, + Comment = solution.Comment, + GroupId = solution.GroupId, + IsModified = isModified, + State = solution.State, + Rating = solution.Rating, + RatingDate = solution.RatingDate, + LecturerId = solution.LecturerId, + LecturerComment = solution.LecturerComment + }); + + solutionId = lastSolution.Id; + } + else + { + solutionId = await _solutionsRepository.AddAsync(solution); + + if (sendNotification) + { + var solutionModel = _mapper.Map(solution); + var course = await _coursesServiceClient.GetCourseByTask(taskId); + var student = await _authServiceClient.GetAccountData(solution.StudentId); + var studentModel = _mapper.Map(student); + + _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); + } + } + + var ratedSolutionModel = _mapper.Map(solution); + _eventBus.Publish(new RateEvent(task, ratedSolutionModel)); + + if (task.Tags.Contains(HomeworkTags.Test) && !string.IsNullOrWhiteSpace(solution.GithubUrl)) + await TrySaveSolutionCommitsInfo(solutionId, solution.GithubUrl); + + return solutionId; + } + private async Task TrySaveSolutionCommitsInfo(long solutionId, string solutionUrl) { var client = CreateGitHubClient(); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs b/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs index 4529e539c..e2fffd25a 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs @@ -11,6 +11,7 @@ public interface ISolutionsServiceClient Task GetUserSolutions(long taskId, string studentId); Task PostSolution(long taskId, PostSolutionModel model); Task PostEmptySolutionWithRate(long taskId, SolutionViewModel solution); + Task PostSolutionWithRate(long taskId, PostSolutionModel model, bool sendNotification = true); Task RateSolution(long solutionId, RateSolutionModel rateSolutionModel); Task MarkSolution(long solutionId); Task DeleteSolution(long solutionId); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs index e6ecdcd93..97a70e169 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs @@ -110,6 +110,27 @@ public async Task PostEmptySolutionWithRate(long taskId, SolutionViewModel model throw new InvalidOperationException(response.ReasonPhrase); } + public async Task PostSolutionWithRate(long taskId, PostSolutionModel model, bool sendNotification = true) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + _solutionServiceUri + + $"api/Solutions/postSolutionWithRate/{taskId}?sendNotification={sendNotification}") + { + Content = new StringContent( + JsonConvert.SerializeObject(model), + Encoding.UTF8, + "application/json") + }; + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + if (!response.IsSuccessStatusCode) + { + throw new ForbiddenException(); + } + } + public async Task RateSolution(long solutionId, RateSolutionModel rateSolutionModel) { using var httpRequest = new HttpRequestMessage( @@ -297,5 +318,6 @@ public async Task Ping() return false; } } + } } diff --git a/hwproj.front/package-lock.json b/hwproj.front/package-lock.json index 634c5f90a..74cd34867 100644 --- a/hwproj.front/package-lock.json +++ b/hwproj.front/package-lock.json @@ -4608,6 +4608,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.16.tgz", "integrity": "sha512-p3DqQi+8QRL5k7jXhXmJZLsE/GqHqyY6PcoA1oNTJr0try48uhTGUOYkgzmqtDaa/qPFO5LP+xCPzZXckGtquQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/api": "6.5.16", @@ -4635,12 +4636,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/api": { "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.16.tgz", "integrity": "sha512-HOsuT8iomqeTMQJrRx5U8nsC7lJTwRr1DhdD0SzlqL4c80S/7uuCy4IZvOt4sYQjOzW5fOo/kamcoBXyLproTA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/channels": "6.5.16", @@ -4674,6 +4677,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/builder-webpack4": { @@ -5072,6 +5076,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.16.tgz", "integrity": "sha512-VylzaWQZaMozEwZPJdyJoz+0jpDa8GRyaqu9TGG6QGv+KU5POoZaGLDkRE7TzWkyyP0KQLo80K99MssZCpgSeg==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5131,6 +5136,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.16.tgz", "integrity": "sha512-pxcNaCj3ItDdicPTXTtmYJE3YC1SjxFrBmHcyrN+nffeNyiMuViJdOOZzzzucTUG0wcOOX8jaSyak+nnHg5H1Q==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5145,6 +5151,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.16.tgz", "integrity": "sha512-LzBOFJKITLtDcbW9jXl0/PaG+4xAz25PK8JxPZpIALbmOpYWOAPcO6V9C2heX6e6NgWFMUxjplkULEk9RCQMNA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -5169,6 +5176,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/core": { @@ -5335,6 +5343,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.16.tgz", "integrity": "sha512-qMZQwmvzpH5F2uwNUllTPg6eZXr2OaYZQRRN8VZJiuorZzDNdAFmiVWMWdkThwmyLEJuQKXxqCL8lMj/7PPM+g==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2" @@ -5435,6 +5444,7 @@ "version": "0.0.2--canary.4566f4d.1", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz", "integrity": "sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==", + "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.15" @@ -5907,6 +5917,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.16.tgz", "integrity": "sha512-ZgeP8a5YV/iuKbv31V8DjPxlV4AzorRiR8OuSt/KqaiYXNXlOoQDz/qMmiNcrshrfLpmkzoq7fSo4T8lWo2UwQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -5928,12 +5939,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz", "integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==", + "dev": true, "license": "ISC", "dependencies": { "core-js": "^3.6.5", @@ -5950,6 +5963,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -5963,6 +5977,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -5975,6 +5990,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -5990,6 +6006,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -6073,6 +6090,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.16.tgz", "integrity": "sha512-hNLctkjaYLRdk1+xYTkC1mg4dYz2wSv6SqbLpcKMbkPHTE0ElhddGPHQqB362md/w9emYXNkt1LSMD8Xk9JzVQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6093,6 +6111,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/ui": { @@ -6719,6 +6738,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.3.tgz", "integrity": "sha512-/CLhCW79JUeLKznI6mbVieGbl4QU5Hfn+6udw1YHZoofASjbQ5zaP5LzAUZYDpRYEjS4/P+DhEgyJ/PQmGGTWw==", + "dev": true, "license": "MIT" }, "node_modules/@types/isomorphic-fetch": { @@ -7069,6 +7089,7 @@ "version": "1.18.8", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", + "dev": true, "license": "MIT" }, "node_modules/@types/webpack-sources": { @@ -17624,6 +17645,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, "license": "MIT" }, "node_modules/is-generator-function": { @@ -17740,6 +17762,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -17817,6 +17840,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18288,13 +18312,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -18844,6 +18861,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true, "license": "MIT" }, "node_modules/map-visit": { @@ -20036,6 +20054,7 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, "license": "MIT", "dependencies": { "map-or-similar": "^1.5.0" @@ -22002,6 +22021,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -22204,6 +22224,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22356,18 +22377,6 @@ "node": ">=6" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/portable-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/portable-fetch/-/portable-fetch-3.0.0.tgz", @@ -26654,6 +26663,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -27530,6 +27540,7 @@ "version": "2.14.4", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "dev": true, "license": "MIT" }, "node_modules/stream-browserify": { @@ -28163,6 +28174,7 @@ "version": "6.0.8", "resolved": "https://registry.npmjs.org/telejson/-/telejson-6.0.8.tgz", "integrity": "sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==", + "dev": true, "license": "MIT", "dependencies": { "@types/is-function": "^1.0.0", @@ -28179,6 +28191,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -28694,6 +28707,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -29586,6 +29600,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/util.promisify": { diff --git a/hwproj.front/src/api/ApiSingleton.ts b/hwproj.front/src/api/ApiSingleton.ts index 1886ef4ce..c1fec086e 100644 --- a/hwproj.front/src/api/ApiSingleton.ts +++ b/hwproj.front/src/api/ApiSingleton.ts @@ -9,7 +9,9 @@ import { StatisticsApi, SystemApi, FilesApi, - CourseGroupsApi + CourseGroupsApi, + LtiToolsApi, + LtiAuthApi, } from "."; import AuthService from "../services/AuthService"; import CustomFilesApi from "./CustomFilesApi"; @@ -29,6 +31,8 @@ class Api { readonly authService: AuthService; readonly customFilesApi: CustomFilesApi; readonly filesApi: FilesApi; + readonly ltiToolsApi: LtiToolsApi; + readonly ltiAuthApi: LtiAuthApi; constructor( accountApi: AccountApi, @@ -43,7 +47,9 @@ class Api { systemApi: SystemApi, authService: AuthService, customFilesApi: CustomFilesApi, - filesApi: FilesApi + filesApi: FilesApi, + ltiToolsApi: LtiToolsApi, + ltiAuthApi: LtiAuthApi ) { this.accountApi = accountApi; this.expertsApi = expertsApi; @@ -58,6 +64,8 @@ class Api { this.authService = authService; this.customFilesApi = customFilesApi; this.filesApi = filesApi; + this.ltiToolsApi = ltiToolsApi; + this.ltiAuthApi = ltiAuthApi; } } @@ -91,6 +99,8 @@ ApiSingleton = new Api( new SystemApi({basePath: basePath}), authService, new CustomFilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), - new FilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}) + new FilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), + new LtiToolsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), + new LtiAuthApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}) ); -export default ApiSingleton; \ No newline at end of file +export default ApiSingleton; diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index e42934f1c..be8afaf0d 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -157,6 +157,19 @@ export interface ActionOptions { */ sendNotification?: boolean; } +/** + * + * @export + * @enum {string} + */ +export enum ActivityProgress { + None = 'None', + Completed = 'Completed', + Initialized = 'Initialized', + InProgress = 'InProgress', + Started = 'Started', + Submitted = 'Submitted' +} /** * * @export @@ -483,6 +496,12 @@ export interface CourseViewModel { * @memberof CourseViewModel */ groups?: Array; + /** + * + * @type {string} + * @memberof CourseViewModel + */ + ltiToolName?: string; /** * * @type {Array} @@ -550,6 +569,12 @@ export interface CreateCourseViewModel { * @memberof CreateCourseViewModel */ baseCourseId?: number; + /** + * + * @type {string} + * @memberof CreateCourseViewModel + */ + ltiToolName?: string; } /** * @@ -1039,6 +1064,19 @@ export interface GithubCredentials { */ githubId?: string; } +/** + * + * @export + * @enum {string} + */ +export enum GradingProgress { + None = 'None', + Failed = 'Failed', + FullyGraded = 'FullyGraded', + NotReady = 'NotReady', + Pending = 'Pending', + PendingManual = 'PendingManual' +} /** * * @export @@ -1230,6 +1268,12 @@ export interface HomeworkTaskViewModel { * @memberof HomeworkTaskViewModel */ criteria?: Array; + /** + * + * @type {LtiLaunchData} + * @memberof HomeworkTaskViewModel + */ + ltiLaunchData?: LtiLaunchData; } /** * @@ -1510,6 +1554,94 @@ export interface LoginViewModel { */ rememberMe: boolean; } +/** + * + * @export + * @interface LtiDeepLinkReturnBody + */ +export interface LtiDeepLinkReturnBody { + /** + * + * @type {Array} + * @memberof LtiDeepLinkReturnBody + */ + form?: Array; +} +/** + * + * @export + * @interface LtiLaunchData + */ +export interface LtiLaunchData { + /** + * + * @type {string} + * @memberof LtiLaunchData + */ + ltiLaunchUrl?: string; + /** + * + * @type {string} + * @memberof LtiLaunchData + */ + customParams?: string; +} +/** + * + * @export + * @interface LtiTokenBody + */ +export interface LtiTokenBody { + /** + * + * @type {Array} + * @memberof LtiTokenBody + */ + form?: Array; +} +/** + * + * @export + * @interface LtiToolDto + */ +export interface LtiToolDto { + /** + * + * @type {string} + * @memberof LtiToolDto + */ + name?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + clientId?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + jwksEndpoint?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + initiateLoginUri?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + launchUrl?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + deepLink?: string; +} /** * * @export @@ -1529,6 +1661,112 @@ export interface MentorToAssignedStudentsDTO { */ selectedStudentsIds?: Array; } +/** + * + * @export + * @interface MocktoolCallbackBody + */ +export interface MocktoolCallbackBody { + /** + * + * @type {string} + * @memberof MocktoolCallbackBody + */ + idToken?: string; +} +/** + * + * @export + * @interface MocktoolLoginBody + */ +export interface MocktoolLoginBody { + /** + * + * @type {string} + * @memberof MocktoolLoginBody + */ + iss?: string; + /** + * + * @type {string} + * @memberof MocktoolLoginBody + */ + loginHint?: string; + /** + * + * @type {string} + * @memberof MocktoolLoginBody + */ + ltiMessageHint?: string; +} +/** + * + * @export + * @interface MocktoolSendscoreBody + */ +export interface MocktoolSendscoreBody { + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + lineItemUrl?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + userId?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + platformIss?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + taskId?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + returnUrl?: string; +} +/** + * + * @export + * @interface MocktoolSubmitselectionBody + */ +export interface MocktoolSubmitselectionBody { + /** + * + * @type {Array} + * @memberof MocktoolSubmitselectionBody + */ + selectedIds?: Array; + /** + * + * @type {string} + * @memberof MocktoolSubmitselectionBody + */ + returnUrl?: string; + /** + * + * @type {string} + * @memberof MocktoolSubmitselectionBody + */ + data?: string; + /** + * + * @type {string} + * @memberof MocktoolSubmitselectionBody + */ + platformIssuer?: string; +} /** * * @export @@ -1731,6 +1969,12 @@ export interface PostTaskViewModel { * @memberof PostTaskViewModel */ criteria?: Array; + /** + * + * @type {LtiLaunchData} + * @memberof PostTaskViewModel + */ + ltiLaunchData?: LtiLaunchData; } /** * @@ -1960,175 +2204,309 @@ export interface ScopeDTO { /** * * @export - * @interface SolutionActualityDto + * @interface Score */ -export interface SolutionActualityDto { +export interface Score { /** * - * @type {SolutionActualityPart} - * @memberof SolutionActualityDto + * @type {ActivityProgress} + * @memberof Score */ - commitsActuality?: SolutionActualityPart; + activityProgress?: ActivityProgress; /** * - * @type {SolutionActualityPart} - * @memberof SolutionActualityDto + * @type {string} + * @memberof Score */ - testsActuality?: SolutionActualityPart; -} -/** - * - * @export - * @interface SolutionActualityPart - */ -export interface SolutionActualityPart { + comment?: string; /** * - * @type {boolean} - * @memberof SolutionActualityPart + * @type {GradingProgress} + * @memberof Score */ - isActual?: boolean; + gradingProgress?: GradingProgress; /** * - * @type {string} - * @memberof SolutionActualityPart + * @type {number} + * @memberof Score */ - comment?: string; + scoreGiven?: number; + /** + * + * @type {number} + * @memberof Score + */ + scoreMaximum?: number; + /** + * + * @type {Date} + * @memberof Score + */ + timestamp?: Date; /** * * @type {string} - * @memberof SolutionActualityPart + * @memberof Score */ - additionalData?: string; + userId?: string; } /** * * @export - * @interface SolutionDto + * @interface Solution */ -export interface SolutionDto { +export interface Solution { /** * * @type {number} - * @memberof SolutionDto + * @memberof Solution */ id?: number; /** * * @type {string} - * @memberof SolutionDto + * @memberof Solution */ githubUrl?: string; /** * * @type {string} - * @memberof SolutionDto + * @memberof Solution */ comment?: string; /** * * @type {SolutionState} - * @memberof SolutionDto + * @memberof Solution */ state?: SolutionState; /** * * @type {number} - * @memberof SolutionDto + * @memberof Solution */ rating?: number; /** * * @type {string} - * @memberof SolutionDto + * @memberof Solution */ studentId?: string; /** * * @type {string} - * @memberof SolutionDto + * @memberof Solution */ lecturerId?: string; /** * * @type {number} - * @memberof SolutionDto + * @memberof Solution */ groupId?: number; /** * * @type {number} - * @memberof SolutionDto + * @memberof Solution */ taskId?: number; /** * * @type {Date} - * @memberof SolutionDto + * @memberof Solution */ publicationDate?: Date; /** * * @type {boolean} - * @memberof SolutionDto + * @memberof Solution */ isModified?: boolean; /** * * @type {Date} - * @memberof SolutionDto + * @memberof Solution */ ratingDate?: Date; /** * * @type {string} - * @memberof SolutionDto + * @memberof Solution */ lecturerComment?: string; } /** * * @export - * @interface SolutionPreviewView + * @interface SolutionActualityDto */ -export interface SolutionPreviewView { - /** - * - * @type {number} - * @memberof SolutionPreviewView - */ - solutionId?: number; +export interface SolutionActualityDto { /** * - * @type {AccountDataDto} - * @memberof SolutionPreviewView + * @type {SolutionActualityPart} + * @memberof SolutionActualityDto */ - student?: AccountDataDto; + commitsActuality?: SolutionActualityPart; /** * - * @type {string} - * @memberof SolutionPreviewView + * @type {SolutionActualityPart} + * @memberof SolutionActualityDto */ - courseTitle?: string; + testsActuality?: SolutionActualityPart; +} +/** + * + * @export + * @interface SolutionActualityPart + */ +export interface SolutionActualityPart { /** * - * @type {number} - * @memberof SolutionPreviewView + * @type {boolean} + * @memberof SolutionActualityPart */ - courseId?: number; + isActual?: boolean; /** * * @type {string} - * @memberof SolutionPreviewView + * @memberof SolutionActualityPart */ - homeworkTitle?: string; + comment?: string; /** * * @type {string} - * @memberof SolutionPreviewView + * @memberof SolutionActualityPart */ - taskTitle?: string; - /** + additionalData?: string; +} +/** + * + * @export + * @interface SolutionDto + */ +export interface SolutionDto { + /** + * + * @type {number} + * @memberof SolutionDto + */ + id?: number; + /** + * + * @type {string} + * @memberof SolutionDto + */ + githubUrl?: string; + /** + * + * @type {string} + * @memberof SolutionDto + */ + comment?: string; + /** + * + * @type {SolutionState} + * @memberof SolutionDto + */ + state?: SolutionState; + /** + * + * @type {number} + * @memberof SolutionDto + */ + rating?: number; + /** + * + * @type {string} + * @memberof SolutionDto + */ + studentId?: string; + /** + * + * @type {string} + * @memberof SolutionDto + */ + lecturerId?: string; + /** + * + * @type {number} + * @memberof SolutionDto + */ + groupId?: number; + /** + * + * @type {number} + * @memberof SolutionDto + */ + taskId?: number; + /** + * + * @type {Date} + * @memberof SolutionDto + */ + publicationDate?: Date; + /** + * + * @type {boolean} + * @memberof SolutionDto + */ + isModified?: boolean; + /** + * + * @type {Date} + * @memberof SolutionDto + */ + ratingDate?: Date; + /** + * + * @type {string} + * @memberof SolutionDto + */ + lecturerComment?: string; +} +/** + * + * @export + * @interface SolutionPreviewView + */ +export interface SolutionPreviewView { + /** + * + * @type {number} + * @memberof SolutionPreviewView + */ + solutionId?: number; + /** + * + * @type {AccountDataDto} + * @memberof SolutionPreviewView + */ + student?: AccountDataDto; + /** + * + * @type {string} + * @memberof SolutionPreviewView + */ + courseTitle?: string; + /** + * + * @type {number} + * @memberof SolutionPreviewView + */ + courseId?: number; + /** + * + * @type {string} + * @memberof SolutionPreviewView + */ + homeworkTitle?: string; + /** + * + * @type {string} + * @memberof SolutionPreviewView + */ + taskTitle?: string; + /** * * @type {number} * @memberof SolutionPreviewView @@ -2236,6 +2614,25 @@ export interface SolutionViewModel { */ ratingDate?: Date; } +/** + * + * @export + * @interface StringStringValuesKeyValuePair + */ +export interface StringStringValuesKeyValuePair { + /** + * + * @type {string} + * @memberof StringStringValuesKeyValuePair + */ + key?: string; + /** + * + * @type {Array} + * @memberof StringStringValuesKeyValuePair + */ + value?: Array; +} /** * * @export @@ -2751,6 +3148,12 @@ export interface UpdateCourseViewModel { * @memberof UpdateCourseViewModel */ isCompleted?: boolean; + /** + * + * @type {string} + * @memberof UpdateCourseViewModel + */ + ltiToolName?: string; } /** * @@ -2908,6 +3311,12 @@ export interface UserTaskSolutionsPageData { * @memberof UserTaskSolutionsPageData */ courseId?: number; + /** + * + * @type {string} + * @memberof UserTaskSolutionsPageData + */ + ltiToolName?: string; /** * * @type {Array} @@ -7321,6 +7730,1319 @@ export class HomeworksApi extends BaseAPI { return HomeworksApiFp(this.configuration).homeworksUpdateHomework(homeworkId, body, options)(this.fetch, this.basePath); } +} +/** + * JwksApi - fetch parameter creator + * @export + */ +export const JwksApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + jwksGetJwks(options: any = {}): FetchArgs { + const localVarPath = `/api/lti/jwks`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * JwksApi - functional programming interface + * @export + */ +export const JwksApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + jwksGetJwks(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = JwksApiFetchParamCreator(configuration).jwksGetJwks(options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + } +}; + +/** + * JwksApi - factory interface + * @export + */ +export const JwksApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + jwksGetJwks(options?: any) { + return JwksApiFp(configuration).jwksGetJwks(options)(fetch, basePath); + }, + }; +}; + +/** + * JwksApi - object-oriented interface + * @export + * @class JwksApi + * @extends {BaseAPI} + */ +export class JwksApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JwksApi + */ + public jwksGetJwks(options?: any) { + return JwksApiFp(this.configuration).jwksGetJwks(options)(this.fetch, this.basePath); + } + +} +/** + * LtiAccessTokenApi - fetch parameter creator + * @export + */ +export const LtiAccessTokenApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAccessTokenGetToken(form?: Array, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/token`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (form) { + form.forEach((element) => { + localVarFormParams.append('form', element as any); + }) + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LtiAccessTokenApi - functional programming interface + * @export + */ +export const LtiAccessTokenApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAccessTokenGetToken(form?: Array, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAccessTokenApiFetchParamCreator(configuration).ltiAccessTokenGetToken(form, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + } +}; + +/** + * LtiAccessTokenApi - factory interface + * @export + */ +export const LtiAccessTokenApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAccessTokenGetToken(form?: Array, options?: any) { + return LtiAccessTokenApiFp(configuration).ltiAccessTokenGetToken(form, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiAccessTokenApi - object-oriented interface + * @export + * @class LtiAccessTokenApi + * @extends {BaseAPI} + */ +export class LtiAccessTokenApi extends BaseAPI { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAccessTokenApi + */ + public ltiAccessTokenGetToken(form?: Array, options?: any) { + return LtiAccessTokenApiFp(this.configuration).ltiAccessTokenGetToken(form, options)(this.fetch, this.basePath); + } + +} +/** + * LtiAssignmentsGradesControllersApi - fetch parameter creator + * @export + */ +export const LtiAssignmentsGradesControllersApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options: any = {}): FetchArgs { + // verify required parameter 'taskId' is not null or undefined + if (taskId === null || taskId === undefined) { + throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling ltiAssignmentsGradesControllersUpdateTaskScore.'); + } + const localVarPath = `/api/lti/lineItem/{taskId}/scores` + .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("Score" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(body || {}) : (body || ""); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LtiAssignmentsGradesControllersApi - functional programming interface + * @export + */ +export const LtiAssignmentsGradesControllersApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAssignmentsGradesControllersApiFetchParamCreator(configuration).ltiAssignmentsGradesControllersUpdateTaskScore(taskId, body, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + } +}; + +/** + * LtiAssignmentsGradesControllersApi - factory interface + * @export + */ +export const LtiAssignmentsGradesControllersApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options?: any) { + return LtiAssignmentsGradesControllersApiFp(configuration).ltiAssignmentsGradesControllersUpdateTaskScore(taskId, body, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiAssignmentsGradesControllersApi - object-oriented interface + * @export + * @class LtiAssignmentsGradesControllersApi + * @extends {BaseAPI} + */ +export class LtiAssignmentsGradesControllersApi extends BaseAPI { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAssignmentsGradesControllersApi + */ + public ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options?: any) { + return LtiAssignmentsGradesControllersApiFp(this.configuration).ltiAssignmentsGradesControllersUpdateTaskScore(taskId, body, options)(this.fetch, this.basePath); + } + +} +/** + * LtiAuthApi - fetch parameter creator + * @export + */ +export const LtiAuthApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} [clientId] + * @param {string} [redirectUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/authorize`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (clientId !== undefined) { + localVarQueryParameter['client_id'] = clientId; + } + + if (redirectUri !== undefined) { + localVarQueryParameter['redirect_uri'] = redirectUri; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (nonce !== undefined) { + localVarQueryParameter['nonce'] = nonce; + } + + if (ltiMessageHint !== undefined) { + localVarQueryParameter['lti_message_hint'] = ltiMessageHint; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthCloseLtiSession(options: any = {}): FetchArgs { + const localVarPath = `/api/lti/closeLtiSession`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolName] + * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/start`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (resourceLinkId !== undefined) { + localVarQueryParameter['resourceLinkId'] = resourceLinkId; + } + + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (toolName !== undefined) { + localVarQueryParameter['toolName'] = toolName; + } + + if (ltiLaunchUrl !== undefined) { + localVarQueryParameter['ltiLaunchUrl'] = ltiLaunchUrl; + } + + if (ltiCustomParams !== undefined) { + localVarQueryParameter['ltiCustomParams'] = ltiCustomParams; + } + + if (isDeepLink !== undefined) { + localVarQueryParameter['isDeepLink'] = isDeepLink; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LtiAuthApi - functional programming interface + * @export + */ +export const LtiAuthApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {string} [clientId] + * @param {string} [redirectUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthCloseLtiSession(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthCloseLtiSession(options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolName] + * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolName, ltiLaunchUrl, ltiCustomParams, isDeepLink, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + } +}; + +/** + * LtiAuthApi - factory interface + * @export + */ +export const LtiAuthApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {string} [clientId] + * @param {string} [redirectUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options)(fetch, basePath); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthCloseLtiSession(options?: any) { + return LtiAuthApiFp(configuration).ltiAuthCloseLtiSession(options)(fetch, basePath); + }, + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolName] + * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolName, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiAuthApi - object-oriented interface + * @export + * @class LtiAuthApi + * @extends {BaseAPI} + */ +export class LtiAuthApi extends BaseAPI { + /** + * + * @param {string} [clientId] + * @param {string} [redirectUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAuthApi + */ + public ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options)(this.fetch, this.basePath); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAuthApi + */ + public ltiAuthCloseLtiSession(options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthCloseLtiSession(options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolName] + * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAuthApi + */ + public ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolName, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(this.fetch, this.basePath); + } + +} +/** + * LtiToolsApi - fetch parameter creator + * @export + */ +export const LtiToolsApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} id + * @param {string} [name] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGet(id: string, name?: string, options: any = {}): FetchArgs { + // verify required parameter 'id' is not null or undefined + if (id === null || id === undefined) { + throw new RequiredError('id','Required parameter id was null or undefined when calling ltiToolsGet.'); + } + const localVarPath = `/api/lti/tools/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (name !== undefined) { + localVarQueryParameter['name'] = name; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGetAll(options: any = {}): FetchArgs { + const localVarPath = `/api/lti/tools`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LtiToolsApi - functional programming interface + * @export + */ +export const LtiToolsApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {string} id + * @param {string} [name] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGet(id: string, name?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiToolsApiFetchParamCreator(configuration).ltiToolsGet(id, name, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGetAll(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = LtiToolsApiFetchParamCreator(configuration).ltiToolsGetAll(options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, + } +}; + +/** + * LtiToolsApi - factory interface + * @export + */ +export const LtiToolsApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {string} id + * @param {string} [name] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGet(id: string, name?: string, options?: any) { + return LtiToolsApiFp(configuration).ltiToolsGet(id, name, options)(fetch, basePath); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGetAll(options?: any) { + return LtiToolsApiFp(configuration).ltiToolsGetAll(options)(fetch, basePath); + }, + }; +}; + +/** + * LtiToolsApi - object-oriented interface + * @export + * @class LtiToolsApi + * @extends {BaseAPI} + */ +export class LtiToolsApi extends BaseAPI { + /** + * + * @param {string} id + * @param {string} [name] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiToolsApi + */ + public ltiToolsGet(id: string, name?: string, options?: any) { + return LtiToolsApiFp(this.configuration).ltiToolsGet(id, name, options)(this.fetch, this.basePath); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiToolsApi + */ + public ltiToolsGetAll(options?: any) { + return LtiToolsApiFp(this.configuration).ltiToolsGetAll(options)(this.fetch, this.basePath); + } + +} +/** + * MockToolApi - fetch parameter creator + * @export + */ +export const MockToolApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolCallback(idToken?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/callback`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (idToken !== undefined) { + localVarFormParams.set('id_token', idToken as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolGetJwks(options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/jwks`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/login`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (iss !== undefined) { + localVarFormParams.set('iss', iss as any); + } + + if (loginHint !== undefined) { + localVarFormParams.set('login_hint', loginHint as any); + } + + if (ltiMessageHint !== undefined) { + localVarFormParams.set('lti_message_hint', ltiMessageHint as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/send-score`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (lineItemUrl !== undefined) { + localVarFormParams.set('lineItemUrl', lineItemUrl as any); + } + + if (userId !== undefined) { + localVarFormParams.set('userId', userId as any); + } + + if (platformIss !== undefined) { + localVarFormParams.set('platformIss', platformIss as any); + } + + if (taskId !== undefined) { + localVarFormParams.set('taskId', taskId as any); + } + + if (returnUrl !== undefined) { + localVarFormParams.set('returnUrl', returnUrl as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/submit-selection`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (selectedIds) { + selectedIds.forEach((element) => { + localVarFormParams.append('selectedIds', element as any); + }) + } + + if (returnUrl !== undefined) { + localVarFormParams.set('returnUrl', returnUrl as any); + } + + if (data !== undefined) { + localVarFormParams.set('data', data as any); + } + + if (platformIssuer !== undefined) { + localVarFormParams.set('platformIssuer', platformIssuer as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * MockToolApi - functional programming interface + * @export + */ +export const MockToolApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolCallback(idToken?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolCallback(idToken, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolGetJwks(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolGetJwks(options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolSendScore(lineItemUrl, userId, platformIss, taskId, returnUrl, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolSubmitDeepLinkingSelection(selectedIds, returnUrl, data, platformIssuer, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, + } +}; + +/** + * MockToolApi - factory interface + * @export + */ +export const MockToolApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolCallback(idToken?: string, options?: any) { + return MockToolApiFp(configuration).mockToolCallback(idToken, options)(fetch, basePath); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolGetJwks(options?: any) { + return MockToolApiFp(configuration).mockToolGetJwks(options)(fetch, basePath); + }, + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { + return MockToolApiFp(configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options)(fetch, basePath); + }, + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options?: any) { + return MockToolApiFp(configuration).mockToolSendScore(lineItemUrl, userId, platformIss, taskId, returnUrl, options)(fetch, basePath); + }, + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options?: any) { + return MockToolApiFp(configuration).mockToolSubmitDeepLinkingSelection(selectedIds, returnUrl, data, platformIssuer, options)(fetch, basePath); + }, + }; +}; + +/** + * MockToolApi - object-oriented interface + * @export + * @class MockToolApi + * @extends {BaseAPI} + */ +export class MockToolApi extends BaseAPI { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolCallback(idToken?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolCallback(idToken, options)(this.fetch, this.basePath); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolGetJwks(options?: any) { + return MockToolApiFp(this.configuration).mockToolGetJwks(options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolSendScore(lineItemUrl, userId, platformIss, taskId, returnUrl, options)(this.fetch, this.basePath); + } + + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolSubmitDeepLinkingSelection(selectedIds, returnUrl, data, platformIssuer, options)(this.fetch, this.basePath); + } + } /** * NotificationsApi - fetch parameter creator diff --git a/hwproj.front/src/components/Courses/AddCourseInfo.tsx b/hwproj.front/src/components/Courses/AddCourseInfo.tsx index 1f113bf03..0aa83c452 100644 --- a/hwproj.front/src/components/Courses/AddCourseInfo.tsx +++ b/hwproj.front/src/components/Courses/AddCourseInfo.tsx @@ -109,6 +109,9 @@ const AddCourseInfo: FC = ({state, setState}) => { setState(prev => ({ ...prev, programName: newValue || '', + selectedGroups: [], + isGroupFromList: false, + fetchStudents: false, })); }} options={state.programNames} @@ -163,6 +166,35 @@ const AddCourseInfo: FC = ({state, setState}) => { )) } /> + option.name ?? ""} + value={ + state.ltiToolName == null + ? null + : state.ltiTools.find(tool => tool.name === state.ltiToolName) ?? null + } + onChange={(_, newValue) => { + setState(prev => ({ + ...prev, + ltiToolName: newValue?.name ?? undefined, + })); + }} + renderInput={(params) => ( + + )} + clearOnEscape + /> {state.isGroupFromList && { {tabValue === "homeworks" && = (props) => { })) } - const addNewTask = (homework: HomeworkViewModel) => { - const id = newTaskCounter - const tags = homework.tags! - const isTest = tags.includes(TestTag) - const isBonus = tags.includes(BonusTag) - - const ratingCandidate = Lodash(homeworks - .map(h => h.tasks![0]) + const calculateSuggestedRating = (homework: HomeworkViewModel) => { + const tags = homework.tags! + const isTest = tags.includes(TestTag) + const isBonus = tags.includes(BonusTag) + + return Lodash(homeworks + .map(h => h.tasks![0]) .filter(x => { if (x === undefined) return false const xIsTest = isTestWork(x) @@ -626,13 +627,18 @@ export const CourseExperimental: FC = (props) => { return x.id! > 0 && (isTest && xIsTest || isBonus && xIsBonus || !isTest && !isBonus && !xIsTest && !xIsBonus) })) .map(x => x.maxRating!) - .groupBy(x => [x]) - .entries() - .sortBy(x => x[1].length).last()?.[1][0] + .groupBy(x => [x]) + .entries() + .sortBy(x => x[1].length).last()?.[1][0] || 10 + } + + const addNewTask = (homework: HomeworkViewModel) => { + const id = newTaskCounter + const ratingCandidate = calculateSuggestedRating(homework) const task = { homeworkId: homework.id, - maxRating: ratingCandidate || 10, + maxRating: ratingCandidate, suggestedMaxRating: ratingCandidate, title: `Новая задача`, tags: homework.tags, @@ -649,8 +655,35 @@ export const CourseExperimental: FC = (props) => { id: id } })) - setNewTaskCounter(id - 1) - } + setNewTaskCounter(id - 1) + } + + const handleLtiImport = (items: LtiItemDto[], homework: HomeworkViewModel) => { + let currentCounter = newTaskCounter + const suggestedRating = calculateSuggestedRating(homework) + + items.forEach(item => { + if (!item.ltiLaunchData) return + const targetRating = item.scoreMaximum > 0 ? item.scoreMaximum : suggestedRating + + const task = { + id: currentCounter, + homeworkId: homework.id, + title: item.title || "External Task", + description: item.text?.trim() || "", + maxRating: targetRating, + suggestedMaxRating: targetRating, + tags: homework.tags, + isDeferred: homework.isDeferred, + criteria: [], + ltiLaunchData: item.ltiLaunchData, + } + props.onTaskUpdate({task}) + currentCounter-- + }) + + setNewTaskCounter(currentCounter) + } const renderHomework = (homework: HomeworkViewModel & { isModified?: boolean }) => { const filesInfo = id ? FileInfoConverter.getCourseUnitFilesInfo(courseFilesInfo, CourseUnitType.Homework, id) : [] @@ -743,7 +776,7 @@ export const CourseExperimental: FC = (props) => { - // useFlexGap обязателен: колонки меняются местами через order, а по умолчанию Stack раздаёт + // useFlexGap обязателен: колонки меняются местами через order, а по умолчанию Stack раздаёт // отступы margin'ом соседним по разметке детям — на мобильных зазор оказывался не между // колонками, а над верхней из них return = (props) => { startIcon={} sx={{textTransform: "none", borderRadius: "10px", flexShrink: 0}} > - Задание - } - + Задание + } + @@ -870,11 +903,11 @@ export const CourseExperimental: FC = (props) => { - {x.title}{getTip(x)} + sx={{fontSize: "1rem", fontWeight: 600, lineHeight: 1.3}} + color={x.isDeferred + ? "textSecondary" + : x.tags!.includes(TestTag) ? "primary" : "textPrimary"}> + {x.title}{getTip(x)} {x.isDeferred && !x.publicationDateNotSet && = (props) => { - {t.title}{getTip(t)} - + color={t.isDeferred ? "textSecondary" : "textPrimary"}> + {t.title}{getTip(t)} + {t.ltiLaunchData && + + + } + {renderTaskDeadline(t)} )} @@ -924,24 +967,32 @@ export const CourseExperimental: FC = (props) => { fontStyle: "italic", color: "text.disabled", }}> - Без задач - - } - {x.id! < 0 && - } - ; - })} + Без задач + + } + {x.id! < 0 && + + + {props.ltiToolName && + handleLtiImport(items, x)} + />} + } + ; + })} - - + + {isHomework ? renderHomework(selectedItem as HomeworkViewModel) : renderTask(selectedItem as HomeworkTaskViewModel, selectedItemHomework!)} diff --git a/hwproj.front/src/components/Courses/CreateCourse.tsx b/hwproj.front/src/components/Courses/CreateCourse.tsx index ee8ef4353..b9f5cb439 100644 --- a/hwproj.front/src/components/Courses/CreateCourse.tsx +++ b/hwproj.front/src/components/Courses/CreateCourse.tsx @@ -79,6 +79,8 @@ export const CreateCourse: FC = () => { selectedGroups: [], fetchingGroups: false, courseIsLoading: false, + ltiTools: [], + ltiToolName: undefined, }) const {activeStep, completedSteps, baseCourses, selectedBaseCourse} = state @@ -142,6 +144,17 @@ export const CreateCourse: FC = () => { {variant: "warning", autoHideDuration: 4000}, ) } + + try { + const ltiTools = await ApiSingleton.ltiToolsApi.ltiToolsGetAll() + setState(prev => ({...prev, ltiTools})) + } catch (e) { + console.error("Ошибка при загрузке LTI-инструментов:", e) + enqueueSnackbar( + "Не удалось загрузить список LTI-инструментов", + {variant: "warning", autoHideDuration: 4000}, + ) + } } loadData() @@ -172,6 +185,7 @@ export const CreateCourse: FC = () => { isOpen: true, baseCourseId: selectedBaseCourse?.id, fetchStudents: state.isGroupFromList ? state.fetchStudents : false, + ltiToolName: state.ltiToolName, } try { setCourseIsLoading(true) diff --git a/hwproj.front/src/components/Courses/ICreateCourseState.tsx b/hwproj.front/src/components/Courses/ICreateCourseState.tsx index add096860..dd9662692 100644 --- a/hwproj.front/src/components/Courses/ICreateCourseState.tsx +++ b/hwproj.front/src/components/Courses/ICreateCourseState.tsx @@ -1,5 +1,5 @@ import {Dispatch, SetStateAction} from "react" -import {CoursePreviewView} from "api"; +import {CoursePreviewView, LtiToolDto} from "api"; export enum CreateCourseStep { SelectBaseCourseStep = 0, @@ -33,6 +33,9 @@ export interface ICreateCourseState { fetchingGroups: boolean; courseIsLoading: boolean; + + ltiTools: LtiToolDto[]; + ltiToolName: string | undefined; } export interface IStepComponentProps { diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 8f40b7bd1..54dd9dbca 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -401,7 +401,8 @@ const CourseHomeworkEditor: FC<{ ...t, title: t.title!, maxRating: t.maxRating!, - criteria: t.criteria || [] + criteria: t.criteria || [], + ltiLaunchData: t.ltiLaunchData, } return task }) : [] diff --git a/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx b/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx new file mode 100644 index 000000000..c7dbf6984 --- /dev/null +++ b/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx @@ -0,0 +1,109 @@ +import React, { FC, useState } from "react"; +import { LoadingButton } from "@mui/lab"; +import ApiSingleton from "../../api/ApiSingleton"; +import {Button, Dialog, DialogActions, DialogContent, DialogTitle} from "@mui/material"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import {LtiLaunchData} from "@/api"; + +interface LtiLaunchButtonProps { + courseId: number; + toolName: string; + taskId: number; + ltiLaunchData: LtiLaunchData; +} + +export const LtiLaunchButton: FC = ({ courseId, toolName, taskId, ltiLaunchData }) => { + const [isLoading, setIsLoading] = useState(false); + const [openDialog, setOpenDialog] = useState(false); + + const submitLtiForm = (formData: any) => { + const windowName = `lti_launch_task_${taskId}`; + window.open('about:blank', windowName); + + const form = document.createElement("form"); + form.method = formData.method; + form.action = formData.actionUrl; + form.target = windowName; + + if (formData.fields) { + Object.entries(formData.fields).forEach(([key, value]) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = key; + input.value = String(value); + form.appendChild(input); + }); + } + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + }; + + const handleLaunch = async () => { + setOpenDialog(false); + setIsLoading(true); + try { + const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( + String(taskId), + String(courseId), + toolName, + ltiLaunchData.ltiLaunchUrl, + ltiLaunchData.customParams, + false + ); + + let dto = response; + if (response && typeof (response as any).json === 'function') { + dto = await (response as any).json(); + } + + submitLtiForm(dto); + } catch (e) { + console.error("Ошибка запуска LTI:", e); + alert("Не удалось запустить задачу. Обратитесь к администратору."); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + setOpenDialog(true)} + loading={isLoading} + > + Решить задачу + + + setOpenDialog(false)} + aria-labelledby="lti-warning-title" + aria-describedby="lti-warning-desc" + > + + Внимание + + + + Вы переходите к решению задачи во внешней системе. +

+ Обратите внимание: баллы за решение могут появиться в HwProj не сразу, а с небольшой задержкой после завершения работы. +
+
+ + + + +
+ + ); +}; \ No newline at end of file diff --git a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx index adedaded6..9a7eb8260 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx @@ -20,11 +20,13 @@ import {appBarStateManager} from "../AppBar"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import {LtiLaunchButton} from "@/components/Solutions/LtiLaunchButton"; interface ITaskSolutionsState { isLoaded: boolean addSolution: boolean courseId: number + ltiToolName: string homeworkGroupedSolutions: HomeworksGroupUserTaskSolutions[] courseMates: AccountDataDto[] } @@ -147,6 +149,7 @@ const TaskSolutionsPage: FC = () => { const [taskSolutionPage, setTaskSolutionPage] = useState({ isLoaded: false, courseId: 0, + ltiToolName: "", addSolution: false, homeworkGroupedSolutions: [], courseMates: [] @@ -164,7 +167,20 @@ const TaskSolutionsPage: FC = () => { const showOnlyNotSolved = filterState.some(x => x === "Только нерешенные") useEffect(() => { - getSolutions() + getSolutions(); + + const handleLtiMessage = (event: MessageEvent) => { + + if (event.data === 'lti_success_refresh') { + getSolutions(); + } + }; + + window.addEventListener("message", handleLtiMessage); + + return () => { + window.removeEventListener("message", handleLtiMessage); + }; }, []) useEffect(() => { @@ -182,12 +198,13 @@ const TaskSolutionsPage: FC = () => { isLoaded: true, addSolution: false, courseId: pageData.courseId!, + ltiToolName: pageData.ltiToolName!, homeworkGroupedSolutions: pageData.taskSolutions!, courseMates: pageData.courseMates!, }) } - const {homeworkGroupedSolutions, courseId, courseMates} = taskSolutionPage + const {homeworkGroupedSolutions, courseId, courseMates, ltiToolName} = taskSolutionPage const student = courseMates.find(x => x.userId === userId)! useEffect(() => { @@ -261,6 +278,42 @@ const TaskSolutionsPage: FC = () => { })) } + const renderSolutionButton = () => { + if (task.ltiLaunchData) { + return ( + + ) + } + + if (task.canSendSolution) { + return ( + + ); + } + + return null + } + const renderRatingChip = (solutionsDescription: string, color: string, lastRatedSolution: SolutionDto) => { return {solutionsDescription}}> @@ -331,22 +384,7 @@ const TaskSolutionsPage: FC = () => { } label={Только нерешенные} /> - {task.canSendSolution && } + {renderSolutionButton()} {currentHomeworksGroup && taskIndexInHomework !== -1 && currentHomeworksGroup.homeworkSolutions!.length > 1 && diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index b17b829f0..32b366f9c 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -384,9 +384,11 @@ const CourseTaskEditor: FC<{ tags: isBonusExplicit ? [...homework.tags!, BonusTag] : homework.tags!, hasErrors: hasErrors, criteria: criteria, + ltiLaunchData: props.speculativeTask.ltiLaunchData, } props.onUpdate({task: update}); - }, [title, description, maxRating, metadata, isBonusExplicit, hasErrors, criteria]); + }, [title, description, maxRating, metadata, isBonusExplicit, hasErrors, criteria, + props.speculativeTask.ltiLaunchData]); useEffect(() => { setHasErrors(!title || maxRating <= 0 || metadata?.hasErrors === true || criteriaHasErrors) @@ -405,6 +407,7 @@ const CourseTaskEditor: FC<{ maxRating: maxRating, actionOptions: editOptions, criteria: criteria, + ltiLaunchData: props.speculativeTask.ltiLaunchData, }; const updatedTask = isNewTask diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx new file mode 100644 index 000000000..98aba6725 --- /dev/null +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -0,0 +1,114 @@ +import React, { FC, useEffect, useState } from "react"; +import Button from "@mui/material/Button"; +import ApiSingleton from "../../api/ApiSingleton"; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import {LtiLaunchData} from "@/api"; + +export interface LtiItemDto { + title: string; + text?: string; + ltiLaunchData: LtiLaunchData; + scoreMaximum: number; +} + +interface LtiImportButtonProps { + courseId: number; + toolName: string; + onImport: (items: LtiItemDto[]) => void; +} + +export const LtiImportButton: FC = ({ courseId, toolName, onImport }) => { + const submitLtiForm = (formData: any) => { + const windowName = "lti_tab_" + new Date().getTime(); + window.open('about:blank', windowName); + + const form = document.createElement("form"); + form.method = formData.method; + form.action = formData.actionUrl; + form.target = windowName; + + if (formData.fields) { + Object.entries(formData.fields).forEach(([key, value]) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = key; + input.value = String(value); + form.appendChild(input); + }); + } + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + }; + + const handleStartLti = async () => { + try { + const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( + undefined, + String(courseId), toolName, + undefined, + undefined, + true + ); + let dto = response; + if (response && typeof (response as any).json === 'function') { + dto = await (response as any).json(); + } + + submitLtiForm(dto); + } catch (e) { + console.error(e); + } + }; + + useEffect(() => { + const handleLtiMessage = (event: MessageEvent) => { + if (event.data && event.data.type === 'LTI_DEEP_LINK_SUCCESS') { + const payload = event.data.payload; + + const rawItems = Array.isArray(payload) ? payload : [payload]; + + const items: LtiItemDto[] = rawItems.map((item: any) => { + let parsedItem = item; + if (typeof item === 'string') { + try { + parsedItem = JSON.parse(item); + } catch (e) { + console.error("Ошибка парсинга JSON от LTI:", item); + return null; + } + } + + const mappedItem: LtiItemDto = { + title: parsedItem.title || "Задача из внешнего инструмента", + text: parsedItem.text || "", + ltiLaunchData: { + ltiLaunchUrl: parsedItem.url, + customParams: parsedItem.custom ? JSON.stringify(parsedItem.custom) : undefined + }, + + scoreMaximum: parsedItem.lineItem?.scoreMaximum || 10 + }; + + return mappedItem; + }).filter((item): item is LtiItemDto => item !== null); + + if (items.length > 0) { + onImport(items); + } + } + }; + window.addEventListener("message", handleLtiMessage); + return () => window.removeEventListener("message", handleLtiMessage); + }, [onImport]); + + return ( + + ); +}; \ No newline at end of file diff --git a/hwproj.front/vite.config.ts b/hwproj.front/vite.config.ts index a5740710f..c178b2cc0 100644 --- a/hwproj.front/vite.config.ts +++ b/hwproj.front/vite.config.ts @@ -32,15 +32,10 @@ export default defineConfig({ host: '0.0.0.0', port: 3000, allowedHosts: ["hwproj.ru"], - hmr: { - host: 'localhost', - port: 3000, - protocol: 'wss' - }, open: true }, build: { outDir: "dist", //emptyOutDir: true } -}) \ No newline at end of file +})