diff --git a/.gitignore b/.gitignore index 7ed326a27..dae2af57f 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,4 @@ StyleCop.Cache swagger-codegen hwproj.front/static_dist/ hwproj.front/dist/ +.DS_Store diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index 7ad6bf87a..2ec8caf2e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -309,7 +309,8 @@ private async Task ToCourseViewModel(CourseDTO course) NewStudents = newStudents.ToArray(), Homeworks = course.Homeworks, 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 1ab2fe869..66ebce612 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -139,6 +139,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..94bc3d075 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..848be6640 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs @@ -0,0 +1,98 @@ +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 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 + ) : 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.Subject; + + 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 = unverifiedToken.Issuer, + + 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 accessToken = tokenService.GenerateAccessTokenForLti(tool.ClientId, scope); + + return Ok(new + { + access_token = accessToken, + token_type = "Bearer", + expires_in = 3600, + scope + }); + } +} \ No newline at end of file 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..ab6e61295 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Services; +using HwProj.CoursesService.Client; +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/json", "application/vnd.ims.lis.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 tool = toolService.GetByClientId(toolClientId); + if (tool == null) + { + return BadRequest("Tool not found."); + } + + var course = await coursesServiceClient.GetCourseByTaskForLti(taskId, score.UserId); + if (course == null) + { + return BadRequest("The task does not belong to any course."); + } + + if (course.LtiToolName != tool.Name) + { + return BadRequest("This tool does not apply to this course."); + } + + if (score.ScoreGiven < 0 || score.ScoreGiven > score.ScoreMaximum) + { + return BadRequest("ScoreGiven must be between 0 and ScoreMaximum."); + } + + try + { + await this.SetTaskGrade(taskId, score); + return Ok(new { message = "Score updated successfully" }); + } + catch (KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (Exception) + { + return StatusCode(500, "Internal Server Error"); + } + } + + private async Task SetTaskGrade(long taskId, Score score) + { + var postSolutionModel = new PostSolutionModel + { + StudentId = score.UserId, + LecturerComment = score.Comment, + Rating = (int)Math.Round(score.ScoreGiven) + }; + + + var solutionId = await solutionsClient.PostSolutionForLti(taskId, postSolutionModel); + + var rate = new RateSolutionModel + { + Rating = (int)Math.Round(score.ScoreGiven), + LecturerComment = score.Comment + }; + + await solutionsClient.RateSolutionForLti(solutionId, rate); + } +} \ No newline at end of file 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..08123878a --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +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.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, + 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"); + } + + if (payload?.ToolName == null || payload.CourseId == null) + { + 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 course = await coursesServiceClient.GetCourseByIdForLti(long.Parse(payload.CourseId)); + 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"); + } + + string idToken; + switch (payload.Type) + { + case "DeepLinking": + idToken = tokenService.CreateDeepLinkingToken( + clientId: clientId, + courseId: payload.CourseId, + targetLinkUri: redirectUri, + userId: payload.UserId, + nonce: nonce + ); + break; + case "ResourceLink": + idToken = tokenService.CreateResourceLinkToken( + clientId: clientId, + courseId: payload.CourseId, + targetLinkUri: redirectUri, + 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.GetCourseByIdForLti(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."); + } + + 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; } + } +} \ No newline at end of file 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..42e1ede95 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Text.Json; +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 +{ + [HttpPost("deepLinkReturn")] + [AllowAnonymous] + public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollection form) + { + if (!form.ContainsKey("JWT")) + { + return BadRequest("Missing JWT parameter"); + } + + string tokenString = form["JWT"]!; + var handler = new JwtSecurityTokenHandler(); + + if (!handler.CanReadToken(tokenString)) + { + return BadRequest("Invalid JWT structure"); + } + + var unverifiedToken = handler.ReadJwtToken(tokenString); + var clientId = unverifiedToken.Subject; + + var tool = toolService.GetByClientId(clientId); + if (tool == null) + { + return Unauthorized($"Unknown tool clientId: {clientId}"); + } + + var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); + + try + { + handler.ValidateToken(tokenString, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = unverifiedToken.Issuer, + + ValidateAudience = true, + ValidAudience = ltiPlatformOptions.Value.Issuer, + + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + + ValidateIssuerSigningKey = true, + IssuerSigningKeys = signingKeys + }, out var validatedToken); + } + 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 resultList = new List(); + + if (unverifiedToken.Payload.TryGetValue(itemsClaimName, out var itemsObject)) + { + var jsonString = itemsObject.ToString(); + if (!string.IsNullOrEmpty(jsonString)) + { + using var doc = JsonDocument.Parse(jsonString); + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + foreach (var rawItem in doc.RootElement.EnumerateArray()) + { + resultList.Add(rawItem.Clone().ToString()); + } + } + } + } + + if (resultList.Count == 0) + { + return Content("", "text/html"); + } + + var responsePayloadJson = JsonSerializer.Serialize(resultList); + + // language=html + var htmlResponse = $@" + + + Processing LTI Return... + +

Задача выбрана. Возвращаемся в HwProj...

+ + + "; + + return Content(htmlResponse, "text/html"); + } +} \ No newline at end of file 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..4aca2acd1 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -0,0 +1,352 @@ +#if DEBUG +using System; +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 const string ToolIss = "Local Mock Tool"; + private const string ToolNameId = "mock-tool-client-id"; + + 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); + var 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) + { + var callbackUrl = $"{iss}/api/lti/authorize?" + + $"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=xyz&nonce={Guid.NewGuid()}"; + + return Redirect(callbackUrl); + } + + [HttpPost("callback")] + public async Task Callback([FromForm] string id_token) + { + 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); + + try { + handler.ValidateToken(id_token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = ToolNameId, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = platformKeySet.Keys + }, out _); + } catch (Exception ex) { + return Unauthorized($"HwProj signature validation error: {ex.Message}"); + } + + var messageType = unverifiedToken.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/message_type")?.Value; + + return messageType switch + { + "LtiDeepLinkingRequest" => RenderDeepLinkingSelectionUI(unverifiedToken), + "LtiResourceLinkRequest" => HandleResourceLink(unverifiedToken), + _ => 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, + ["url"] = $"http://localhost:5000/mock/task/{t.Id}", + + ["lineItem"] = new Dictionary + { + ["scoreMaximum"] = t.Score, + ["label"] = t.Title + }, + + ["custom"] = new Dictionary + { + { "internal_task_id", t.Id } + } + + }).ToList(); + + var payload = new JwtPayload + { + { "iss", ToolIss }, + { "sub", 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, ToolIss), + new(JwtRegisteredClaimNames.Sub, 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 \ No newline at end of file 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..28815f192 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs @@ -0,0 +1,9 @@ +namespace HwProj.APIGateway.API.Lti.DTOs; + +public record LtiToolDto( + string Name, + 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..08f9b329b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs @@ -0,0 +1,19 @@ +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.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..1de7f1e0b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -0,0 +1,27 @@ +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 scope); +} \ No newline at end of file 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..050b0ad39 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs @@ -0,0 +1,63 @@ +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 +{ + + 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); + + if (response.Headers.CacheControl?.NoCache == true || + response.Headers.CacheControl?.NoStore == true || + response.Headers.CacheControl?.Private == true) + { + return keySet.Keys; + } + + const int ageByDefault = 24; + var cacheDuration = TimeSpan.FromHours(ageByDefault); + + if (response.Headers.CacheControl?.MaxAge.HasValue == true) + { + cacheDuration = response.Headers.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..e02222cbc --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -0,0 +1,170 @@ +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 scope) + { + var now = DateTime.UtcNow; + + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, clientId), + + 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); + } +} \ No newline at end of file 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/appsettings.json b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json index bb18739ae..c2777316b 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json +++ b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=AuthServiceDB;Trusted_Connection=True;TrustServerCertificate=true;", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=AuthServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=AuthServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { diff --git a/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs b/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs index 32bc4db97..be9dd0c40 100644 --- a/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs +++ b/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs @@ -8,7 +8,8 @@ public static class ConnectionString { public static string GetConnectionString(IConfiguration configuration) { - var option = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + var option = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "DefaultConnectionForLinux" : "DefaultConnectionForWindows"; return configuration.GetConnectionString(option) ?? ""; diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs index 18e2b51b6..7bffc7e7b 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 AccountDataDto[] Mentors { get; set; } public AccountDataDto[] AcceptedStudents { get; set; } diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs index f2cc40854..ec998dfd0 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs @@ -38,6 +38,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 +73,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.Common/HwProj.Utils/Configuration/ConnectionString.cs b/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs index 2c0ea9135..3e130809a 100644 --- a/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs +++ b/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs @@ -8,7 +8,8 @@ public static class ConnectionString { public static string GetConnectionString(IConfiguration configuration) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { return configuration.GetConnectionString("DefaultConnectionForLinux"); } 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.ContentService/HwProj.ContentService.API/appsettings.json b/HwProj.ContentService/HwProj.ContentService.API/appsettings.json index de0b9bc31..25f5a1090 100644 --- a/HwProj.ContentService/HwProj.ContentService.API/appsettings.json +++ b/HwProj.ContentService/HwProj.ContentService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=ContentServiceDB;Trusted_Connection=True;TrustServerCertificate=true;", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=ContentServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=ContentServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index 022aad42c..fbd288cc2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -58,11 +58,13 @@ public async Task GetView(long courseId) [HttpGet("{courseId}")] public async Task Get(long courseId) { - var userId = Request.GetUserIdFromHeader(); - var course = await _coursesService.GetAsync(courseId, userId); - if (course == null) return NotFound(); + return await GetInternal(courseId); + } - return Ok(course); + [HttpGet("getForLti/{courseId}")] + public async Task GetForLti(long courseId) + { + return await GetInternal(courseId); } [HttpGet("getForMentor/{courseId}/{mentorId}")] @@ -89,11 +91,13 @@ public async Task GetAllData(long courseId) [HttpGet("getByTask/{taskId}")] public async Task GetByTask(long taskId) { - var userId = Request.GetUserIdFromHeader(); - var course = await _coursesService.GetByTaskAsync(taskId, userId); - if (course == null) return NotFound(); + return await GetByTaskInternal(taskId); + } - return Ok(course); + [HttpGet("getByTaskForLti/{taskId}/{userId}")] + public async Task GetByTaskForLti(long taskId, string userId) + { + return await GetByTaskInternal(taskId, userId, true); } [HttpPost("create")] @@ -122,7 +126,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(); @@ -257,5 +262,24 @@ public async Task GetMentorsToAssignedStudents(long courseId) var mentorsToAssignedStudents = await _courseFilterService.GetAssignedStudentsIds(courseId, mentorIds); return Ok(mentorsToAssignedStudents); } + + private async Task GetInternal(long courseId) + { + var userId = Request.GetUserIdFromHeader(); + var course = await _coursesService.GetAsync(courseId, userId); + if (course == null) return NotFound(); + + return Ok(course); + } + + private async Task GetByTaskInternal( + long taskId, string? userId = null, bool isLtiRequest = false) + { + userId ??= Request.GetUserIdFromHeader(); + var course = await _coursesService.GetByTaskAsync(taskId, userId!); + if (course == null) return NotFound(); + + return Ok(course); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs index dbbdb1ba6..a2d6e20dc 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs @@ -13,10 +13,12 @@ namespace HwProj.CoursesService.API.Controllers public class HomeworksController : Controller { private readonly IHomeworksService _homeworksService; + private readonly ITasksService _tasksService; - public HomeworksController(IHomeworksService homeworksService) + public HomeworksController(IHomeworksService homeworksService, ITasksService tasksService) { _homeworksService = homeworksService; + _tasksService = tasksService; } [HttpPost("{courseId}/add")] @@ -28,15 +30,22 @@ public async Task AddHomework(long courseId, if (validationResult.Any()) return BadRequest(validationResult); var newHomework = await _homeworksService.AddHomeworkAsync(courseId, homeworkViewModel); - return Ok(newHomework.ToHomeworkViewModel()); + var responseViewModel = newHomework.ToHomeworkViewModel(); + + await FillLtiLaunchDataForTasks(responseViewModel); + + return Ok(responseViewModel); } [HttpGet("get/{homeworkId}")] public async Task GetHomework(long homeworkId) { var homeworkFromDb = await _homeworksService.GetHomeworkAsync(homeworkId); - var homework = homeworkFromDb.ToHomeworkViewModel(); - return homework; + var homeworkViewModel = homeworkFromDb.ToHomeworkViewModel(); + + await FillLtiLaunchDataForTasks(homeworkViewModel); + + return homeworkViewModel; } [HttpGet("getForEditing/{homeworkId}")] @@ -44,8 +53,11 @@ public async Task GetHomework(long homeworkId) public async Task GetForEditingHomework(long homeworkId) { var homeworkFromDb = await _homeworksService.GetForEditingHomeworkAsync(homeworkId); - var homework = homeworkFromDb.ToHomeworkViewModel(); - return homework; + var homeworkViewModel = homeworkFromDb.ToHomeworkViewModel(); + + await FillLtiLaunchDataForTasks(homeworkViewModel); + + return homeworkViewModel; } [HttpDelete("delete/{homeworkId}")] @@ -65,7 +77,28 @@ public async Task UpdateHomework(long homeworkId, if (validationResult.Any()) return BadRequest(validationResult); var updatedHomework = await _homeworksService.UpdateHomeworkAsync(homeworkId, homeworkViewModel); - return Ok(updatedHomework.ToHomeworkViewModel()); + var responseViewModel = updatedHomework.ToHomeworkViewModel(); + + await FillLtiLaunchDataForTasks(responseViewModel); + + return Ok(responseViewModel); + } + + private async Task FillLtiLaunchDataForTasks(HomeworkViewModel viewModel) + { + if (viewModel.Tasks != null && viewModel.Tasks.Any()) + { + var taskIds = viewModel.Tasks.Select(t => t.Id).ToArray(); + var ltilaunchMultipleData = await _tasksService.GetLtiDataForTasksAsync(taskIds); + + foreach (var task in viewModel.Tasks) + { + if (ltilaunchMultipleData.TryGetValue(task.Id, out var ltiLaunchData)) + { + task.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + } + } + } } } -} +} \ 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 e8b8ec613..607c58816 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs @@ -44,7 +44,11 @@ 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()); + + var taskViewModel = task.ToHomeworkTaskViewModel(); + await _tasksService.FillTaskViewModelWithLtiLaunchDataAsync(taskViewModel, taskId); + + return Ok(taskViewModel); } [HttpGet("getForEditing/{taskId}")] @@ -69,12 +73,14 @@ public async Task AddTask(long homeworkId, [FromBody] PostTaskVie var validationResult = Validator.ValidateTask(taskViewModel, homework); if (validationResult.Any()) return BadRequest(validationResult); - var task = await _tasksService.AddTaskAsync(homeworkId, taskViewModel); + var task = await _tasksService.AddTaskAsync( + homeworkId, + taskViewModel); return Ok(task); } - [HttpDelete("delete/{taskId}")] //bug with rights + [HttpDelete("delete/{taskId}")] [ServiceFilter(typeof(CourseMentorOnlyAttribute))] public async Task DeleteTask(long taskId) { @@ -92,8 +98,10 @@ public async Task UpdateTask(long taskId, [FromBody] PostTaskView if (validationResult.Any()) return BadRequest(validationResult); var updatedTask = - await _tasksService.UpdateTaskAsync(taskId, taskViewModel, + await _tasksService.UpdateTaskAsync(taskId, + taskViewModel, taskViewModel.ActionOptions ?? ActionOptions.Default); + return Ok(updatedTask.ToHomeworkTaskViewModel()); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index 1ff471f2f..7cf2de604 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -103,6 +103,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) @@ -156,6 +157,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) @@ -165,6 +167,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) @@ -188,7 +191,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) @@ -197,6 +200,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) @@ -224,5 +228,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/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 f6c691ff2..fab15ce1a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -52,6 +52,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsOpen"); + b.Property("LtiToolName"); + b.Property("MentorIds"); b.Property("Name"); @@ -207,6 +209,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("TaskLtiData"); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.Property("CourseMateId"); @@ -327,6 +343,14 @@ protected override void BuildModel(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/Models/Course.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs index e5f6af8a3..1ea32aa29 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 42587af31..53cd7f80a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -16,6 +16,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..4229b3fe8 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs @@ -15,7 +15,8 @@ public class HomeworkTaskTemplate public bool HasSpecialPublicationDate { get; set; } 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 5d43ba77c..de41befe9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System; using System.Linq.Expressions; using System.Threading.Tasks; using HwProj.CoursesService.API.Models; @@ -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 f998be609..bbe30d149 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -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 69ef5e6c8..f3ba087b1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -77,6 +77,9 @@ public async Task GetAllAsync() var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); 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; } @@ -108,9 +111,30 @@ public async Task AddAsync(CreateCourseViewModel courseViewModel, string m $"Пользователь с id{mentorId} не является ментором указанного базового курса"); } - courseTemplate.Homeworks = - baseCourse?.Homeworks.Select(h => h.ToHomeworkTemplate()).ToList() ?? - new List(); + courseTemplate.Homeworks ??= new List(); + if (baseCourse?.LtiToolName != null) + { + var allTaskIds = baseCourse.Homeworks + .SelectMany(h => h.Tasks.Select(t => t.Id)); + + var ltiDataDict = await _tasksRepository.GetLtiDataForTasksAsync(allTaskIds); + + foreach (var homework in baseCourse.Homeworks) + { + var homeworkTemplate = homework.ToHomeworkTemplate(); + + // Здесь опираемся на порядок + for (var i = 0; i < homeworkTemplate.Tasks.Count; i++) + { + if (ltiDataDict.TryGetValue(homework.Tasks[i].Id, out var ltiData)) + { + homeworkTemplate.Tasks[i].LtiLaunchData = ltiData; + } + } + + courseTemplate.Homeworks.Add(homeworkTemplate); + } + } using var transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); @@ -154,13 +178,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 +256,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 +348,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) @@ -373,5 +429,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 76844defc..afda302aa 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -16,13 +16,15 @@ public class HomeworksService : IHomeworksService private readonly IHomeworksRepository _homeworksRepository; private readonly IEventBus _eventBus; private readonly ICoursesRepository _coursesRepository; + private readonly ITasksRepository _tasksRepository; public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, - ICoursesRepository coursesRepository) + ICoursesRepository coursesRepository, ITasksRepository tasksRepository) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; + _tasksRepository = tasksRepository; } public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) @@ -39,7 +41,25 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.DeadlineDate)); } - await _homeworksRepository.AddAsync(homework); + await _homeworksRepository.AddAsync(homework); + + if (homeworkViewModel.Tasks != null && homework.Tasks != null) + { + var createdTasks = homework.Tasks.ToList(); + var taskModels = homeworkViewModel.Tasks; + + for (var i = 0; i < createdTasks.Count && i < taskModels.Count; i++) + { + var ltiLaunchData = taskModels[i].LtiLaunchData.ToLtiLaunchData(); + if (ltiLaunchData == null) + { + continue; + } + + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(createdTasks[i].Id, ltiLaunchData); + } + } + return await GetHomeworkAsync(homework.Id, withCriteria: true); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index 0f6301fa5..f41119934 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -1,7 +1,8 @@ -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 { @@ -9,8 +10,11 @@ public interface ITasksService { Task GetTaskAsync(long taskId, bool withCriteria = false); Task GetForEditingTaskAsync(long taskId); + Task GetTaskLtiDataAsync(long taskId); + Task> GetLtiDataForTasksAsync(long[] taskIds); Task AddTaskAsync(long homeworkId, PostTaskViewModel taskViewModel); Task DeleteTaskAsync(long taskId); Task UpdateTaskAsync(long taskId, PostTaskViewModel taskViewModel, ActionOptions options); + Task FillTaskViewModelWithLtiLaunchDataAsync(HomeworkTaskViewModel taskViewModel, long taskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index 2d4d7cb6e..3694aa0ba 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using HwProj.CoursesService.API.Domains; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories; @@ -5,9 +8,7 @@ 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 { @@ -43,7 +44,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,8 +55,14 @@ 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(); + var studentIds = course!.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); if (task.PublicationDate <= DateTime.UtcNow) _eventBus.Publish(new NewHomeworkTaskEvent(task.Title, taskId, deadlineDate, course.Name, course.Id, @@ -67,7 +76,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 +105,28 @@ 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 GetTaskLtiDataAsync(long taskId) + { + return await _tasksRepository.GetLtiDataAsync(taskId); + } + + public async Task> GetLtiDataForTasksAsync(long[] taskIds) + { + return await _tasksRepository.GetLtiDataForTasksAsync(taskIds); + } } } 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 2cbd55fd2..14108c1af 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -52,24 +52,22 @@ public async Task GetAllCourses() public async Task GetCourseByTask(long taskId) { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Get, - _coursesServiceUri + $"api/Courses/getByTask/{taskId}"); + return await GetCourseByTaskInternal(taskId); + } - httpRequest.TryAddUserId(_httpContextAccessor); - var response = await _httpClient.SendAsync(httpRequest); - return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + public async Task GetCourseByTaskForLti(long taskId, string userId) + { + return await GetCourseByTaskInternal(taskId, userId, true); } public async Task GetCourseById(long courseId) { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Get, - _coursesServiceUri + $"api/Courses/{courseId}"); + return await GetCourseByIdInternal(courseId); + } - httpRequest.TryAddUserId(_httpContextAccessor); - var response = await _httpClient.SendAsync(httpRequest); - return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + public async Task GetCourseByIdForLti(long courseId) + { + return await GetCourseByIdInternal(courseId, true); } public async Task> GetCourseByIdForMentor(long courseId, string mentorId) @@ -652,5 +650,32 @@ public async Task Ping() return false; } } + + private async Task GetCourseByIdInternal( + long courseId, bool isLtiRequest = false) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Get, + _coursesServiceUri + $"api/Courses/" + + $"{(isLtiRequest ? "getForLti/" : "")}{courseId}"); + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + } + + private async Task GetCourseByTaskInternal( + long taskId, string? userId = null, bool isLtiRequest = false) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Get, + _coursesServiceUri + $"api/Courses/" + + $"{(isLtiRequest ? $"getByTaskForLti/{taskId}/{userId}" : $"getByTask/{taskId}")}"); + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + var result =response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + return result; + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index da84eb73b..80f345d36 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -10,10 +10,12 @@ public interface ICoursesServiceClient Task GetAllCourses(); Task GetCourseView(long courseId); Task GetCourseById(long courseId); + Task GetCourseByIdForLti(long courseId); Task> GetCourseByIdForMentor(long courseId, string mentorId); /// Получить полную информацию о курсе без учетов фильтров для преподавателей Task> GetCourseDataRaw(long courseId); Task GetCourseByTask(long taskId); + Task GetCourseByTaskForLti(long taskId, string userId); Task DeleteCourse(long courseId); Task> CreateCourse(CreateCourseViewModel model); Task UpdateCourse(UpdateCourseViewModel model, long courseId); diff --git a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json index f6fe217f3..f196fffa6 100644 --- a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json +++ b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=NotificationsServiceDB;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=True", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=NotificationsServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=NotificationsServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index 0cd2215a7..5b1437fc0 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using AutoMapper; using HwProj.CoursesService.Client; using HwProj.Models.CoursesService; -using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.SolutionsService; using HwProj.Models.StatisticsService; using HwProj.SolutionsService.API.Domains; @@ -74,33 +72,28 @@ public async Task GetTaskSolutionsFromStudent(long taskId, string st [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] public async Task PostSolution(long taskId, [FromBody] PostSolutionModel solutionModel) { - var task = await _coursesClient.GetTask(taskId); - if (!task.CanSendSolution) - return BadRequest(); - - var solution = _mapper.Map(solutionModel); - var solutionId = await _solutionsService.PostOrUpdateAsync(taskId, solution); + return await PostSolutionInternal(taskId, solutionModel); + } - return Ok(solutionId); + [HttpPost("postSolutionForLti/{taskId}")] + [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] + public async Task PostSolutionForLti(long taskId, [FromBody] PostSolutionModel solutionModel) + { + return await PostSolutionInternal(taskId, solutionModel, true); } [HttpPost("rateSolution/{solutionId}")] public async Task RateSolution(long solutionId, [FromBody] RateSolutionModel rateSolutionModel) { - 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); - - var lecturerId = Request.GetUserIdFromHeader(); - if (course != null && lecturerId != null && course.MentorIds.Contains(lecturerId)) - { - await _solutionsService.RateSolutionAsync(solutionId, lecturerId, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); - return Ok(); - } + return await this.RateSolutionInternal(solutionId, rateSolutionModel); + } - return Forbid(); + [HttpPost("rateSolutionForLti/{solutionId}")] + public async Task RateSolutionForLti(long solutionId, + [FromBody] RateSolutionModel rateSolutionModel) + { + return await this.RateSolutionInternal(solutionId, rateSolutionModel, true); } [HttpPost("rateEmptySolution/{taskId}")] @@ -309,5 +302,46 @@ public async Task GetSolutionActuality(long solutionId) { return await _solutionsService.GetSolutionActuality(solutionId); } + + private async Task PostSolutionInternal( + long taskId, PostSolutionModel solutionModel, bool isLtiRequest = false) + { + var task = await _coursesClient.GetTask(taskId); + if (!task.CanSendSolution) + return BadRequest(); + + var solution = _mapper.Map(solutionModel); + var solutionId = isLtiRequest ? + await _solutionsService.PostOrUpdateAsyncForLti(taskId, solution) : + await _solutionsService.PostOrUpdateAsync(taskId, solution); + + return Ok(solutionId); + } + + private async Task RateSolutionInternal(long solutionId, + RateSolutionModel rateSolutionModel, bool isLtiRequest = false) + { + var solution = await _solutionsService.GetSolutionAsync(solutionId); + var task = await _coursesClient.GetTask(solution.TaskId); + var homework = await _coursesClient.GetHomework(task.HomeworkId); + var course = isLtiRequest + ? await _coursesClient.GetCourseByTaskForLti(homework.CourseId, solution.StudentId) + : await _coursesClient.GetCourseByTask(homework.CourseId); + + var lecturerId = Request.GetUserIdFromHeader(); + if (isLtiRequest && course != null) + { + await _solutionsService.RateSolutionAsync(solutionId, lecturerId!, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); + return Ok(); + } + + if (course != null && lecturerId != null && course.MentorIds.Contains(lecturerId)) + { + await _solutionsService.RateSolutionAsync(solutionId, lecturerId, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); + return Ok(); + } + + return Forbid(); + } } } \ 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 441a6433c..146f1b7e8 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs @@ -16,6 +16,7 @@ public interface ISolutionsService Task GetTaskSolutionsFromGroupAsync(long taskId, long groupId); Task PostOrUpdateAsync(long taskId, Solution solution); + Task PostOrUpdateAsyncForLti(long taskId, Solution solution); 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 d10941c24..674afea75 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs @@ -61,6 +61,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 @@ -105,46 +106,12 @@ public async Task GetTaskSolutionsFromStudentAsync(long taskId, stri 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)); - } + return await PostOrUpdateInternalAsync(taskId, solution); + } - if (task.Tags.Contains(HomeworkTags.Test)) - await TrySaveSolutionCommitsInfo(solutionId.Value, solution.GithubUrl); - return solutionId.Value; + public async Task PostOrUpdateAsyncForLti(long taskId, Solution solution) + { + return await PostOrUpdateInternalAsync(taskId, solution, true); } public async Task PostEmptySolutionWithRateAsync(long taskId, Solution solution) @@ -352,6 +319,53 @@ await client.PullRequest.Commits(pullRequest.Owner, pullRequest.RepoName, pullRe return solutionsActuality; } + private async Task PostOrUpdateInternalAsync( + long taskId, Solution solution, bool isLtiRequest = false) + { + 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 = isLtiRequest ? + await _coursesServiceClient.GetCourseByTaskForLti(solution.TaskId, solution.StudentId) : + 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; + } + 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 6a73f271a..05b846c17 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs @@ -10,8 +10,10 @@ public interface ISolutionsServiceClient Task GetSolutionById(long solutionId); Task GetUserSolutions(long taskId, string studentId); Task PostSolution(long taskId, PostSolutionModel model); + Task PostSolutionForLti(long taskId, PostSolutionModel model); Task PostEmptySolutionWithRate(long taskId, SolutionViewModel solution); Task RateSolution(long solutionId, RateSolutionModel rateSolutionModel); + Task RateSolutionForLti(long solutionId, RateSolutionModel rateSolutionModel); Task MarkSolution(long solutionId); Task DeleteSolution(long solutionId); Task PostGroupSolution(SolutionViewModel model, long taskId, long groupId); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs index da26f52a9..c1d97476d 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs @@ -72,24 +72,12 @@ public async Task GetUserSolutions(long taskId, string studentId) public async Task PostSolution(long taskId, PostSolutionModel model) { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Post, - _solutionServiceUri + $"api/Solutions/{taskId}") - { - Content = new StringContent( - JsonConvert.SerializeObject(model), - Encoding.UTF8, - "application/json") - }; - - httpRequest.TryAddUserId(_httpContextAccessor); - var response = await _httpClient.SendAsync(httpRequest); - if (response.IsSuccessStatusCode) - { - return await response.DeserializeAsync(); - } + return await PostSolutionInternal(taskId, model); + } - throw new ForbiddenException(); + public async Task PostSolutionForLti(long taskId, PostSolutionModel model) + { + return await PostSolutionInternal(taskId, model, true); } public async Task PostEmptySolutionWithRate(long taskId, SolutionViewModel model) @@ -111,11 +99,23 @@ public async Task PostEmptySolutionWithRate(long taskId, SolutionViewModel model } public async Task RateSolution(long solutionId, RateSolutionModel rateSolutionModel) + { + await RateSolutionInternal(solutionId, rateSolutionModel); + } + + public async Task RateSolutionForLti(long solutionId, RateSolutionModel rateSolutionModel) + { + await RateSolutionInternal(solutionId, rateSolutionModel, true); + } + + private async Task RateSolutionInternal( + long solutionId, RateSolutionModel rateSolutionModel, bool isLtiRequest = false) { using var httpRequest = new HttpRequestMessage( HttpMethod.Post, _solutionServiceUri + - $"api/Solutions/rateSolution/{solutionId}") + $"api/Solutions/" + + $"{(isLtiRequest ? "rateSolutionForLti" : "rateSolution")}/{solutionId}") { Content = new StringContent( JsonConvert.SerializeObject(rateSolutionModel), @@ -297,5 +297,29 @@ public async Task Ping() return false; } } + + private async Task PostSolutionInternal( + long taskId, PostSolutionModel model, bool isLtiRequest = false) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + _solutionServiceUri + $"api/Solutions/" + + $"{(isLtiRequest ? "postSolutionForLti/" : "")}{taskId}") + { + Content = new StringContent( + JsonConvert.SerializeObject(model), + Encoding.UTF8, + "application/json") + }; + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + if (response.IsSuccessStatusCode) + { + return await response.DeserializeAsync(); + } + + throw new ForbiddenException(); + } } } diff --git a/hwproj.front/package-lock.json b/hwproj.front/package-lock.json index a0882868d..0b5a42006 100644 --- a/hwproj.front/package-lock.json +++ b/hwproj.front/package-lock.json @@ -31,7 +31,7 @@ "@uiw/react-markdown-preview": "4.2.2", "@uiw/react-md-editor": "3.25.6", "avatarka": "^1.0.1", - "avatarka-react": "^1.0.1", + "avatarka-react": "^1.1.0", "axios": "^0.30.2", "bootstrap": "^4.3.1", "classnames": "^2.3.1", @@ -4977,6 +4977,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", @@ -5004,12 +5005,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", @@ -5043,6 +5046,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": { @@ -5441,6 +5445,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", @@ -5500,6 +5505,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", @@ -5514,6 +5520,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", @@ -5538,6 +5545,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": { @@ -5704,6 +5712,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" @@ -5804,6 +5813,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" @@ -6276,6 +6286,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", @@ -6297,12 +6308,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", @@ -6319,6 +6332,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", @@ -6332,6 +6346,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" @@ -6344,6 +6359,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" @@ -6359,6 +6375,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" @@ -6442,6 +6459,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", @@ -6462,6 +6480,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": { @@ -7088,6 +7107,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": { @@ -7456,6 +7476,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": { @@ -9618,21 +9639,21 @@ } }, "node_modules/avatarka": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/avatarka/-/avatarka-1.0.1.tgz", - "integrity": "sha512-uFRzGLP+6pALoT3D9ckNcaub7AtL8fUfS3l9jwzIj66PyQ6OqfbP4HrFLlpgDXIJ09Z7q7Xx+ltDqIFHW3MNcw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/avatarka/-/avatarka-1.1.0.tgz", + "integrity": "sha512-9FLaTchGxJBovxJjAlM6AgcURYS/u14sA1bE1FfDpnJoaqH66c2xgv5nDw3/TrEC5mxGwWiaKYUoMnOh2kp3sA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/avatarka-react": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/avatarka-react/-/avatarka-react-1.0.1.tgz", - "integrity": "sha512-tF4P5HJezV2wVk3+D/8EbYG6A+wpdpXQak7JIQynKsFfJu14SRJoWRwHGZ5TOK/fjLjbPYWNrA5PHkGGaWtvVw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/avatarka-react/-/avatarka-react-1.1.0.tgz", + "integrity": "sha512-5L3OssQsvYLS83lnGCqJv48QIvqGTBguY0tN6ABwi2k34GL4i/3aijZaI7ZyrloQxJCthzgBsb3nAki/L5hs+A==", "license": "MIT", "dependencies": { - "avatarka": "^1.0.1" + "avatarka": "^1.1.0" }, "engines": { "node": ">=18" @@ -18027,6 +18048,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": { @@ -18149,6 +18171,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", @@ -18226,6 +18249,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", @@ -18697,13 +18721,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", @@ -19343,6 +19360,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": { @@ -20535,6 +20553,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" @@ -20591,21 +20610,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -22516,6 +22520,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" @@ -22718,6 +22723,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" @@ -22870,18 +22876,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", @@ -27176,6 +27170,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", @@ -28052,6 +28047,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": { @@ -28685,6 +28681,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", @@ -28701,6 +28698,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" @@ -29222,6 +29220,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" @@ -30114,6 +30113,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": { @@ -31969,21 +31969,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/hwproj.front/package.json b/hwproj.front/package.json index 0ddb05582..2878ab776 100644 --- a/hwproj.front/package.json +++ b/hwproj.front/package.json @@ -27,7 +27,7 @@ "@uiw/react-markdown-preview": "4.2.2", "@uiw/react-md-editor": "3.25.6", "avatarka": "^1.0.1", - "avatarka-react": "^1.0.1", + "avatarka-react": "^1.1.0", "axios": "^0.30.2", "bootstrap": "^4.3.1", "classnames": "^2.3.1", diff --git a/hwproj.front/src/api/ApiSingleton.ts b/hwproj.front/src/api/ApiSingleton.ts index df3527ba0..3d2113b13 100644 --- a/hwproj.front/src/api/ApiSingleton.ts +++ b/hwproj.front/src/api/ApiSingleton.ts @@ -8,7 +8,9 @@ import { TasksApi, StatisticsApi, SystemApi, - FilesApi + FilesApi, + LtiToolsApi, + LtiAuthApi, } from "."; import AuthService from "../services/AuthService"; import CustomFilesApi from "./CustomFilesApi"; @@ -27,6 +29,8 @@ class Api { readonly authService: AuthService; readonly customFilesApi: CustomFilesApi; readonly filesApi: FilesApi; + readonly ltiToolsApi: LtiToolsApi; + readonly ltiAuthApi: LtiAuthApi; constructor( accountApi: AccountApi, @@ -40,7 +44,9 @@ class Api { systemApi: SystemApi, authService: AuthService, customFilesApi: CustomFilesApi, - filesApi: FilesApi + filesApi: FilesApi, + ltiToolsApi: LtiToolsApi, + ltiAuthApi: LtiAuthApi ) { this.accountApi = accountApi; this.expertsApi = expertsApi; @@ -54,6 +60,8 @@ class Api { this.authService = authService; this.customFilesApi = customFilesApi; this.filesApi = filesApi; + this.ltiToolsApi = ltiToolsApi; + this.ltiAuthApi = ltiAuthApi; } } @@ -86,6 +94,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 diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 10fb0302d..7860890ae 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 @@ -477,6 +490,12 @@ export interface CourseViewModel { * @memberof CourseViewModel */ isCompleted?: boolean; + /** + * + * @type {string} + * @memberof CourseViewModel + */ + ltiToolName?: string; /** * * @type {Array} @@ -544,6 +563,12 @@ export interface CreateCourseViewModel { * @memberof CreateCourseViewModel */ baseCourseId?: number; + /** + * + * @type {string} + * @memberof CreateCourseViewModel + */ + ltiToolName?: string; } /** * @@ -1020,6 +1045,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 @@ -1205,6 +1243,12 @@ export interface HomeworkTaskViewModel { * @memberof HomeworkTaskViewModel */ criteria?: Array; + /** + * + * @type {LtiLaunchData} + * @memberof HomeworkTaskViewModel + */ + ltiLaunchData?: LtiLaunchData; } /** * @@ -1479,6 +1523,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 @@ -1498,6 +1630,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 @@ -1700,6 +1938,21 @@ export interface PostTaskViewModel { * @memberof PostTaskViewModel */ criteria?: Array; + /** + * + * @type {LtiLaunchData} + * @memberof PostTaskViewModel + */ + ltiLaunchData?: LtiLaunchData; +} +/** + * + * @export + * @interface ProblemDetails + */ +export interface ProblemDetails { + [key: string]: any; + } /** * @@ -1926,6 +2179,55 @@ export interface ScopeDTO { */ courseUnitId?: number; } +/** + * + * @export + * @interface Score + */ +export interface Score { + /** + * + * @type {ActivityProgress} + * @memberof Score + */ + activityProgress?: ActivityProgress; + /** + * + * @type {string} + * @memberof Score + */ + comment?: string; + /** + * + * @type {GradingProgress} + * @memberof Score + */ + gradingProgress?: GradingProgress; + /** + * + * @type {number} + * @memberof Score + */ + scoreGiven?: number; + /** + * + * @type {number} + * @memberof Score + */ + scoreMaximum?: number; + /** + * + * @type {Date} + * @memberof Score + */ + timestamp?: Date; + /** + * + * @type {string} + * @memberof Score + */ + userId?: string; +} /** * * @export @@ -2330,6 +2632,25 @@ export interface StatisticsLecturersModel { */ numberOfCheckedUniqueSolutions?: number; } +/** + * + * @export + * @interface StringStringValuesKeyValuePair + */ +export interface StringStringValuesKeyValuePair { + /** + * + * @type {string} + * @memberof StringStringValuesKeyValuePair + */ + key?: string; + /** + * + * @type {Array} + * @memberof StringStringValuesKeyValuePair + */ + value?: Array; +} /** * * @export @@ -2720,6 +3041,12 @@ export interface UpdateCourseViewModel { * @memberof UpdateCourseViewModel */ isCompleted?: boolean; + /** + * + * @type {string} + * @memberof UpdateCourseViewModel + */ + ltiToolName?: string; } /** * @@ -2877,6 +3204,12 @@ export interface UserTaskSolutionsPageData { * @memberof UserTaskSolutionsPageData */ courseId?: number; + /** + * + * @type {string} + * @memberof UserTaskSolutionsPageData + */ + ltiToolName?: string; /** * * @type {Array} @@ -7381,6 +7714,1433 @@ 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: '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 (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); + } + +} +/** + * LtiDeepLinkingReturnApi - fetch parameter creator + * @export + */ +export const LtiDeepLinkingReturnApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/deepLinkReturn`; + 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, + }; + }, + } +}; + +/** + * LtiDeepLinkingReturnApi - functional programming interface + * @export + */ +export const LtiDeepLinkingReturnApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiDeepLinkingReturnApiFetchParamCreator(configuration).ltiDeepLinkingReturnOnDeepLinkingReturn(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; + } + }); + }; + }, + } +}; + +/** + * LtiDeepLinkingReturnApi - factory interface + * @export + */ +export const LtiDeepLinkingReturnApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options?: any) { + return LtiDeepLinkingReturnApiFp(configuration).ltiDeepLinkingReturnOnDeepLinkingReturn(form, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiDeepLinkingReturnApi - object-oriented interface + * @export + * @class LtiDeepLinkingReturnApi + * @extends {BaseAPI} + */ +export class LtiDeepLinkingReturnApi extends BaseAPI { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiDeepLinkingReturnApi + */ + public ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options?: any) { + return LtiDeepLinkingReturnApiFp(this.configuration).ltiDeepLinkingReturnOnDeepLinkingReturn(form, 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/Common/UserAvatar.tsx b/hwproj.front/src/components/Common/UserAvatar.tsx index 62b7ed129..0b1285765 100644 --- a/hwproj.front/src/components/Common/UserAvatar.tsx +++ b/hwproj.front/src/components/Common/UserAvatar.tsx @@ -2,7 +2,7 @@ import {AccountDataDto} from "@/api"; import {FC} from "react"; import AvatarUtils from "@/components/Utils/AvatarUtils"; import {Avatar} from "@mui/material"; -import {Avatar as Avatarka, generateParams, getThemeNames} from 'avatarka-react'; +import {Avatar as Avatarka, generateParams, getThemeNames} from "avatarka-react"; const themes = getThemeNames().filter(x => x !== "geometric") diff --git a/hwproj.front/src/components/Courses/AddCourseInfo.tsx b/hwproj.front/src/components/Courses/AddCourseInfo.tsx index c0b98a1ae..5dade4ab8 100644 --- a/hwproj.front/src/components/Courses/AddCourseInfo.tsx +++ b/hwproj.front/src/components/Courses/AddCourseInfo.tsx @@ -3,6 +3,7 @@ import { Grid, TextField, Button, Typography, + MenuItem, } from "@material-ui/core"; import {LoadingButton} from "@mui/lab"; import {IStepComponentProps} from "./ICreateCourseState"; @@ -112,6 +113,47 @@ const AddCourseInfo: FC = ({state, setState}) => { /> + + option.name || "Без названия"} + + value={ + state.ltiToolName + ? state.ltiTools?.find(t => t.name === state.ltiToolName) || undefined + : undefined + } + + // Обработчик изменения + onChange={(_, newValue) => { + setState(prev => ({ + ...prev, + // Если выбрали (newValue не null), берем ID. Иначе undefined. + ltiToolName: newValue ? newValue.name : undefined + })); + }} + + // Рендер инпута + renderInput={(params) => ( + + )} + + // Позволяет очистить выбор (крестик) + clearOnEscape + fullWidth + /> + + {state.isGroupFromList && ( { {tabValue === "homeworks" && void - onTaskUpdate: (update: { task: HomeworkTaskViewModel, isDeleted?: boolean }) => void, - processingFiles: { - [homeworkId: number]: { - isLoading: boolean; - }; - }; - onStartProcessing: (homeworkId: number, - courseUnitType: CourseUnitType, - previouslyExistingFilesCount: number, - waitingNewFilesCount: number, - deletingFilesIds: number[]) => void; -} - -interface ICourseExperimentalState { - initialEditMode: boolean, - selectedItem: { - isHomework: boolean, - id: number | undefined, - } -} - -export const CourseExperimental: FC = (props) => { - const [hideDeferred, setHideDeferred] = useState(false) - const [showOnlyGroupedTest, setShowOnlyGroupedTest] = useState(undefined) - const filterAdded = hideDeferred || showOnlyGroupedTest !== undefined - - // Определяем разрешение экрана пользователя - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - // Состояние для кнопки "Наверх" - const [showScrollButton, setShowScrollButton] = useState(false); - - const homeworks = props.homeworks.slice().reverse().filter(x => { - if (hideDeferred) return !x.isDeferred - if (showOnlyGroupedTest !== undefined) return x.tags!.includes(TestTag) && x.tags!.includes(showOnlyGroupedTest) - return true - }) - - const {isMentor, studentSolutions, isStudentAccepted, userId, selectedHomeworkId, courseFilesInfo} = props - - const [state, setState] = useState({ - initialEditMode: false, - selectedItem: {id: undefined, isHomework: true}, - }) - - useEffect(() => { - const defaultHomeworkIndex = Math.max(selectedHomeworkId ? homeworks?.findIndex(x => x.id === selectedHomeworkId) : 0, 0) - const defaultHomework = homeworks?.[defaultHomeworkIndex] - setState((prevState) => ({ - ...prevState, - selectedItem: {isHomework: true, id: defaultHomework?.id}, - })) - }, [hideDeferred]) - - // Обработчик прокрутки страницы - useEffect(() => { - const handleScroll = () => { - // Показывать кнопку при прокрутке ниже 400px - const shouldShow = window.scrollY > 400; - if (shouldShow !== showScrollButton) { - setShowScrollButton(shouldShow); - } - }; - - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, [showScrollButton]); - - // Функция прокрутки вверх - const scrollToTop = () => { - window.scrollTo({ - top: 110, - behavior: 'instant' - }); - }; - - const initialEditMode = state.initialEditMode - const {id, isHomework} = state.selectedItem - - const renderDate = (date: Date) => { - date = new Date(date) - const options: Intl.DateTimeFormatOptions = { - month: 'long', - day: 'numeric' - }; - return date.toLocaleString("ru-RU", options) - } - - const renderTime = (date: Date) => { - date = new Date(date) - const options: Intl.DateTimeFormatOptions = { - hour: "2-digit", - minute: "2-digit" - }; - return date.toLocaleString("ru-RU", options) - } - - const clickedItemStyle = { - backgroundColor: "ghostwhite", - borderRadius: 9, - cursor: "pointer", - border: "1px solid lightgrey" - } - - const hoveredItemStyle = {...clickedItemStyle, border: "1px solid lightgrey"} - - const warningTimelineDotStyle = { - borderWidth: 0, - margin: 0, - padding: "4px 0px", - } - - const getStyle = (itemIsHomework: boolean, itemId: number) => - itemIsHomework === isHomework && itemId === id ? clickedItemStyle : {borderRadius: 9} - - const taskSolutionsMap = new Map() - - if (!isMentor && isStudentAccepted) { - studentSolutions - .filter(t => t.id === userId) - .flatMap(t => t.homeworks!) - .flatMap(t => t.tasks!) - .forEach(x => taskSolutionsMap.set(x.id!, x.solution!)) - } - - const showWarningsForEntity = (entity: HomeworkViewModel | HomeworkTaskViewModel, isHomework: boolean) => { - if (!isMentor) return false - if (entity.publicationDateNotSet || entity.hasDeadline && entity.deadlineDateNotSet) return true - - if (!isHomework) return false - const result = validateTestGrouping(entity) - return result !== true && result.hasErrors - } - - const renderHomeworkStatus = (homework: HomeworkViewModel & { isModified?: boolean, hasErrors?: boolean }) => { - const hasErrors = homework.id! < 0 && (homework.hasErrors || homework.tasks!.some((t: HomeworkTaskViewModel & { - hasErrors?: boolean - }) => t.hasErrors)) - if (hasErrors) - return

