From 28ad344018cf47897ff08f1fd0dc3954296f4316 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Thu, 17 Aug 2023 18:50:59 -0600 Subject: [PATCH] got basic question and answer creation from canvas --- Management.Web/Pages/Index.razor | 46 ++--- Management.Web/Program.cs | 2 +- Management.Web/Shared/Module/QuizDetail.razor | 18 +- .../Features/Configuration/CoursePlanner.cs | 100 +---------- .../AssignmentSyncronizationExtensions.cs | 53 ++++-- .../ModuleSyncronizationExtensions.cs | 95 +++++++++++ .../QuizSyncronizationExtensions.cs | 35 ++-- .../Models/CanvasModels/Quizzes/CanvasQuiz.cs | 161 +++++++++++++----- .../CanvasModels/Quizzes/CanvasQuizAnswer.cs | 55 ++++++ .../Quizzes/CanvasQuizQuestion.cs | 34 ++++ Management/Models/Local/LocalQuiz.cs | 9 +- Management/Models/Local/LocalQuizQuestion.cs | 1 + .../Models/Local/LocalQuizQuestionAnswer.cs | 3 +- .../Canvas/CanvasAssignmentService.cs | 10 +- .../Services/Canvas/CanvasQuizService.cs | 126 ++++++++++++++ Management/Services/Canvas/CanvasService.cs | 11 +- Management/Services/WebRequestor.cs | 2 + requests/quiz.http | 47 ++++- 18 files changed, 604 insertions(+), 204 deletions(-) create mode 100644 Management/Models/CanvasModels/Quizzes/CanvasQuizAnswer.cs create mode 100644 Management/Models/CanvasModels/Quizzes/CanvasQuizQuestion.cs create mode 100644 Management/Services/Canvas/CanvasQuizService.cs diff --git a/Management.Web/Pages/Index.razor b/Management.Web/Pages/Index.razor index 99eb9bd..7dc6434 100644 --- a/Management.Web/Pages/Index.razor +++ b/Management.Web/Pages/Index.razor @@ -79,29 +79,31 @@ @if(planner.LocalCourse != null) { -
- - - +
+
+ + + - - - View In Canvas - + + + View In Canvas + +
@if(planner.LoadingCanvasData) { diff --git a/Management.Web/Program.cs b/Management.Web/Program.cs index 8c30dbd..2f3bd64 100644 --- a/Management.Web/Program.cs +++ b/Management.Web/Program.cs @@ -32,10 +32,10 @@ if (canvas_url == null) builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); - builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Management.Web/Shared/Module/QuizDetail.razor b/Management.Web/Shared/Module/QuizDetail.razor index 894d46d..d3136b8 100644 --- a/Management.Web/Shared/Module/QuizDetail.razor +++ b/Management.Web/Shared/Module/QuizDetail.razor @@ -1,12 +1,13 @@ +@using Management.Web.Shared.Components @using Management.Web.Shared.Components.Quiz @inject DragContainer dragContainer @inject QuizEditorContext quizContext +@inject CoursePlanner planner @inherits DroppableQuiz @code { - private void HandleDragStart() { dragContainer.DropCallback = dropCallback; @@ -16,6 +17,10 @@ { dragContainer.DropCallback = null; } + private bool isSyncedWithCanvas => + planner.CanvasQuizzes != null + ? Quiz.QuizIsCreated(planner.CanvasQuizzes) + : false; } @@ -29,9 +34,16 @@ >
-
+

@Quiz.Name

- + @if(isSyncedWithCanvas) + { + + } + else + { + + }
diff --git a/Management/Features/Configuration/CoursePlanner.cs b/Management/Features/Configuration/CoursePlanner.cs index 9310106..2870ed0 100644 --- a/Management/Features/Configuration/CoursePlanner.cs +++ b/Management/Features/Configuration/CoursePlanner.cs @@ -6,6 +6,7 @@ using CanvasModel.Assignments; using CanvasModel.Modules; using Management.Services.Canvas; using System.Text.RegularExpressions; +using CanvasModel.Quizzes; namespace Management.Planner; @@ -54,7 +55,7 @@ public class CoursePlanner private void saveCourseToFile(LocalCourse courseAsOfDebounce) { _debounceTimer?.Dispose(); - + // ignore initial load of course if (LocalCourse == null) { @@ -71,6 +72,7 @@ public class CoursePlanner public event Action? StateHasChanged; public IEnumerable? CanvasAssignments { get; internal set; } + public IEnumerable? CanvasQuizzes { get; internal set; } public IEnumerable? CanvasModules { get; internal set; } public Dictionary>? CanvasModulesItems { get; internal set; } @@ -87,13 +89,14 @@ public class CoursePlanner LocalCourse?.CanvasId ?? throw new Exception("no canvas id found for selected course"); var assignmentsTask = canvas.Assignments.GetAll(canvasId); + var quizzesTask = canvas.Quizzes.GetAll(canvasId); var modulesTask = canvas.GetModules(canvasId); CanvasAssignments = await assignmentsTask; + CanvasQuizzes = await quizzesTask; CanvasModules = await modulesTask; CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules); - // Console.WriteLine(JsonSerializer.Serialize(CanvasModulesItems)); LoadingCanvasData = false; StateHasChanged?.Invoke(); @@ -107,6 +110,7 @@ public class CoursePlanner || LocalCourse.CanvasId == null || CanvasAssignments == null || CanvasModules == null + || CanvasQuizzes == null ) return; @@ -138,9 +142,9 @@ public class CoursePlanner LocalCourse = await LocalCourse.SyncAssignmentsWithCanvas(canvasId, CanvasAssignments, canvas); CanvasAssignments = await canvas.Assignments.GetAll(canvasId); - + LocalCourse = await LocalCourse.SyncQuizzesWithCanvas(canvasId, CanvasQuizzes, canvas); - await syncModuleItemsWithCanvas(canvasId); + await LocalCourse.SyncModuleItemsWithCanvas(canvasId, CanvasModulesItems, canvas); CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules); LoadingCanvasData = false; @@ -148,94 +152,6 @@ public class CoursePlanner Console.WriteLine("done syncing with canvas\n"); } - private async Task syncModuleItemsWithCanvas(ulong canvasId) - { - if (LocalCourse == null) - throw new Exception("cannot sync modules without localcourse selected"); - if (CanvasModulesItems == null) - throw new Exception("cannot sync modules with canvas if they are not loaded in the variable"); - - foreach (var localModule in LocalCourse.Modules) - { - var moduleCanvasId = - localModule.CanvasId - ?? throw new Exception("cannot sync canvas modules items if module not synced with canvas"); - - bool anyUpdated = await ensureAllItemsCreated(canvasId, localModule, moduleCanvasId); - - var canvasModuleItems = anyUpdated - ? await canvas.GetModuleItems(canvasId, moduleCanvasId) - : CanvasModulesItems[moduleCanvasId]; - - await sortModuleItems(canvasId, localModule, moduleCanvasId, canvasModuleItems); - } - } - - private async Task sortModuleItems( - ulong canvasId, - LocalModule localModule, - ulong moduleCanvasId, - IEnumerable canvasModuleItems - ) - { - var localItemsWithCorrectOrder = localModule.Assignments - .OrderBy(a => a.DueAt) - .Select((a, i) => (Assignment: a, Position: i + 1)); - - var canvasContentIdsByCurrentPosition = - canvasModuleItems.ToDictionary(item => item.Position, item => item.ContentId) - ?? new Dictionary(); - - foreach (var (localAssignment, position) in localItemsWithCorrectOrder) - { - var itemIsInCorrectOrder = - canvasContentIdsByCurrentPosition.ContainsKey(position) - && canvasContentIdsByCurrentPosition[position] == localAssignment.CanvasId; - - var currentCanvasItem = canvasModuleItems.First(i => i.ContentId == localAssignment.CanvasId); - if (!itemIsInCorrectOrder) - { - await canvas.UpdateModuleItem( - canvasId, - moduleCanvasId, - currentCanvasItem with - { - Position = position - } - ); - } - } - } - - private async Task ensureAllItemsCreated( - ulong canvasId, - LocalModule localModule, - ulong moduleCanvasId - ) - { - var anyUpdated = false; - foreach (var localAssignment in localModule.Assignments) - { - var canvasModuleItemContentIds = CanvasModulesItems[moduleCanvasId].Select(i => i.ContentId); - if (!canvasModuleItemContentIds.Contains(localAssignment.CanvasId)) - { - var canvasAssignmentId = - localAssignment.CanvasId - ?? throw new Exception("cannot create module item if assignment does not have canvas id"); - await canvas.CreateModuleItem( - canvasId, - moduleCanvasId, - localAssignment.Name, - "Assignment", - canvasAssignmentId - ); - anyUpdated = true; - } - } - - return anyUpdated; - } - public void Clear() { LocalCourse = null; diff --git a/Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs b/Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs index bac8b8d..ea7eb0e 100644 --- a/Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs +++ b/Management/Features/Configuration/Synchronization/AssignmentSyncronizationExtensions.cs @@ -9,10 +9,9 @@ namespace Management.Planner; public static partial class AssignmentSyncronizationExtensions { - internal static async Task SyncAssignmentToCanvas( this LocalCourse localCourse, - ulong canvasId, + ulong canvasCourseId, LocalAssignment localAssignment, IEnumerable canvasAssignments, CanvasService canvas @@ -25,23 +24,41 @@ public static partial class AssignmentSyncronizationExtensions localCourse.AssignmentTemplates ); - if (canvasAssignment != null) - { - var assignmentNeedsUpdates = localAssignment.NeedsUpdates( + return canvasAssignment != null + ? await updateAssignmentIfNeeded( + localCourse, + canvasCourseId, + localAssignment, canvasAssignments, - localCourse.AssignmentTemplates, - quiet: false - ); - if (assignmentNeedsUpdates) - { - await canvas.Assignments.Update(courseId: canvasId, localAssignment, localHtmlDescription); - } - return localAssignment; - } - else + canvas, + localHtmlDescription + ) + : await canvas.Assignments.Create(canvasCourseId, localAssignment, localHtmlDescription); + } + + private static async Task updateAssignmentIfNeeded( + LocalCourse localCourse, + ulong canvasCourseId, + LocalAssignment localAssignment, + IEnumerable canvasAssignments, + CanvasService canvas, + string localHtmlDescription + ) + { + var assignmentNeedsUpdates = localAssignment.NeedsUpdates( + canvasAssignments, + localCourse.AssignmentTemplates, + quiet: false + ); + if (assignmentNeedsUpdates) { - return await canvas.Assignments.Create(canvasId, localAssignment, localHtmlDescription); + await canvas.Assignments.Update( + courseId: canvasCourseId, + localAssignment, + localHtmlDescription + ); } + return localAssignment; } public static bool NeedsUpdates( @@ -168,7 +185,7 @@ public static partial class AssignmentSyncronizationExtensions internal static async Task SyncAssignmentsWithCanvas( this LocalCourse localCourse, - ulong canvasId, + ulong canvasCourseId, IEnumerable canvasAssignments, CanvasService canvas ) @@ -176,7 +193,7 @@ public static partial class AssignmentSyncronizationExtensions var moduleTasks = localCourse.Modules.Select(async m => { var assignmentTasks = m.Assignments.Select( - (a) => localCourse.SyncAssignmentToCanvas(canvasId, a, canvasAssignments, canvas) + (a) => localCourse.SyncAssignmentToCanvas(canvasCourseId, 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 index 75e0f35..1a550c7 100644 --- a/Management/Features/Configuration/Synchronization/ModuleSyncronizationExtensions.cs +++ b/Management/Features/Configuration/Synchronization/ModuleSyncronizationExtensions.cs @@ -69,4 +69,99 @@ public static partial class ModuleSyncronizationExtensions }) }; } + internal static async Task SortModuleItems( + this LocalModule localModule, + ulong canvasId, + ulong moduleCanvasId, + IEnumerable canvasModuleItems, + CanvasService canvas + ) + { + var localItemsWithCorrectOrder = localModule.Assignments + .OrderBy(a => a.DueAt) + .Select((a, i) => (Assignment: a, Position: i + 1)); + + var canvasContentIdsByCurrentPosition = + canvasModuleItems.ToDictionary(item => item.Position, item => item.ContentId) + ?? new Dictionary(); + + foreach (var (localAssignment, position) in localItemsWithCorrectOrder) + { + var itemIsInCorrectOrder = + canvasContentIdsByCurrentPosition.ContainsKey(position) + && canvasContentIdsByCurrentPosition[position] == localAssignment.CanvasId; + + var currentCanvasItem = canvasModuleItems.First(i => i.ContentId == localAssignment.CanvasId); + if (!itemIsInCorrectOrder) + { + await canvas.UpdateModuleItem( + canvasId, + moduleCanvasId, + currentCanvasItem with + { + Position = position + } + ); + } + } + } + + internal static async Task EnsureAllModulesItemsCreated( + this LocalModule localModule, + ulong canvasId, + ulong moduleCanvasId, + Dictionary> canvasModulesItems, + CanvasService canvas + ) + { + var anyUpdated = false; + foreach (var localAssignment in localModule.Assignments) + { + var canvasModuleItemContentIds = canvasModulesItems[moduleCanvasId].Select(i => i.ContentId); + if (!canvasModuleItemContentIds.Contains(localAssignment.CanvasId)) + { + var canvasAssignmentId = + localAssignment.CanvasId + ?? throw new Exception("cannot create module item if assignment does not have canvas id"); + await canvas.CreateModuleItem( + canvasId, + moduleCanvasId, + localAssignment.Name, + "Assignment", + canvasAssignmentId + ); + anyUpdated = true; + } + } + + return anyUpdated; + } + + internal static async Task SyncModuleItemsWithCanvas( + this LocalCourse localCourse, + ulong canvasId, + Dictionary> canvasModulesItems, + CanvasService canvas + ) + { + foreach (var localModule in localCourse.Modules) + { + var moduleCanvasId = + localModule.CanvasId + ?? throw new Exception("cannot sync canvas modules items if module not synced with canvas"); + + bool anyUpdated = await localModule.EnsureAllModulesItemsCreated( + canvasId, + moduleCanvasId, + canvasModulesItems, + canvas + ); + + var canvasModuleItems = anyUpdated + ? await canvas.GetModuleItems(canvasId, moduleCanvasId) + : canvasModulesItems[moduleCanvasId]; + + await localModule.SortModuleItems(canvasId, moduleCanvasId, canvasModuleItems, canvas); + } + } } diff --git a/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs b/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs index febfd7a..f6d39ae 100644 --- a/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs +++ b/Management/Features/Configuration/Synchronization/QuizSyncronizationExtensions.cs @@ -9,16 +9,9 @@ namespace Management.Planner; public static partial class QuizSyncronizationExtensions { - internal static async Task SyncQuizToCanvas( - this LocalCourse localCourse, - ulong canvasId, - LocalQuiz localQuiz, - IEnumerable canvasQuizzes, - CanvasService canvas - ) + public static bool QuizIsCreated(this LocalQuiz localQuiz, IEnumerable canvasQuizzes) { - // TODO actually sync - return localQuiz; + return canvasQuizzes.Any(q => q.Id == localQuiz.CanvasId); } internal static async Task SyncQuizzesWithCanvas( @@ -38,6 +31,28 @@ public static partial class QuizSyncronizationExtensions }); var modules = await Task.WhenAll(moduleTasks); - return localCourse; + return localCourse with { Modules = modules }; + } + + internal static async Task SyncQuizToCanvas( + this LocalCourse localCourse, + ulong canvasCourseId, + LocalQuiz localQuiz, + IEnumerable canvasQuizzes, + CanvasService canvas + ) + { + var isCreated = localQuiz.QuizIsCreated(canvasQuizzes); + + if (isCreated) + { + // TODO write update + } + else + { + return await canvas.Quizzes.Create(canvasCourseId, localQuiz); + } + + return localQuiz; } } diff --git a/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs b/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs index d1613b9..2e74d9a 100644 --- a/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs +++ b/Management/Models/CanvasModels/Quizzes/CanvasQuiz.cs @@ -2,45 +2,122 @@ 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 -); +public record CanvasQuiz +{ + [JsonPropertyName("id")] + public ulong Id { get; init; } + + [JsonPropertyName("title")] + public required string Title { get; init; } + + [JsonPropertyName("html_url")] + public required string HtmlUrl { get; init; } + + [JsonPropertyName("mobile_url")] + public required string MobileUrl { get; init; } + + [JsonPropertyName("preview_url")] + public string? PreviewUrl { get; init; } + + [JsonPropertyName("description")] + public required string Description { get; init; } + + [JsonPropertyName("quiz_type")] + public required string QuizType { get; init; } + + [JsonPropertyName("assignment_group_id")] + public ulong? AssignmentGroupId { get; init; } + + [JsonPropertyName("time_limit")] + public decimal? TimeLimit { get; init; } + + [JsonPropertyName("shuffle_answers")] + public bool? ShuffleAnswers { get; init; } + + [JsonPropertyName("hide_results")] + public string? HideResults { get; init; } + + [JsonPropertyName("show_correct_answers")] + public bool? ShowCorrectAnswers { get; init; } + + [JsonPropertyName("show_correct_answers_last_attempt")] + public bool? ShowCorrectAnswersLastAttempt { get; init; } + + [JsonPropertyName("show_correct_answers_at")] + public DateTime? ShowCorrectAnswersAt { get; init; } + + [JsonPropertyName("hide_correct_answers_at")] + public DateTime? HideCorrectAnswersAt { get; init; } + + [JsonPropertyName("one_time_results")] + public bool? OneTimeResults { get; init; } + + [JsonPropertyName("scoring_policy")] + public string? ScoringPolicy { get; init; } + + [JsonPropertyName("allowed_attempts")] + public int AllowedAttempts { get; init; } + + [JsonPropertyName("one_question_at_a_time")] + public bool? OneQuestionAtATime { get; init; } + + [JsonPropertyName("question_count")] + public uint? QuestionCount { get; init; } + + [JsonPropertyName("points_possible")] + public decimal? PointsPossible { get; init; } + + [JsonPropertyName("cant_go_back")] + public bool? CantGoBack { get; init; } + + [JsonPropertyName("access_code")] + public string? AccessCode { get; init; } + + [JsonPropertyName("ip_filter")] + public string? IpFilter { get; init; } + + [JsonPropertyName("due_at")] + public DateTime? DueAt { get; init; } + + [JsonPropertyName("lock_at")] + public DateTime? LockAt { get; init; } + + [JsonPropertyName("unlock_at")] + public DateTime? UnlockAt { get; init; } + + [JsonPropertyName("published")] + public bool? Published { get; init; } + + [JsonPropertyName("unpublishable")] + public bool? Unpublishable { get; init; } + + [JsonPropertyName("locked_for_user")] + public bool? LockedForUser { get; init; } + + [JsonPropertyName("lock_info")] + public CanvasLockInfo? LockInfo { get; init; } + + [JsonPropertyName("lock_explanation")] + public string? LockExplanation { get; init; } + + [JsonPropertyName("speedgrader_url")] + public string? SpeedGraderUrl { get; init; } + + [JsonPropertyName("quiz_extensions_url")] + public string? QuizExtensionsUrl { get; init; } + + [JsonPropertyName("permissions")] + public required CanvasQuizPermissions Permissions { get; init; } + + [JsonPropertyName("all_dates")] + public object? AllDates { get; init; } + + [JsonPropertyName("version_number")] + public uint? VersionNumber { get; init; } + + [JsonPropertyName("question_types")] + public IEnumerable? QuestionTypes { get; init; } + + [JsonPropertyName("anonymous_submissions")] + public bool? AnonymousSubmissions { get; init; } +} diff --git a/Management/Models/CanvasModels/Quizzes/CanvasQuizAnswer.cs b/Management/Models/CanvasModels/Quizzes/CanvasQuizAnswer.cs new file mode 100644 index 0000000..8b69e64 --- /dev/null +++ b/Management/Models/CanvasModels/Quizzes/CanvasQuizAnswer.cs @@ -0,0 +1,55 @@ +namespace CanvasModel.Quizzes; + +public record CanvasQuizAnswer +{ + [JsonPropertyName("id")] + public ulong Id { get; init; } + + [JsonPropertyName("text")] + public required string Text { get; init; } + + [JsonPropertyName("html")] + public required string Html { get; init; } + + [JsonPropertyName("weight")] + public double Weight { get; init; } + + // [JsonPropertyName("comments")] + // public string? Comments { get; init; } + + // [JsonPropertyName("text_after_answers")] + // public string? TextAfterAnswers { get; init; } + + // [JsonPropertyName("answer_match_left")] + // public string? AnswerMatchLeft { get; init; } + + // [JsonPropertyName("answer_match_right")] + // public string? AnswerMatchRight { get; init; } + + // [JsonPropertyName("matching_answer_incorrect_matches")] + // public string? MatchingAnswerIncorrectMatches { get; init; } + + // [JsonPropertyName("numerical_answer_type")] + // public string? NumericalAnswerType { get; init; } + + // [JsonPropertyName("exact")] + // public int? Exact { get; init; } + + // [JsonPropertyName("margin")] + // public int? Margin { get; init; } + + // [JsonPropertyName("approximate")] + // public double? Approximate { get; init; } + + // [JsonPropertyName("precision")] + // public int? Precision { get; init; } + + // [JsonPropertyName("start")] + // public int? Start { get; init; } + + // [JsonPropertyName("end")] + // public int? End { get; init; } + + // [JsonPropertyName("blank_id")] + // public int? BlankId { get; init; } +} diff --git a/Management/Models/CanvasModels/Quizzes/CanvasQuizQuestion.cs b/Management/Models/CanvasModels/Quizzes/CanvasQuizQuestion.cs new file mode 100644 index 0000000..1d4d64d --- /dev/null +++ b/Management/Models/CanvasModels/Quizzes/CanvasQuizQuestion.cs @@ -0,0 +1,34 @@ +namespace CanvasModel.Quizzes; + +public record CanvasQuizQuestion +{ + [JsonPropertyName("id")] + public ulong Id { get; init; } + + [JsonPropertyName("quiz_id")] + public int QuizId { get; init; } + + [JsonPropertyName("position")] + public int? Position { get; init; } + + [JsonPropertyName("question_name")] + public required string QuestionName { get; init; } + + [JsonPropertyName("question_type")] + public required string QuestionType { get; init; } + + [JsonPropertyName("question_text")] + public required string QuestionText { get; init; } + + [JsonPropertyName("correct_comments")] + public required string CorrectComments { get; init; } + + [JsonPropertyName("incorrect_comments")] + public required string IncorrectComments { get; init; } + + [JsonPropertyName("neutral_comments")] + public required string NeutralComments { get; init; } + + [JsonPropertyName("answers")] + public IEnumerable? Answers { get; init; } +} diff --git a/Management/Models/Local/LocalQuiz.cs b/Management/Models/Local/LocalQuiz.cs index 3db7543..03c2d1b 100644 --- a/Management/Models/Local/LocalQuiz.cs +++ b/Management/Models/Local/LocalQuiz.cs @@ -11,7 +11,14 @@ public record LocalQuiz public DateTime DueAt { get; init; } public bool ShuffleAnswers { get; init; } public bool OneQuestionAtATime { get; init; } - public int AllowedAttempts { get; init; } + public int AllowedAttempts { get; init; } = -1; // -1 is infinite + public bool ShowCorrectAnswers { get; init; } + public int? TimeLimit { get; init; } = null; + public string? HideResults { get; init; } = null; + + // If null, students can see their results after any attempt. + // If “always”, students can never see their results. + // If “until_after_last_attempt”, students can only see results after their last attempt. (Only valid if allowed_attempts > 1). Defaults to null. public IEnumerable Questions { get; init; } = Enumerable.Empty(); } diff --git a/Management/Models/Local/LocalQuizQuestion.cs b/Management/Models/Local/LocalQuizQuestion.cs index 18088fb..a18d275 100644 --- a/Management/Models/Local/LocalQuizQuestion.cs +++ b/Management/Models/Local/LocalQuizQuestion.cs @@ -2,6 +2,7 @@ namespace LocalModels; public record LocalQuizQuestion { + public ulong? CanvasId { get; set; } public string Id { get; set; } = ""; public string Text { get; init; } = string.Empty; public string QuestionType { get; init; } = string.Empty; diff --git a/Management/Models/Local/LocalQuizQuestionAnswer.cs b/Management/Models/Local/LocalQuizQuestionAnswer.cs index edb5264..7f249eb 100644 --- a/Management/Models/Local/LocalQuizQuestionAnswer.cs +++ b/Management/Models/Local/LocalQuizQuestionAnswer.cs @@ -2,7 +2,8 @@ namespace LocalModels; public record LocalQuizQuestionAnswer { - public string Id { get; set; } = string.Empty; + public ulong? CanvasId { get; init; } + public string Id { get; init; } = string.Empty; //correct gets a weight of 100 in canvas public bool Correct { get; init; } diff --git a/Management/Services/Canvas/CanvasAssignmentService.cs b/Management/Services/Canvas/CanvasAssignmentService.cs index 03540a7..d7ff29a 100644 --- a/Management/Services/Canvas/CanvasAssignmentService.cs +++ b/Management/Services/Canvas/CanvasAssignmentService.cs @@ -30,13 +30,13 @@ public class CanvasAssignmentService } public async Task Create( - ulong courseId, + ulong canvasCourseId, LocalAssignment localAssignment, string htmlDescription ) { Console.WriteLine($"creating assignment: {localAssignment.Name}"); - var url = $"courses/{courseId}/assignments"; + var url = $"courses/{canvasCourseId}/assignments"; var request = new RestRequest(url); var body = new CanvasAssignmentCreationRequest() { @@ -47,7 +47,6 @@ public class CanvasAssignmentService lock_at = localAssignment.LockAt, points_possible = localAssignment.PointsPossible }; - request.AddHeader("Content-Type", "application/json"); var bodyObj = new { assignment = body }; request.AddBody(bodyObj); var (canvasAssignment, response) = await webRequestor.PostAsync(request); @@ -56,7 +55,7 @@ public class CanvasAssignmentService var updatedLocalAssignment = localAssignment with { CanvasId = canvasAssignment.Id }; - await CreateRubric(courseId, updatedLocalAssignment); + await CreateRubric(canvasCourseId, updatedLocalAssignment); return updatedLocalAssignment; } @@ -75,7 +74,6 @@ public class CanvasAssignmentService lock_at = localAssignment.LockAt, points_possible = localAssignment.PointsPossible }; - request.AddHeader("Content-Type", "application/json"); var bodyObj = new { assignment = body }; request.AddBody(bodyObj); Console.WriteLine(url); @@ -145,7 +143,6 @@ public class CanvasAssignmentService var creationUrl = $"courses/{courseId}/rubrics"; var rubricCreationRequest = new RestRequest(creationUrl); rubricCreationRequest.AddBody(body); - rubricCreationRequest.AddHeader("Content-Type", "application/json"); var (rubricCreationResponse, _) = await webRequestor.PostAsync( rubricCreationRequest ); @@ -160,7 +157,6 @@ public class CanvasAssignmentService var adjustmentUrl = $"courses/{courseId}/assignments/{localAssignment.CanvasId}"; var pointAdjustmentRequest = new RestRequest(adjustmentUrl); pointAdjustmentRequest.AddBody(assignmentPointCorrectionBody); - pointAdjustmentRequest.AddHeader("Content-Type", "application/json"); var (_, _) = await webRequestor.PutAsync(pointAdjustmentRequest); } } diff --git a/Management/Services/Canvas/CanvasQuizService.cs b/Management/Services/Canvas/CanvasQuizService.cs new file mode 100644 index 0000000..8bc475b --- /dev/null +++ b/Management/Services/Canvas/CanvasQuizService.cs @@ -0,0 +1,126 @@ +using CanvasModel.Quizzes; +using LocalModels; +using RestSharp; + +namespace Management.Services.Canvas; + +public class CanvasQuizService +{ + private readonly IWebRequestor webRequestor; + private readonly CanvasServiceUtils utils; + + public CanvasQuizService(IWebRequestor webRequestor, CanvasServiceUtils utils) + { + this.webRequestor = webRequestor; + this.utils = utils; + } + + public async Task> GetAll(ulong courseId) + { + var url = $"courses/{courseId}/quizzes"; + var request = new RestRequest(url); + var quizResponse = await utils.PaginatedRequest>(request); + return quizResponse.SelectMany( + quizzes => + quizzes.Select( + a => a with { DueAt = a.DueAt?.ToLocalTime(), LockAt = a.LockAt?.ToLocalTime() } + ) + ); + } + + public async Task Create(ulong canvasCourseId, LocalQuiz localQuiz) + { + Console.WriteLine($"Creating Quiz ${localQuiz.Name}"); + + var url = $"courses/{canvasCourseId}/quizzes"; + var body = new + { + quiz = new + { + title = localQuiz.Name, + description = localQuiz.Description, + // assignment_group_id = "quiz", TODO: support specific assignment groups + time_limit = localQuiz.TimeLimit, + shuffle_answers = localQuiz.ShuffleAnswers, + hide_results = localQuiz.HideResults, + allowed_attempts = localQuiz.AllowedAttempts, + one_question_at_a_time = true, + cant_go_back = false, + due_at = localQuiz.DueAt, + lock_at = localQuiz.LockAt, + } + }; + var request = new RestRequest(url); + request.AddBody(body); + var (canvasQuiz, response) = await webRequestor.PostAsync(request); + if (canvasQuiz == null) + throw new Exception("Created canvas quiz was null"); + + + var updatedQuiz = localQuiz with { CanvasId = canvasQuiz.Id }; + var quizWithQuestions = await CreateQuizQuestions(canvasCourseId, updatedQuiz); + + + return quizWithQuestions; + } + + public async Task CreateQuizQuestions(ulong canvasCourseId, LocalQuiz localQuiz) + { + var tasks = localQuiz.Questions + .Select( + async (question) => + { + var newQuestion = await createQuestionOnly(canvasCourseId, localQuiz, question); + + var answersWithIds = question.Answers + .Select(answer => + { + var canvasAnswer = newQuestion.Answers?.FirstOrDefault(ca => ca.Html == answer.Text); + if (canvasAnswer == null) + { + Console.WriteLine(JsonSerializer.Serialize(newQuestion)); + Console.WriteLine(JsonSerializer.Serialize(question)); + throw new NullReferenceException( + "Could not find canvas answer to update local answer id" + ); + } + return answer with { CanvasId = canvasAnswer.Id }; + }) + .ToArray(); + return question with { CanvasId = newQuestion.Id, Answers = answersWithIds }; + } + ) + .ToArray(); + var updatedQuestions = await Task.WhenAll(tasks); + return localQuiz with { Questions = updatedQuestions }; + } + + private async Task createQuestionOnly( + ulong canvasCourseId, + LocalQuiz localQuiz, + LocalQuizQuestion q + ) + { + var url = $"courses/{canvasCourseId}/quizzes/{localQuiz.CanvasId}/questions"; + var answers = q.Answers + .Select(a => new { answer_html = a.Text, answer_weight = a.Correct ? 100 : 0 }) + .ToArray(); + var body = new + { + question = new + { + question_text = q.Text, + question_type = q.QuestionType+"_question", + possible_points = q.Points, + // position + answers + } + }; + var request = new RestRequest(url); + request.AddBody(body); + var (newQuestion, response) = await webRequestor.PostAsync(request); + if (newQuestion == null) + throw new NullReferenceException("error creating new question, created question is null"); + return newQuestion; + } +} diff --git a/Management/Services/Canvas/CanvasService.cs b/Management/Services/Canvas/CanvasService.cs index 517385f..1ae58bd 100644 --- a/Management/Services/Canvas/CanvasService.cs +++ b/Management/Services/Canvas/CanvasService.cs @@ -13,16 +13,19 @@ public class CanvasService private readonly CanvasServiceUtils utils; public CanvasAssignmentService Assignments { get; } + public CanvasQuizService Quizzes { get; } public CanvasService( IWebRequestor webRequestor, CanvasServiceUtils utils, - CanvasAssignmentService Assignments + CanvasAssignmentService Assignments, + CanvasQuizService Quizzes ) { this.webRequestor = webRequestor; this.utils = utils; this.Assignments = Assignments; + this.Quizzes = Quizzes; } public async Task> GetTerms() @@ -51,8 +54,8 @@ public class CanvasService if (data == null) { - System.Console.WriteLine(response.Content); - System.Console.WriteLine(response.ResponseUri); + Console.WriteLine(response.Content); + Console.WriteLine(response.ResponseUri); throw new Exception("error getting course from canvas"); } return data; @@ -151,7 +154,6 @@ public class CanvasService var body = new { module_item = new { title = item.Title, position = item.Position } }; var request = new RestRequest(url); request.AddBody(body); - request.AddHeader("Content-Type", "application/json"); var (newItem, response) = await webRequestor.PutAsync(request); if (newItem == null) @@ -179,7 +181,6 @@ public class CanvasService }; var request = new RestRequest(url); request.AddBody(body); - request.AddHeader("Content-Type", "application/json"); var (newItem, response) = await webRequestor.PostAsync(request); if (newItem == null) diff --git a/Management/Services/WebRequestor.cs b/Management/Services/WebRequestor.cs index 1f711d1..2ae60af 100644 --- a/Management/Services/WebRequestor.cs +++ b/Management/Services/WebRequestor.cs @@ -30,6 +30,7 @@ public class WebRequestor : IWebRequestor public async Task PostAsync(RestRequest request) { + request.AddHeader("Content-Type", "application/json"); var response = await client.ExecutePostAsync(request); if (!response.IsSuccessful) { @@ -43,6 +44,7 @@ public class WebRequestor : IWebRequestor public async Task<(T?, RestResponse)> PostAsync(RestRequest request) { + request.AddHeader("Content-Type", "application/json"); var response = await client.ExecutePostAsync(request); return (deserialize(response), response); } diff --git a/requests/quiz.http b/requests/quiz.http index 8205d63..c17e4a4 100644 --- a/requests/quiz.http +++ b/requests/quiz.http @@ -1,3 +1,46 @@ -GET https://snow.instructure.com/api/v1/courses/705168 -Authorization: Bearer {{$dotenv CANVAS_TOKEN}} \ No newline at end of file +GET https://snow.instructure.com/api/v1/courses/871954/quizzes +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} + +### +GET https://snow.instructure.com/api/v1/courses/871954/assignments +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} + +### +POST https://snow.instructure.com/api/v1/courses/871954/quizzes/3236013/questions +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} +Content-Type: application/json + +{ + "question":{ + "question_text": "Other clues to how things work come from their visible structure. Specifically from _____, _____, and _____", + "question_type": "multiple_answers_question", + "points_possible": 3, + "answers": [ + { + "answer_text": "color", + "answer_weight": 0 + }, + { + "answer_text": "affordances", + "answer_weight": 100 + }, + { + "answer_text": "structure", + "answer_weight": 0 + }, + { + "answer_text": "constraints", + "answer_weight": 100 + }, + { + "answer_text": "mappings", + "answer_weight": 100 + }, + { + "answer_text": "placement", + "answer_weight": 0 + } + ] + } +} \ No newline at end of file