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