- if (homework.isModified) - return

- return showWarningsForEntity(homework, true) &&
⚠️
- } - - const renderTaskStatus = (task: HomeworkTaskViewModel & { isModified?: boolean, hasErrors?: boolean }) => { - if (taskSolutionsMap.has(task.id!)) { - const solutions = taskSolutionsMap.get(task.id!) - const { - lastSolution, - lastRatedSolution, - color, - solutionsDescription - } = StudentStatsUtils.calculateLastRatedSolutionInfo(solutions!, task.maxRating!) - if (lastSolution != null) return ( - {solutionsDescription}}> - - - ) - } - if (task.hasErrors) return - if (task.isModified) return - return showWarningsForEntity(task, false) ? ( - - ⚠️ - - ) : - } - - const onSelectedItemMount = () => - setState((prevState) => ({ - ...prevState, - initialEditMode: false, - })) - - const toEditHomework = (homework: HomeworkViewModel) => - setState({ - initialEditMode: true, - selectedItem: {id: homework.id!, isHomework: true}, - }) - - const validateTestGrouping = (homework: HomeworkViewModel) => { - if (!homework.tags!.includes(TestTag)) return true - - const groupingTag = homework.tags!.find(x => !DefaultTags.includes(x)) - if (groupingTag === undefined) return true - - const groupedHomeworks = homeworks.filter(x => x.tags!.includes(TestTag) && x.tags!.includes(groupingTag)) - if (groupedHomeworks.length === 1) return true - - const keys = new Set(groupedHomeworks.map(h => h.tasks!.map(t => t.maxRating).join(";"))) - return {groupingTag: groupingTag, hasErrors: keys.size !== 1} - } - - const getDatesAlert = (entity: HomeworkViewModel | HomeworkTaskViewModel, isHomework: boolean) => { - if (entity.publicationDateNotSet) { - return ( - - {"Не выставлена дата публикации"} - - ) - } - - if (isMentor && entity.hasDeadline && entity.deadlineDateNotSet) return ( - - {"Не выставлена дата дедлайна"} - - ) - - if (entity.id! < 0) { - if (isHomework) - return Новое задание будет добавлено после нажатия на 'Добавить задание' - if ((entity as HomeworkTaskViewModel)?.homeworkId! < 0) - return setState((prevState) => ({ - ...prevState, - selectedItem: { - isHomework: true, - id: (entity as HomeworkTaskViewModel).homeworkId! - } - }))} - > - Перейти к заданию - }>Часть добавления нового задания - return Новая задача будет добавлена после нажатия на 'Добавить задачу' - } - - if (entity.isDeferred) return ( - setHideDeferred(true)} - > - Скрыть неопубликованное - }> - {isHomework ? "Задание будет опубликовано " : "Задача будет опубликована "} - {renderDate(entity.publicationDate!) + " " + renderTime(entity.publicationDate!)} - - ) - } - const getGroupingAlert = (homework: HomeworkViewModel) => { - const result = validateTestGrouping(homework) - if (result === true) return null - const {hasErrors, groupingTag} = result - if (!hasErrors) return setShowOnlyGroupedTest(groupingTag)} - > - Задания - }> - Работа сгруппирована по ключу '{groupingTag}'. - - - return setShowOnlyGroupedTest(groupingTag)} - > - Задания - }> - Группировка контрольных работ - Создано несколько контрольных работ, сгруппированных по ключу '{groupingTag}', - однако работы отличаются между собой по количеству задач или их максимальным баллам. -
-
- Количество задач должно быть одинаковым, а баллы между соответствующими задачами равными. -
- } - - const selectedItemHomework = isHomework - ? homeworks.find(x => x.id === id)! - : homeworks.find(x => x.tasks!.some(t => t.id === id))! - - const selectedItem = isHomework - ? selectedItemHomework - : selectedItemHomework?.tasks!.find(x => x.id === id) as HomeworkTaskViewModel - - const [newTaskCounter, setNewTaskCounter] = useState(-1) - - const addNewHomework = () => { - props.onHomeworkUpdate({ - homework: { - courseId: props.courseId, - title: "Новое задание", - publicationDateNotSet: false, - publicationDate: undefined, - hasDeadline: false, - id: -1, - isGroupWork: false, - deadlineDateNotSet: false, - deadlineDate: undefined, - isDeadlineStrict: false, - description: "", - tasks: [], - tags: [] - } - }) - setState((prevState) => ({ - ...prevState, - selectedItem: { - isHomework: true, - id: -1 - } - })) - } - - 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]) - .filter(x => { - if (x === undefined) return false - const xIsTest = isTestWork(x) - const xIsBonus = isBonusWork(x) - 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] - - const task = { - homeworkId: homework.id, - maxRating: ratingCandidate || 10, - suggestedMaxRating: ratingCandidate, - title: `Новая задача`, - tags: homework.tags, - isDeferred: homework.isDeferred, - description: "", - id - } - - props.onTaskUpdate({task}) - setState((prevState) => ({ - ...prevState, - selectedItem: { - isHomework: false, - id: id - } - })) - setNewTaskCounter(id - 1) - } - - const renderHomework = (homework: HomeworkViewModel & { isModified?: boolean }) => { - const filesInfo = id ? FileInfoConverter.getCourseUnitFilesInfo(courseFilesInfo, CourseUnitType.Homework, id) : [] - const homeworkEditMode = homework && (homework.id! < 0 || homework.isModified === true) - return homework && - - {isMentor && getGroupingAlert(homework)} - {isMentor && getDatesAlert(homework, true)} - homeworks} - homeworkAndFilesInfo={{homework, filesInfo}} - isMentor={isMentor} - initialEditMode={initialEditMode || homeworkEditMode} - onMount={onSelectedItemMount} - onAddTask={addNewTask} - onUpdate={update => { - props.onHomeworkUpdate(update) - setState((prevState) => ({ - ...prevState, - selectedItem: { - isHomework: true, - id: update.isDeleted ? undefined : update.homework.id! - } - })) - }} - isProcessing={props.processingFiles[homework.id!]?.isLoading || false} - onStartProcessing={props.onStartProcessing} - /> - - - } - - const renderTask = (task: HomeworkTaskViewModel & { isModified?: boolean }, homework: HomeworkViewModel) => { - const taskEditMode = task && (task.id! < 0 || task.isModified === true) - return task && - {isMentor && getDatesAlert(task, false)} - { - props.onTaskUpdate(update) - if (update.isDeleted) - setState((prevState) => ({ - ...prevState, - selectedItem: { - isHomework: true, - id: homework!.id - } - })) - }} - toEditHomework={() => toEditHomework(homework!)} getAllHomeworks={() => homeworks}/> - {!props.isMentor && props.isStudentAccepted && < CardActions> - - - - } - - } - - const renderGif = () => - - - const renderLecturerWelcomeScreen = () => - - - Спасибо за ещё один курс - Самое время добавить новое задание! - - - - return - - - {props.isMentor && filterAdded && - - {hideDeferred - ? "только опубликованные задания" - : showOnlyGroupedTest - ? `контрольные работы '${showOnlyGroupedTest}'` - : ""} - - } - {props.isMentor && !filterAdded && (homeworks[0]?.id || 1) > 0 && } - {isMentor && homeworks.length === 0 && renderLecturerWelcomeScreen()} - - {homeworks.map((x: HomeworkViewModel & { isModified?: boolean, hasErrors?: boolean }) => { - return
- - { - setState(prevState => ({ - ...prevState, - selectedItem: { - data: x, - isHomework: true, - id: x.id, - homeworkFilesInfo: FileInfoConverter.getCourseUnitFilesInfo(courseFilesInfo, CourseUnitType.Homework, x.id!) - } - })) - }}> - - {isMentor && renderHomeworkStatus(x)} - {x.title}{getTip(x)} - - {x.isDeferred && !x.publicationDateNotSet && - - {"🕘 " + renderDate(x.publicationDate!) + " " + renderTime(x.publicationDate!)} - } - {x.tasks?.length === 0 && - - - - - } - - {x.tasks!.map(t => { - setState(prevState => ({ - ...prevState, - selectedItem: { - data: t, - isHomework: false, - id: t.id, - homeworkFilesInfo: [] - } - })) - }} - style={getStyle(false, t.id!)} - sx={{":hover": hoveredItemStyle}}> - {!t.deadlineDateNotSet && - - {t.deadlineDate ? renderDate(t.deadlineDate) : ""} -
- {t.deadlineDate ? renderTime(t.deadlineDate) : ""} -
- } - - {renderTaskStatus(t)} - - - - - {t.title}{getTip(t)} - - -
)} -
- {x.id! < 0 && - } -
; - })} -
-
-
- - {isHomework - ? renderHomework(selectedItem as HomeworkViewModel) - : renderTask(selectedItem as HomeworkTaskViewModel, selectedItemHomework!)} - - {renderGif()} - - - - {renderGif()} - - - {/* Кнопка "Наверх" для мобильных устройств */} - - - - - -
-} +import * as React from "react"; +import { + FileInfoDTO, + HomeworkTaskViewModel, + HomeworkViewModel, Solution, StatisticsCourseMatesModel, +} from "@/api"; +import { + AlertTitle, + Button, + Fab, + Grid, + Typography, + useMediaQuery, + useTheme, + Zoom +} from "@mui/material"; +import {FC, useEffect, useState} from "react"; +import Timeline from '@mui/lab/Timeline'; +import TimelineItem from '@mui/lab/TimelineItem'; +import TimelineSeparator from '@mui/lab/TimelineSeparator'; +import TimelineConnector from '@mui/lab/TimelineConnector'; +import TimelineContent from '@mui/lab/TimelineContent'; +import TimelineDot from '@mui/lab/TimelineDot'; +import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'; +import {Alert, Card, CardActions, Chip, Paper, Stack, Tooltip} from "@mui/material"; +import {Link} from "react-router-dom"; +import StudentStatsUtils from "../../services/StudentStatsUtils"; +import {BonusTag, DefaultTags, getTip, isBonusWork, isTestWork, TestTag} from "../Common/HomeworkTags"; +import FileInfoConverter from "components/Utils/FileInfoConverter"; +import CourseHomeworkExperimental from "components/Homeworks/CourseHomeworkExperimental"; +import CourseTaskExperimental from "../Tasks/CourseTaskExperimental"; +import {DotLottieReact} from "@lottiefiles/dotlottie-react"; +import EditIcon from "@mui/icons-material/Edit"; +import ErrorIcon from '@mui/icons-material/Error'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; +import Lodash from "lodash"; +import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import {LtiImportButton, LtiItemDto} from "@/components/Tasks/LtiImportButton"; + +interface ICourseExperimentalProps { + homeworks: HomeworkViewModel[] + courseFilesInfo: FileInfoDTO[] + studentSolutions: StatisticsCourseMatesModel[] + courseId: number + ltiToolName: string | undefined + isMentor: boolean + isStudentAccepted: boolean + userId: string + selectedHomeworkId: number | undefined + onHomeworkUpdate: (update: { homework: HomeworkViewModel } & { + isDeleted?: boolean + }) => void + onTaskUpdate: (update: { task: HomeworkTaskViewModel, isDeleted?: boolean }) => void, + processingFiles: { + [homeworkId: number]: { + isLoading: boolean; + }; + }; + onStartProcessing: (homeworkId: number, + courseUnitType: CourseUnitType, + previouslyExistingFilesCount: number, + waitingNewFilesCount: number, + deletingFilesIds: number[]) => void; +} + +interface ICourseExperimentalState { + initialEditMode: boolean, + selectedItem: { + isHomework: boolean, + id: number | undefined, + } +} + +export const CourseExperimental: FC = (props) => { + const [hideDeferred, setHideDeferred] = useState(false) + const [showOnlyGroupedTest, setShowOnlyGroupedTest] = useState(undefined) + const filterAdded = hideDeferred || showOnlyGroupedTest !== undefined + + // Определяем разрешение экрана пользователя + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + // Состояние для кнопки "Наверх" + const [showScrollButton, setShowScrollButton] = useState(false); + + const homeworks = props.homeworks.slice().reverse().filter(x => { + if (hideDeferred) return !x.isDeferred + if (showOnlyGroupedTest !== undefined) return x.tags!.includes(TestTag) && x.tags!.includes(showOnlyGroupedTest) + return true + }) + + const {isMentor, studentSolutions, isStudentAccepted, userId, selectedHomeworkId, courseFilesInfo} = props + + const [state, setState] = useState({ + initialEditMode: false, + selectedItem: {id: undefined, isHomework: true}, + }) + + useEffect(() => { + const defaultHomeworkIndex = Math.max(selectedHomeworkId ? homeworks?.findIndex(x => x.id === selectedHomeworkId) : 0, 0) + const defaultHomework = homeworks?.[defaultHomeworkIndex] + setState((prevState) => ({ + ...prevState, + selectedItem: {isHomework: true, id: defaultHomework?.id}, + })) + }, [hideDeferred]) + + // Обработчик прокрутки страницы + useEffect(() => { + const handleScroll = () => { + // Показывать кнопку при прокрутке ниже 400px + const shouldShow = window.scrollY > 400; + if (shouldShow !== showScrollButton) { + setShowScrollButton(shouldShow); + } + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [showScrollButton]); + + // Функция прокрутки вверх + const scrollToTop = () => { + window.scrollTo({ + top: 110, + behavior: 'instant' + }); + }; + + const initialEditMode = state.initialEditMode + const {id, isHomework} = state.selectedItem + + const renderDate = (date: Date) => { + date = new Date(date) + const options: Intl.DateTimeFormatOptions = { + month: 'long', + day: 'numeric' + }; + return date.toLocaleString("ru-RU", options) + } + + const renderTime = (date: Date) => { + date = new Date(date) + const options: Intl.DateTimeFormatOptions = { + hour: "2-digit", + minute: "2-digit" + }; + return date.toLocaleString("ru-RU", options) + } + + const clickedItemStyle = { + backgroundColor: "ghostwhite", + borderRadius: 9, + cursor: "pointer", + border: "1px solid lightgrey" + } + + const hoveredItemStyle = {...clickedItemStyle, border: "1px solid lightgrey"} + + const warningTimelineDotStyle = { + borderWidth: 0, + margin: 0, + padding: "4px 0px", + } + + const getStyle = (itemIsHomework: boolean, itemId: number) => + itemIsHomework === isHomework && itemId === id ? clickedItemStyle : {borderRadius: 9} + + const taskSolutionsMap = new Map() + + if (!isMentor && isStudentAccepted) { + studentSolutions + .filter(t => t.id === userId) + .flatMap(t => t.homeworks!) + .flatMap(t => t.tasks!) + .forEach(x => taskSolutionsMap.set(x.id!, x.solution!)) + } + + const showWarningsForEntity = (entity: HomeworkViewModel | HomeworkTaskViewModel, isHomework: boolean) => { + if (!isMentor) return false + if (entity.publicationDateNotSet || entity.hasDeadline && entity.deadlineDateNotSet) return true + + if (!isHomework) return false + const result = validateTestGrouping(entity) + return result !== true && result.hasErrors + } + + const renderHomeworkStatus = (homework: HomeworkViewModel & { isModified?: boolean, hasErrors?: boolean }) => { + const hasErrors = homework.id! < 0 && (homework.hasErrors || homework.tasks!.some((t: HomeworkTaskViewModel & { + hasErrors?: boolean + }) => t.hasErrors)) + if (hasErrors) + return

