diff --git a/Management.Web/Shared/Components/Quiz/QuizQuestionForm.razor b/Management.Web/Shared/Components/Quiz/QuizQuestionForm.razor index 2841c40..deace05 100644 --- a/Management.Web/Shared/Components/Quiz/QuizQuestionForm.razor +++ b/Management.Web/Shared/Components/Quiz/QuizQuestionForm.razor @@ -82,6 +82,11 @@ UpdateQuestion(Question with { Answers = answers }); } + private void handleTextUpdate(ChangeEventArgs e) + { + var newText = e.Value?.ToString() ?? ""; + UpdateQuestion(Question with { Text = newText }); + } }
@@ -99,6 +104,7 @@ id="question_text" name="question_text" @bind="text" + @oninput="handleTextUpdate" />
diff --git a/Management/Features/Configuration/CoursePlanner.cs b/Management/Features/Configuration/CoursePlanner.cs index be57cc0..9310106 100644 --- a/Management/Features/Configuration/CoursePlanner.cs +++ b/Management/Features/Configuration/CoursePlanner.cs @@ -21,7 +21,7 @@ public class CoursePlanner this.canvas = canvas; } - private Timer _debounceTimer; + private Timer? _debounceTimer; private int _debounceInterval = 1000; private LocalCourse? _localCourse { get; set; } public LocalCourse? LocalCourse @@ -54,6 +54,7 @@ public class CoursePlanner private void saveCourseToFile(LocalCourse courseAsOfDebounce) { _debounceTimer?.Dispose(); + // ignore initial load of course if (LocalCourse == null) { @@ -137,6 +138,8 @@ public class CoursePlanner LocalCourse = await LocalCourse.SyncAssignmentsWithCanvas(canvasId, CanvasAssignments, canvas); CanvasAssignments = await canvas.Assignments.GetAll(canvasId); + + await syncModuleItemsWithCanvas(canvasId); CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules); diff --git a/Management/Features/Configuration/CoursePlannerSyncronizationExtensions.cs b/Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs similarity index 67% rename from Management/Features/Configuration/CoursePlannerSyncronizationExtensions.cs rename to Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs index c796b64..bac8b8d 100644 --- a/Management/Features/Configuration/CoursePlannerSyncronizationExtensions.cs +++ b/Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs @@ -1,75 +1,16 @@ using System.Text.RegularExpressions; using CanvasModel.Assignments; using CanvasModel.Modules; +using CanvasModel.Quizzes; using LocalModels; using Management.Services.Canvas; namespace Management.Planner; -public static partial class CoursePlannerSyncronizationExtensions +public static partial class AssignmentSyncronizationExtensions { - internal static async Task> EnsureAllModulesExistInCanvas( - this LocalCourse localCourse, - ulong canvasId, - IEnumerable canvasModules, - CanvasService canvas - ) - { - var moduleTasks = localCourse.Modules.Select(async module => - { - var canvasModule = canvasModules.FirstOrDefault(cm => cm.Id == module.CanvasId); - if (canvasModule == null) - { - var newModule = await canvas.CreateModule(canvasId, module.Name); - return module with { CanvasId = newModule.Id }; - } - else - return module; - }); - var newModules = await Task.WhenAll(moduleTasks); - return newModules ?? throw new Exception("Error ensuring all modules exist in canvas"); - } - internal static async Task SortCanvasModules( - this LocalCourse localCourse, - ulong canvasId, - IEnumerable canvasModules, - CanvasService canvas - ) - { - var currentCanvasPositions = canvasModules.ToDictionary(m => m.Id, m => m.Position); - foreach (var (localModule, i) in localCourse.Modules.Select((m, i) => (m, i))) - { - var correctPosition = i + 1; - var moduleCanvasId = - localModule.CanvasId ?? throw new Exception("cannot sort module if no module canvas id"); - var currentCanvasPosition = currentCanvasPositions[moduleCanvasId]; - if (currentCanvasPosition != correctPosition) - { - await canvas.UpdateModule(canvasId, moduleCanvasId, localModule.Name, correctPosition); - } - } - } - - internal static async Task SyncModulesWithCanvasData( - this LocalCourse localCourse, - ulong canvasId, - IEnumerable canvasModules, - CanvasService canvas - ) - { - canvasModules = await canvas.GetModules(canvasId); - return localCourse with - { - Modules = localCourse.Modules.Select(m => - { - var canvasModule = canvasModules.FirstOrDefault(cm => cm.Name == m.Name); - return canvasModule == null ? m : m with { CanvasId = canvasModule.Id }; - }) - }; - } - - internal static async Task SyncToCanvas( + internal static async Task SyncAssignmentToCanvas( this LocalCourse localCourse, ulong canvasId, LocalAssignment localAssignment, @@ -114,38 +55,51 @@ public static partial class CoursePlannerSyncronizationExtensions var localHtmlDescription = localAssignment .GetDescriptionHtml(courseAssignmentTemplates) + .Replace("
", "
") .Replace(">", "") .Replace("<", "") .Replace(">", "") - .Replace("<", ""); + .Replace("<", "") + .Replace(""", "") + .Replace("\"", ""); var canvasHtmlDescription = canvasAssignment.Description; canvasHtmlDescription = CanvasScriptTagRegex().Replace(canvasHtmlDescription, ""); canvasHtmlDescription = CanvasLinkTagRegex().Replace(canvasHtmlDescription, ""); canvasHtmlDescription = canvasHtmlDescription + .Replace("
", "
") .Replace(">", "") .Replace("<", "") .Replace(">", "") - .Replace("<", ""); + .Replace("<", "") + .Replace(""", "") + .Replace("\"", ""); - var dueDatesSame = + var canvasComparisonDueDate = canvasAssignment.DueAt != null - && new DateTime( - year: canvasAssignment.DueAt.Value.Year, - month: canvasAssignment.DueAt.Value.Month, - day: canvasAssignment.DueAt.Value.Day, - hour: canvasAssignment.DueAt.Value.Hour, - minute: canvasAssignment.DueAt.Value.Minute, - second: canvasAssignment.DueAt.Value.Second - ) - == new DateTime( + ? new DateTime( + year: canvasAssignment.DueAt.Value.Year, + month: canvasAssignment.DueAt.Value.Month, + day: canvasAssignment.DueAt.Value.Day, + hour: canvasAssignment.DueAt.Value.Hour, + minute: canvasAssignment.DueAt.Value.Minute, + second: canvasAssignment.DueAt.Value.Second + ) + : new DateTime(); + var localComparisonDueDate = + canvasAssignment.DueAt != null + ? new DateTime( year: localAssignment.DueAt.Year, month: localAssignment.DueAt.Month, day: localAssignment.DueAt.Day, hour: localAssignment.DueAt.Hour, minute: localAssignment.DueAt.Minute, second: localAssignment.DueAt.Second - ); + ) + : new DateTime(); + + var dueDatesSame = + canvasAssignment.DueAt != null && canvasComparisonDueDate == localComparisonDueDate; var descriptionSame = canvasHtmlDescription == localHtmlDescription; var nameSame = canvasAssignment.Name == localAssignment.Name; @@ -159,6 +113,9 @@ public static partial class CoursePlannerSyncronizationExtensions { if (!dueDatesSame) { + Console.WriteLine(JsonSerializer.Serialize(canvasAssignment)); + Console.WriteLine(canvasComparisonDueDate); + Console.WriteLine(localComparisonDueDate); Console.WriteLine( $"Due dates different for {localAssignment.Name}, local: {localAssignment.DueAt}, in canvas {canvasAssignment.DueAt}" ); @@ -219,7 +176,7 @@ public static partial class CoursePlannerSyncronizationExtensions var moduleTasks = localCourse.Modules.Select(async m => { var assignmentTasks = m.Assignments.Select( - (a) => localCourse.SyncToCanvas(canvasId, a, canvasAssignments, canvas) + (a) => localCourse.SyncAssignmentToCanvas(canvasId, a, canvasAssignments, canvas) ); var assignments = await Task.WhenAll(assignmentTasks); return m with { Assignments = assignments }; diff --git a/Management/Features/Configuration/Synchronization/ModuleSyncronizationExtensions.cs b/Management/Features/Configuration/Synchronization/ModuleSyncronizationExtensions.cs new file mode 100644 index 0000000..75e0f35 --- /dev/null +++ b/Management/Features/Configuration/Synchronization/ModuleSyncronizationExtensions.cs @@ -0,0 +1,72 @@ +using System.Text.RegularExpressions; +using CanvasModel.Assignments; +using CanvasModel.Modules; +using CanvasModel.Quizzes; +using LocalModels; +using Management.Services.Canvas; + +namespace Management.Planner; + +public static partial class ModuleSyncronizationExtensions +{ + internal static async Task> EnsureAllModulesExistInCanvas( + this LocalCourse localCourse, + ulong canvasId, + IEnumerable canvasModules, + CanvasService canvas + ) + { + var moduleTasks = localCourse.Modules.Select(async module => + { + var canvasModule = canvasModules.FirstOrDefault(cm => cm.Id == module.CanvasId); + if (canvasModule == null) + { + var newModule = await canvas.CreateModule(canvasId, module.Name); + return module with { CanvasId = newModule.Id }; + } + else + return module; + }); + var newModules = await Task.WhenAll(moduleTasks); + return newModules ?? throw new Exception("Error ensuring all modules exist in canvas"); + } + + internal static async Task SortCanvasModules( + this LocalCourse localCourse, + ulong canvasId, + IEnumerable canvasModules, + CanvasService canvas + ) + { + var currentCanvasPositions = canvasModules.ToDictionary(m => m.Id, m => m.Position); + foreach (var (localModule, i) in localCourse.Modules.Select((m, i) => (m, i))) + { + var correctPosition = i + 1; + var moduleCanvasId = + localModule.CanvasId ?? throw new Exception("cannot sort module if no module canvas id"); + var currentCanvasPosition = currentCanvasPositions[moduleCanvasId]; + if (currentCanvasPosition != correctPosition) + { + await canvas.UpdateModule(canvasId, moduleCanvasId, localModule.Name, correctPosition); + } + } + } + + internal static async Task SyncModulesWithCanvasData( + this LocalCourse localCourse, + ulong canvasId, + IEnumerable canvasModules, + CanvasService canvas + ) + { + canvasModules = await canvas.GetModules(canvasId); + return localCourse with + { + Modules = localCourse.Modules.Select(m => + { + var canvasModule = canvasModules.FirstOrDefault(cm => cm.Name == m.Name); + return canvasModule == null ? m : m with { CanvasId = canvasModule.Id }; + }) + }; + } +} diff --git a/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs b/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs new file mode 100644 index 0000000..febfd7a --- /dev/null +++ b/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs @@ -0,0 +1,43 @@ +using System.Text.RegularExpressions; +using CanvasModel.Assignments; +using CanvasModel.Modules; +using CanvasModel.Quizzes; +using LocalModels; +using Management.Services.Canvas; + +namespace Management.Planner; + +public static partial class QuizSyncronizationExtensions +{ + internal static async Task SyncQuizToCanvas( + this LocalCourse localCourse, + ulong canvasId, + LocalQuiz localQuiz, + IEnumerable canvasQuizzes, + CanvasService canvas + ) + { + // TODO actually sync + return localQuiz; + } + + internal static async Task SyncQuizzesWithCanvas( + this LocalCourse localCourse, + ulong canvasId, + IEnumerable canvasQuizzes, + CanvasService canvas + ) + { + var moduleTasks = localCourse.Modules.Select(async m => + { + var quizTasks = m.Quizzes.Select( + (q) => localCourse.SyncQuizToCanvas(canvasId, q, canvasQuizzes, canvas) + ); + var quizzes = await Task.WhenAll(quizTasks); + return m with { Quizzes = quizzes }; + }); + + var modules = await Task.WhenAll(moduleTasks); + return localCourse; + } +} diff --git a/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs b/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs new file mode 100644 index 0000000..d1613b9 --- /dev/null +++ b/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs @@ -0,0 +1,46 @@ +using CanvasModel.Assignments; + +namespace CanvasModel.Quizzes; + +public record CanvasQuiz( + [property: JsonPropertyName("id")] ulong Id, + [property: JsonPropertyName("title")] string Title, + [property: JsonPropertyName("html_url")] string HtmlUrl, + [property: JsonPropertyName("mobile_url")] string MobileUrl, + [property: JsonPropertyName("preview_url")] string PreviewUrl, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("quiz_type")] string QuizType, + [property: JsonPropertyName("assignment_group_id")] ulong AssignmentGroupId, + [property: JsonPropertyName("time_limit")] decimal? TimeLimit, + [property: JsonPropertyName("shuffle_answers")] bool? ShuffleAnswers, + [property: JsonPropertyName("hide_results")] string? HideResults, + [property: JsonPropertyName("show_correct_answers")] bool? ShowCorrectAnswers, + [property: JsonPropertyName("show_correct_answers_last_attempt")] + bool? ShowCorrectAnswersLastAttempt, + [property: JsonPropertyName("show_correct_answers_at")] DateTime? ShowCorrectAnswersAt, + [property: JsonPropertyName("hide_correct_answers_at")] DateTime? HideCorrectAnswersAt, + [property: JsonPropertyName("one_time_results")] bool? OneTimeResults, + [property: JsonPropertyName("scoring_policy")] string? ScoringPolicy, + [property: JsonPropertyName("allowed_attempts")] int AllowedAttempts, + [property: JsonPropertyName("one_question_at_a_time")] bool? OneQuestionAtATime, + [property: JsonPropertyName("question_count")] uint? QuestionCount, + [property: JsonPropertyName("points_possible")] decimal? PointsPossible, + [property: JsonPropertyName("cant_go_back")] bool? CantGoBack, + [property: JsonPropertyName("access_code")] string? AccessCode, + [property: JsonPropertyName("ip_filter")] string? IpFilter, + [property: JsonPropertyName("due_at")] DateTime? DueAt, + [property: JsonPropertyName("lock_at")] DateTime? LockAt, + [property: JsonPropertyName("unlock_at")] DateTime? UnlockAt, + [property: JsonPropertyName("published")] bool? Published, + [property: JsonPropertyName("unpublishable")] bool? Unpublishable, + [property: JsonPropertyName("locked_for_user")] bool? LockedForUser, + [property: JsonPropertyName("lock_info")] CanvasLockInfo? LockInfo, + [property: JsonPropertyName("lock_explanation")] string? LockExplanation, + [property: JsonPropertyName("speedgrader_url")] string? SpeedGraderUrl, + [property: JsonPropertyName("quiz_extensions_url")] string QuizExtensionsUrl, + [property: JsonPropertyName("permissions")] CanvasQuizPermissions Permissions, + [property: JsonPropertyName("all_dates")] object AllDates, + [property: JsonPropertyName("version_number")] uint? VersionNumber, + [property: JsonPropertyName("question_types")] IEnumerable QuestionTypes, + [property: JsonPropertyName("anonymous_submissions")] bool? AnonymousSubmissions +); diff --git a/Management/Models/CanvasModels/Quizzes/CanvasQuizPermissions.cs b/Management/Models/CanvasModels/Quizzes/CanvasQuizPermissions.cs new file mode 100644 index 0000000..7a2c261 --- /dev/null +++ b/Management/Models/CanvasModels/Quizzes/CanvasQuizPermissions.cs @@ -0,0 +1,27 @@ + + +namespace CanvasModel.Quizzes; +public class CanvasQuizPermissions +{ + + [JsonPropertyName("read")] + public bool Read { get; set; } + + [JsonPropertyName("submit")] + public bool Submit { get; set; } + + [JsonPropertyName("create")] + public bool Create { get; set; } + + [JsonPropertyName("manage")] + public bool Manage { get; set; } + + [JsonPropertyName("read_statistics")] + public bool ReadStatistics { get; set; } + + [JsonPropertyName("review_grades")] + public bool ReviewGrades { get; set; } + + [JsonPropertyName("update")] + public bool Update { get; set; } +} diff --git a/Management/Models/Local/LocalModules.cs b/Management/Models/Local/LocalModules.cs index cdd27c1..e1438ff 100644 --- a/Management/Models/Local/LocalModules.cs +++ b/Management/Models/Local/LocalModules.cs @@ -3,6 +3,7 @@ namespace LocalModels; public record LocalModule { public string Name { get; init; } = string.Empty; + public string Id { get; init; } = string.Empty; public IEnumerable Assignments { get; init; } = Enumerable.Empty(); diff --git a/Management/Services/Canvas/CanvasAssignmentService.cs b/Management/Services/Canvas/CanvasAssignmentService.cs index e40dc2e..03540a7 100644 --- a/Management/Services/Canvas/CanvasAssignmentService.cs +++ b/Management/Services/Canvas/CanvasAssignmentService.cs @@ -19,6 +19,7 @@ public class CanvasAssignmentService { var url = $"courses/{courseId}/assignments"; var request = new RestRequest(url); + request.AddParameter("include[]", "overrides"); var assignmentResponse = await utils.PaginatedRequest>(request); return assignmentResponse.SelectMany( assignments => @@ -77,6 +78,8 @@ public class CanvasAssignmentService request.AddHeader("Content-Type", "application/json"); var bodyObj = new { assignment = body }; request.AddBody(bodyObj); + Console.WriteLine(url); + Console.WriteLine(JsonSerializer.Serialize(bodyObj)); await webRequestor.PutAsync(request); await CreateRubric(courseId, localAssignment); diff --git a/Management/Services/WebRequestor.cs b/Management/Services/WebRequestor.cs index 3c7a77e..1f711d1 100644 --- a/Management/Services/WebRequestor.cs +++ b/Management/Services/WebRequestor.cs @@ -13,6 +13,7 @@ public class WebRequestor : IWebRequestor ?? throw new Exception("CANVAS_TOKEN not in environment"); client = new RestClient(BaseUrl); client.AddDefaultHeader("Authorization", $"Bearer {token}"); + } public async Task<(T[]?, RestResponse)> GetManyAsync(RestRequest request) @@ -48,6 +49,7 @@ public class WebRequestor : IWebRequestor public async Task PutAsync(RestRequest request) { + request.AddHeader("Content-Type", "application/json"); var response = await client.ExecutePutAsync(request); // if (!response.IsSuccessful) // { @@ -61,6 +63,7 @@ public class WebRequestor : IWebRequestor public async Task<(T?, RestResponse)> PutAsync(RestRequest request) { + request.AddHeader("Content-Type", "application/json"); var response = await client.ExecutePutAsync(request); return (deserialize(response), response); } diff --git a/requests/assignment.http b/requests/assignment.http index 3d3c07d..71d6b17 100644 --- a/requests/assignment.http +++ b/requests/assignment.http @@ -108,3 +108,20 @@ Authorization: Bearer {{$dotenv CANVAS_TOKEN}} "unlock_at":null } } + + +### +PUT https://snow.instructure.com/api/v1/courses/872095/assignments/12676639 +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} +Content-Type: application/json + +{ + "assignment": { + "due_at": "2021-09-15T23:59:59Z", + "lock_at": "2023-08-22T05:59:00Z" + } +} + +### +GET https://snow.instructure.com/api/v1/courses/872095/assignments/12676639?include[]=overrides +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} \ No newline at end of file diff --git a/requests/quiz.http b/requests/quiz.http new file mode 100644 index 0000000..8205d63 --- /dev/null +++ b/requests/quiz.http @@ -0,0 +1,3 @@ + +GET https://snow.instructure.com/api/v1/courses/705168 +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} \ No newline at end of file