+ if (homework.isModified) + return

+ return showWarningsForEntity(homework, true) &&
⚠️
+ } + + const renderTaskStatus = (task: HomeworkTaskViewModel & { isModified?: boolean, hasErrors?: boolean }) => { + if (taskSolutionsMap.has(task.id!)) { + const solutions = taskSolutionsMap.get(task.id!) + const { + lastSolution, + lastRatedSolution, + color, + solutionsDescription + } = StudentStatsUtils.calculateLastRatedSolutionInfo(solutions!, task.maxRating!) + if (lastSolution != null) return ( + {solutionsDescription}}> + + + ) + } + if (task.hasErrors) return + if (task.isModified) return + return showWarningsForEntity(task, false) ? ( + + ⚠️ + + ) : + } + + const onSelectedItemMount = () => + setState((prevState) => ({ + ...prevState, + initialEditMode: false, + })) + + const toEditHomework = (homework: HomeworkViewModel) => + setState({ + initialEditMode: true, + selectedItem: {id: homework.id!, isHomework: true}, + }) + + const validateTestGrouping = (homework: HomeworkViewModel) => { + if (!homework.tags!.includes(TestTag)) return true + + const groupingTag = homework.tags!.find(x => !DefaultTags.includes(x)) + if (groupingTag === undefined) return true + + const groupedHomeworks = homeworks.filter(x => x.tags!.includes(TestTag) && x.tags!.includes(groupingTag)) + if (groupedHomeworks.length === 1) return true + + const keys = new Set(groupedHomeworks.map(h => h.tasks!.map(t => t.maxRating).join(";"))) + return {groupingTag: groupingTag, hasErrors: keys.size !== 1} + } + + const getDatesAlert = (entity: HomeworkViewModel | HomeworkTaskViewModel, isHomework: boolean) => { + if (entity.publicationDateNotSet) { + return ( + + {"Не выставлена дата публикации"} + + ) + } + + if (isMentor && entity.hasDeadline && entity.deadlineDateNotSet) return ( + + {"Не выставлена дата дедлайна"} + + ) + + if (entity.id! < 0) { + if (isHomework) + return Новое задание будет добавлено после нажатия на 'Добавить задание' + if ((entity as HomeworkTaskViewModel)?.homeworkId! < 0) + return setState((prevState) => ({ + ...prevState, + selectedItem: { + isHomework: true, + id: (entity as HomeworkTaskViewModel).homeworkId! + } + }))} + > + Перейти к заданию + }>Часть добавления нового задания + return Новая задача будет добавлена после нажатия на 'Добавить задачу' + } + + if (entity.isDeferred) return ( + setHideDeferred(true)} + > + Скрыть неопубликованное + }> + {isHomework ? "Задание будет опубликовано " : "Задача будет опубликована "} + {renderDate(entity.publicationDate!) + " " + renderTime(entity.publicationDate!)} + + ) + } + const getGroupingAlert = (homework: HomeworkViewModel) => { + const result = validateTestGrouping(homework) + if (result === true) return null + const {hasErrors, groupingTag} = result + if (!hasErrors) return setShowOnlyGroupedTest(groupingTag)} + > + Задания + }> + Работа сгруппирована по ключу '{groupingTag}'. + + + return setShowOnlyGroupedTest(groupingTag)} + > + Задания + }> + Группировка контрольных работ + Создано несколько контрольных работ, сгруппированных по ключу '{groupingTag}', + однако работы отличаются между собой по количеству задач или их максимальным баллам. +
+
+ Количество задач должно быть одинаковым, а баллы между соответствующими задачами равными. +
+ } + + const selectedItemHomework = isHomework + ? homeworks.find(x => x.id === id)! + : homeworks.find(x => x.tasks!.some(t => t.id === id))! + + const selectedItem = isHomework + ? selectedItemHomework + : selectedItemHomework?.tasks!.find(x => x.id === id) as HomeworkTaskViewModel + + const [newTaskCounter, setNewTaskCounter] = useState(-1) + + const addNewHomework = () => { + props.onHomeworkUpdate({ + homework: { + courseId: props.courseId, + title: "Новое задание", + publicationDateNotSet: false, + publicationDate: undefined, + hasDeadline: false, + id: -1, + isGroupWork: false, + deadlineDateNotSet: false, + deadlineDate: undefined, + isDeadlineStrict: false, + description: "", + tasks: [], + tags: [] + } + }) + setState((prevState) => ({ + ...prevState, + selectedItem: { + isHomework: true, + id: -1 + } + })) + } + + const calculateSuggestedRating = (homework: HomeworkViewModel) => { + const tags = homework.tags!; + const isTest = tags.includes(TestTag); + const isBonus = tags.includes(BonusTag); + + const ratingCandidate = Lodash(homeworks + .map(h => h.tasks![0]) + .filter(x => { + if (x === undefined) return false; + const xIsTest = isTestWork(x); + const xIsBonus = isBonusWork(x); + 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]; + + return ratingCandidate || 10; + }; + + const handleLtiImport = (items: LtiItemDto[], homework: HomeworkViewModel) => { + let currentCounter = newTaskCounter; + const suggestedRating = calculateSuggestedRating(homework); + + items.forEach(item => { + if (!item.ltiLaunchData) { + return; + } + + const defaultRating = calculateSuggestedRating(homework); + const taskId = currentCounter; + const description = item.text && item.text.trim().length > 0 + ? item.text + : ""; + const targetRating = (item.scoreMaximum && item.scoreMaximum > 0) + ? item.scoreMaximum + : defaultRating; + + const newTask = { + id: taskId, + homeworkId: homework.id, + title: item.title || "External Task", + description: description, + maxRating: targetRating, + suggestedMaxRating: targetRating, + tags: homework.tags, + isDeferred: homework.isDeferred, + ltiLaunchData: item.ltiLaunchData, + criteria: [] + }; + + props.onTaskUpdate({ task: newTask }); + currentCounter--; + }); + + setNewTaskCounter(currentCounter); + }; + + const addNewTask = (homework: HomeworkViewModel) => { + const id = newTaskCounter + + const ratingCandidate = calculateSuggestedRating(homework); + + const task = { + homeworkId: homework.id, + maxRating: ratingCandidate || 10, + suggestedMaxRating: ratingCandidate, + title: `Новая задача`, + tags: homework.tags, + isDeferred: homework.isDeferred, + description: "", + id + } + + props.onTaskUpdate({task}) + setState((prevState) => ({ + ...prevState, + selectedItem: { + isHomework: false, + id: id + } + })) + setNewTaskCounter(id - 1) + } + + const renderHomework = (homework: HomeworkViewModel & { isModified?: boolean }) => { + const filesInfo = id ? FileInfoConverter.getCourseUnitFilesInfo(courseFilesInfo, CourseUnitType.Homework, id) : [] + const homeworkEditMode = homework && (homework.id! < 0 || homework.isModified === true) + return homework && + + {isMentor && getGroupingAlert(homework)} + {isMentor && getDatesAlert(homework, true)} + homeworks} + homeworkAndFilesInfo={{homework, filesInfo}} + isMentor={isMentor} + initialEditMode={initialEditMode || homeworkEditMode} + onMount={onSelectedItemMount} + onAddTask={addNewTask} + onUpdate={update => { + props.onHomeworkUpdate(update) + setState((prevState) => ({ + ...prevState, + selectedItem: { + isHomework: true, + id: update.isDeleted ? undefined : update.homework.id! + } + })) + }} + isProcessing={props.processingFiles[homework.id!]?.isLoading || false} + onStartProcessing={props.onStartProcessing} + /> + + + } + + const renderTask = (task: HomeworkTaskViewModel & { isModified?: boolean }, homework: HomeworkViewModel) => { + const taskEditMode = task && (task.id! < 0 || task.isModified === true) + return task && + {isMentor && getDatesAlert(task, false)} + { + props.onTaskUpdate(update) + if (update.isDeleted) + setState((prevState) => ({ + ...prevState, + selectedItem: { + isHomework: true, + id: homework!.id + } + })) + }} + toEditHomework={() => toEditHomework(homework!)} getAllHomeworks={() => homeworks}/> + {!props.isMentor && props.isStudentAccepted && < CardActions> + + + + } + + } + + const renderGif = () => + + + const renderLecturerWelcomeScreen = () => + + + Спасибо за ещё один курс + Самое время добавить новое задание! + + + + return + + + {props.isMentor && filterAdded && + + {hideDeferred + ? "только опубликованные задания" + : showOnlyGroupedTest + ? `контрольные работы '${showOnlyGroupedTest}'` + : ""} + + } + {props.isMentor && !filterAdded && (homeworks[0]?.id || 1) > 0 && } + {isMentor && homeworks.length === 0 && renderLecturerWelcomeScreen()} + + {homeworks.map((x: HomeworkViewModel & { isModified?: boolean, hasErrors?: boolean }) => { + return
+ + { + setState(prevState => ({ + ...prevState, + selectedItem: { + data: x, + isHomework: true, + id: x.id, + homeworkFilesInfo: FileInfoConverter.getCourseUnitFilesInfo(courseFilesInfo, CourseUnitType.Homework, x.id!) + } + })) + }}> + + {isMentor && renderHomeworkStatus(x)} + {x.title}{getTip(x)} + + {x.isDeferred && !x.publicationDateNotSet && + + {"🕘 " + renderDate(x.publicationDate!) + " " + renderTime(x.publicationDate!)} + } + {x.tasks?.length === 0 && + + + + + } + + {x.tasks!.map(t => { + setState(prevState => ({ + ...prevState, + selectedItem: { + data: t, + isHomework: false, + id: t.id, + homeworkFilesInfo: [] + } + })) + }} + style={getStyle(false, t.id!)} + sx={{":hover": hoveredItemStyle}}> + {!t.deadlineDateNotSet && + + {t.deadlineDate ? renderDate(t.deadlineDate) : ""} +
+ {t.deadlineDate ? renderTime(t.deadlineDate) : ""} +
+ } + + {renderTaskStatus(t)} + + + + + {t.title}{getTip(t)} + {(t.ltiLaunchData) && ( + + + + )} + + +
)} +
+ {x.id! < 0 && + + + + {props.ltiToolName && ( +
+ handleLtiImport(items, x)} + /> +
+ )} +
} +
; + })} +
+
+
+ + {isHomework + ? renderHomework(selectedItem as HomeworkViewModel) + : renderTask(selectedItem as HomeworkTaskViewModel, selectedItemHomework!)} + + {renderGif()} + + + + {renderGif()} + + + {/* Кнопка "Наверх" для мобильных устройств */} + + + + + +
+} diff --git a/hwproj.front/src/components/Courses/CreateCourse.tsx b/hwproj.front/src/components/Courses/CreateCourse.tsx index 0a481c015..82cd08916 100644 --- a/hwproj.front/src/components/Courses/CreateCourse.tsx +++ b/hwproj.front/src/components/Courses/CreateCourse.tsx @@ -56,6 +56,8 @@ export const CreateCourse: FC = () => { selectedGroups: [], fetchingGroups: false, courseIsLoading: false, + ltiTools: [], + ltiToolName: undefined, }) const {activeStep, completedSteps, baseCourses, selectedBaseCourse} = state @@ -134,6 +136,7 @@ export const CreateCourse: FC = () => { isOpen: true, baseCourseId: selectedBaseCourse?.id, fetchStudents: state.isGroupFromList ? state.fetchStudents : false, + ltiToolName: state.ltiToolName, } try { setCourseIsLoading(true) @@ -151,15 +154,26 @@ export const CreateCourse: FC = () => { useEffect(() => { const loadData = async () => { try { - const userCourses = await ApiSingleton.coursesApi.coursesGetAllUserCourses(); + const [ + userCourses, + programResponse, + ltiToolsResponse, + ] = await Promise.all([ + ApiSingleton.coursesApi.coursesGetAllUserCourses(), + ApiSingleton.coursesApi.coursesGetProgramNames(), + ApiSingleton.ltiToolsApi.ltiToolsGetAll(), + ]); if (!userCourses.length) skipCurrentStep(); setBaseCourses(userCourses); - const programResponse = await ApiSingleton.coursesApi.coursesGetProgramNames(); const programNames = programResponse .map(model => model.programName) .filter((name): name is string => name !== undefined); - setState(prev => ({...prev, programNames})); + setState(prev => ({ + ...prev, + programNames, + ltiTools: ltiToolsResponse + })); } catch (e) { console.error("Error loading data:", e); setBaseCourses([]); 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 fca365547..0021c7e56 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -233,7 +233,8 @@ const CourseHomeworkEditor: FC<{ const task: PostTaskViewModel = { ...t, title: t.title!, - maxRating: t.maxRating! + maxRating: t.maxRating!, + 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 865ebe999..c33a80940 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx @@ -17,6 +17,7 @@ import {getTip} from "../Common/HomeworkTags"; import Lodash from "lodash"; import {appBarStateManager} from "../AppBar"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; +import {LtiLaunchButton} from "@/components/Solutions/LtiLaunchButton"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; @@ -24,6 +25,7 @@ interface ITaskSolutionsState { isLoaded: boolean addSolution: boolean courseId: number + ltiToolName: string homeworkGroupedSolutions: HomeworksGroupUserTaskSolutions[] courseMates: AccountDataDto[] } @@ -50,6 +52,7 @@ const TaskSolutionsPage: FC = () => { const [taskSolutionPage, setTaskSolutionPage] = useState({ isLoaded: false, courseId: 0, + ltiToolName: "", addSolution: false, homeworkGroupedSolutions: [], courseMates: [] @@ -67,7 +70,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(() => { @@ -85,12 +101,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(() => { @@ -164,6 +181,43 @@ const TaskSolutionsPage: FC = () => { })) } + const renderSolutionButton = () => { + if (task.ltiLaunchData) { + return ( + + ) + } + + if (task.canSendSolution) { + return ( + + ); + } + + return null + } + const renderRatingChip = (solutionsDescription: string, color: string, lastRatedSolution: Solution) => { return {solutionsDescription}}> @@ -215,21 +269,9 @@ const TaskSolutionsPage: FC = () => { Только нерешенные
- {task.canSendSolution && } + + {renderSolutionButton()} + diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 120ac3be7..124624452 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -170,6 +170,7 @@ 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]); @@ -190,6 +191,7 @@ const CourseTaskEditor: FC<{ maxRating: maxRating, actionOptions: editOptions, criteria: criteria, + ltiLaunchData: props.speculativeTask.ltiLaunchData }; const updatedTask = isNewTask @@ -288,6 +290,7 @@ const CourseTaskEditor: FC<{ }} /> + {metadata && homeworkPublicationDateIsSet && 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