got basic question and answer creation from canvas

This commit is contained in:
2023-08-17 18:50:59 -06:00
parent 4fb257e000
commit 28ad344018
18 changed files with 604 additions and 204 deletions

View File

@@ -79,29 +79,31 @@
@if(planner.LocalCourse != null) @if(planner.LocalCourse != null)
{ {
<div class="mb-3"> <div class="mb-3 d-flex justify-content-between" style="height: 4em;">
<button <div class="my-auto">
@onclick="planner.Clear" <button
class="btn btn-primary" @onclick="planner.Clear"
> class="btn btn-primary"
Select New Course >
</button> Select New Course
<CourseSettings /> </button>
<AssignmentTemplateManagement /> <CourseSettings />
<AssignmentTemplateManagement />
<button <button
class="btn btn-outline-primary" class="btn btn-outline-primary"
@onclick="planner.SyncWithCanvas" @onclick="planner.SyncWithCanvas"
> >
Sync With Canvas Sync With Canvas
</button> </button>
<a <a
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
target="_blank" target="_blank"
href="@($"{Environment.GetEnvironmentVariable("CANVAS_URL")}/courses/{planner.LocalCourse.CanvasId}")" href="@($"{Environment.GetEnvironmentVariable("CANVAS_URL")}/courses/{planner.LocalCourse.CanvasId}")"
> >
View In Canvas View In Canvas
</a> </a>
</div>
@if(planner.LoadingCanvasData) @if(planner.LoadingCanvasData)
{ {

View File

@@ -32,10 +32,10 @@ if (canvas_url == null)
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<IWebRequestor, WebRequestor>(); builder.Services.AddScoped<IWebRequestor, WebRequestor>();
builder.Services.AddScoped<CanvasServiceUtils>(); builder.Services.AddScoped<CanvasServiceUtils>();
builder.Services.AddScoped<CanvasAssignmentService>(); builder.Services.AddScoped<CanvasAssignmentService>();
builder.Services.AddScoped<CanvasQuizService>();
builder.Services.AddScoped<CanvasService, CanvasService>(); builder.Services.AddScoped<CanvasService, CanvasService>();
builder.Services.AddScoped<YamlManager>(); builder.Services.AddScoped<YamlManager>();

View File

@@ -1,12 +1,13 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Quiz @using Management.Web.Shared.Components.Quiz
@inject DragContainer dragContainer @inject DragContainer dragContainer
@inject QuizEditorContext quizContext @inject QuizEditorContext quizContext
@inject CoursePlanner planner
@inherits DroppableQuiz @inherits DroppableQuiz
@code { @code {
private void HandleDragStart() private void HandleDragStart()
{ {
dragContainer.DropCallback = dropCallback; dragContainer.DropCallback = dropCallback;
@@ -16,6 +17,10 @@
{ {
dragContainer.DropCallback = null; dragContainer.DropCallback = null;
} }
private bool isSyncedWithCanvas =>
planner.CanvasQuizzes != null
? Quiz.QuizIsCreated(planner.CanvasQuizzes)
: false;
} }
@@ -29,9 +34,16 @@
> >
<div class="card"> <div class="card">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="card-title pt-2 px-2 m-0"> <div class="card-title pt-2 px-2 m-0 d-flex justify-content-between">
<h4>@Quiz.Name</h4> <h4>@Quiz.Name</h4>
@if(isSyncedWithCanvas)
{
<CheckIcon />
}
else
{
<SyncIcon />
}
</div> </div>
<div class="card-text overflow-hidden p-2"> <div class="card-text overflow-hidden p-2">

View File

@@ -6,6 +6,7 @@ using CanvasModel.Assignments;
using CanvasModel.Modules; using CanvasModel.Modules;
using Management.Services.Canvas; using Management.Services.Canvas;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CanvasModel.Quizzes;
namespace Management.Planner; namespace Management.Planner;
@@ -71,6 +72,7 @@ public class CoursePlanner
public event Action? StateHasChanged; public event Action? StateHasChanged;
public IEnumerable<CanvasAssignment>? CanvasAssignments { get; internal set; } public IEnumerable<CanvasAssignment>? CanvasAssignments { get; internal set; }
public IEnumerable<CanvasQuiz>? CanvasQuizzes { get; internal set; }
public IEnumerable<CanvasModule>? CanvasModules { get; internal set; } public IEnumerable<CanvasModule>? CanvasModules { get; internal set; }
public Dictionary<ulong, IEnumerable<CanvasModuleItem>>? CanvasModulesItems { get; internal set; } public Dictionary<ulong, IEnumerable<CanvasModuleItem>>? CanvasModulesItems { get; internal set; }
@@ -87,13 +89,14 @@ public class CoursePlanner
LocalCourse?.CanvasId ?? throw new Exception("no canvas id found for selected course"); LocalCourse?.CanvasId ?? throw new Exception("no canvas id found for selected course");
var assignmentsTask = canvas.Assignments.GetAll(canvasId); var assignmentsTask = canvas.Assignments.GetAll(canvasId);
var quizzesTask = canvas.Quizzes.GetAll(canvasId);
var modulesTask = canvas.GetModules(canvasId); var modulesTask = canvas.GetModules(canvasId);
CanvasAssignments = await assignmentsTask; CanvasAssignments = await assignmentsTask;
CanvasQuizzes = await quizzesTask;
CanvasModules = await modulesTask; CanvasModules = await modulesTask;
CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules); CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules);
// Console.WriteLine(JsonSerializer.Serialize(CanvasModulesItems));
LoadingCanvasData = false; LoadingCanvasData = false;
StateHasChanged?.Invoke(); StateHasChanged?.Invoke();
@@ -107,6 +110,7 @@ public class CoursePlanner
|| LocalCourse.CanvasId == null || LocalCourse.CanvasId == null
|| CanvasAssignments == null || CanvasAssignments == null
|| CanvasModules == null || CanvasModules == null
|| CanvasQuizzes == null
) )
return; return;
@@ -138,9 +142,9 @@ public class CoursePlanner
LocalCourse = await LocalCourse.SyncAssignmentsWithCanvas(canvasId, CanvasAssignments, canvas); LocalCourse = await LocalCourse.SyncAssignmentsWithCanvas(canvasId, CanvasAssignments, canvas);
CanvasAssignments = await canvas.Assignments.GetAll(canvasId); CanvasAssignments = await canvas.Assignments.GetAll(canvasId);
LocalCourse = await LocalCourse.SyncQuizzesWithCanvas(canvasId, CanvasQuizzes, canvas);
await LocalCourse.SyncModuleItemsWithCanvas(canvasId, CanvasModulesItems, canvas);
await syncModuleItemsWithCanvas(canvasId);
CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules); CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules);
LoadingCanvasData = false; LoadingCanvasData = false;
@@ -148,94 +152,6 @@ public class CoursePlanner
Console.WriteLine("done syncing with canvas\n"); 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<CanvasModuleItem> 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<int, ulong?>();
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<bool> 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() public void Clear()
{ {
LocalCourse = null; LocalCourse = null;

View File

@@ -9,10 +9,9 @@ namespace Management.Planner;
public static partial class AssignmentSyncronizationExtensions public static partial class AssignmentSyncronizationExtensions
{ {
internal static async Task<LocalAssignment> SyncAssignmentToCanvas( internal static async Task<LocalAssignment> SyncAssignmentToCanvas(
this LocalCourse localCourse, this LocalCourse localCourse,
ulong canvasId, ulong canvasCourseId,
LocalAssignment localAssignment, LocalAssignment localAssignment,
IEnumerable<CanvasAssignment> canvasAssignments, IEnumerable<CanvasAssignment> canvasAssignments,
CanvasService canvas CanvasService canvas
@@ -25,23 +24,41 @@ public static partial class AssignmentSyncronizationExtensions
localCourse.AssignmentTemplates localCourse.AssignmentTemplates
); );
if (canvasAssignment != null) return canvasAssignment != null
{ ? await updateAssignmentIfNeeded(
var assignmentNeedsUpdates = localAssignment.NeedsUpdates( localCourse,
canvasCourseId,
localAssignment,
canvasAssignments, canvasAssignments,
localCourse.AssignmentTemplates, canvas,
quiet: false localHtmlDescription
); )
if (assignmentNeedsUpdates) : await canvas.Assignments.Create(canvasCourseId, localAssignment, localHtmlDescription);
{ }
await canvas.Assignments.Update(courseId: canvasId, localAssignment, localHtmlDescription);
} private static async Task<LocalAssignment> updateAssignmentIfNeeded(
return localAssignment; LocalCourse localCourse,
} ulong canvasCourseId,
else LocalAssignment localAssignment,
IEnumerable<CanvasAssignment> 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( public static bool NeedsUpdates(
@@ -168,7 +185,7 @@ public static partial class AssignmentSyncronizationExtensions
internal static async Task<LocalCourse> SyncAssignmentsWithCanvas( internal static async Task<LocalCourse> SyncAssignmentsWithCanvas(
this LocalCourse localCourse, this LocalCourse localCourse,
ulong canvasId, ulong canvasCourseId,
IEnumerable<CanvasAssignment> canvasAssignments, IEnumerable<CanvasAssignment> canvasAssignments,
CanvasService canvas CanvasService canvas
) )
@@ -176,7 +193,7 @@ public static partial class AssignmentSyncronizationExtensions
var moduleTasks = localCourse.Modules.Select(async m => var moduleTasks = localCourse.Modules.Select(async m =>
{ {
var assignmentTasks = m.Assignments.Select( 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); var assignments = await Task.WhenAll(assignmentTasks);
return m with { Assignments = assignments }; return m with { Assignments = assignments };

View File

@@ -69,4 +69,99 @@ public static partial class ModuleSyncronizationExtensions
}) })
}; };
} }
internal static async Task SortModuleItems(
this LocalModule localModule,
ulong canvasId,
ulong moduleCanvasId,
IEnumerable<CanvasModuleItem> 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<int, ulong?>();
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<bool> EnsureAllModulesItemsCreated(
this LocalModule localModule,
ulong canvasId,
ulong moduleCanvasId,
Dictionary<ulong, IEnumerable<CanvasModuleItem>> 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<ulong, IEnumerable<CanvasModuleItem>> 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);
}
}
} }

View File

@@ -9,16 +9,9 @@ namespace Management.Planner;
public static partial class QuizSyncronizationExtensions public static partial class QuizSyncronizationExtensions
{ {
internal static async Task<LocalQuiz> SyncQuizToCanvas( public static bool QuizIsCreated(this LocalQuiz localQuiz, IEnumerable<CanvasQuiz> canvasQuizzes)
this LocalCourse localCourse,
ulong canvasId,
LocalQuiz localQuiz,
IEnumerable<CanvasQuiz> canvasQuizzes,
CanvasService canvas
)
{ {
// TODO actually sync return canvasQuizzes.Any(q => q.Id == localQuiz.CanvasId);
return localQuiz;
} }
internal static async Task<LocalCourse> SyncQuizzesWithCanvas( internal static async Task<LocalCourse> SyncQuizzesWithCanvas(
@@ -38,6 +31,28 @@ public static partial class QuizSyncronizationExtensions
}); });
var modules = await Task.WhenAll(moduleTasks); var modules = await Task.WhenAll(moduleTasks);
return localCourse; return localCourse with { Modules = modules };
}
internal static async Task<LocalQuiz> SyncQuizToCanvas(
this LocalCourse localCourse,
ulong canvasCourseId,
LocalQuiz localQuiz,
IEnumerable<CanvasQuiz> canvasQuizzes,
CanvasService canvas
)
{
var isCreated = localQuiz.QuizIsCreated(canvasQuizzes);
if (isCreated)
{
// TODO write update
}
else
{
return await canvas.Quizzes.Create(canvasCourseId, localQuiz);
}
return localQuiz;
} }
} }

View File

@@ -2,45 +2,122 @@ using CanvasModel.Assignments;
namespace CanvasModel.Quizzes; namespace CanvasModel.Quizzes;
public record CanvasQuiz( public record CanvasQuiz
[property: JsonPropertyName("id")] ulong Id, {
[property: JsonPropertyName("title")] string Title, [JsonPropertyName("id")]
[property: JsonPropertyName("html_url")] string HtmlUrl, public ulong Id { get; init; }
[property: JsonPropertyName("mobile_url")] string MobileUrl,
[property: JsonPropertyName("preview_url")] string PreviewUrl, [JsonPropertyName("title")]
[property: JsonPropertyName("description")] string Description, public required string Title { get; init; }
[property: JsonPropertyName("quiz_type")] string QuizType,
[property: JsonPropertyName("assignment_group_id")] ulong AssignmentGroupId, [JsonPropertyName("html_url")]
[property: JsonPropertyName("time_limit")] decimal? TimeLimit, public required string HtmlUrl { get; init; }
[property: JsonPropertyName("shuffle_answers")] bool? ShuffleAnswers,
[property: JsonPropertyName("hide_results")] string? HideResults, [JsonPropertyName("mobile_url")]
[property: JsonPropertyName("show_correct_answers")] bool? ShowCorrectAnswers, public required string MobileUrl { get; init; }
[property: JsonPropertyName("show_correct_answers_last_attempt")]
bool? ShowCorrectAnswersLastAttempt, [JsonPropertyName("preview_url")]
[property: JsonPropertyName("show_correct_answers_at")] DateTime? ShowCorrectAnswersAt, public string? PreviewUrl { get; init; }
[property: JsonPropertyName("hide_correct_answers_at")] DateTime? HideCorrectAnswersAt,
[property: JsonPropertyName("one_time_results")] bool? OneTimeResults, [JsonPropertyName("description")]
[property: JsonPropertyName("scoring_policy")] string? ScoringPolicy, public required string Description { get; init; }
[property: JsonPropertyName("allowed_attempts")] int AllowedAttempts,
[property: JsonPropertyName("one_question_at_a_time")] bool? OneQuestionAtATime, [JsonPropertyName("quiz_type")]
[property: JsonPropertyName("question_count")] uint? QuestionCount, public required string QuizType { get; init; }
[property: JsonPropertyName("points_possible")] decimal? PointsPossible,
[property: JsonPropertyName("cant_go_back")] bool? CantGoBack, [JsonPropertyName("assignment_group_id")]
[property: JsonPropertyName("access_code")] string? AccessCode, public ulong? AssignmentGroupId { get; init; }
[property: JsonPropertyName("ip_filter")] string? IpFilter,
[property: JsonPropertyName("due_at")] DateTime? DueAt, [JsonPropertyName("time_limit")]
[property: JsonPropertyName("lock_at")] DateTime? LockAt, public decimal? TimeLimit { get; init; }
[property: JsonPropertyName("unlock_at")] DateTime? UnlockAt,
[property: JsonPropertyName("published")] bool? Published, [JsonPropertyName("shuffle_answers")]
[property: JsonPropertyName("unpublishable")] bool? Unpublishable, public bool? ShuffleAnswers { get; init; }
[property: JsonPropertyName("locked_for_user")] bool? LockedForUser,
[property: JsonPropertyName("lock_info")] CanvasLockInfo? LockInfo, [JsonPropertyName("hide_results")]
[property: JsonPropertyName("lock_explanation")] string? LockExplanation, public string? HideResults { get; init; }
[property: JsonPropertyName("speedgrader_url")] string? SpeedGraderUrl,
[property: JsonPropertyName("quiz_extensions_url")] string QuizExtensionsUrl, [JsonPropertyName("show_correct_answers")]
[property: JsonPropertyName("permissions")] CanvasQuizPermissions Permissions, public bool? ShowCorrectAnswers { get; init; }
[property: JsonPropertyName("all_dates")] object AllDates,
[property: JsonPropertyName("version_number")] uint? VersionNumber, [JsonPropertyName("show_correct_answers_last_attempt")]
[property: JsonPropertyName("question_types")] IEnumerable<string> QuestionTypes, public bool? ShowCorrectAnswersLastAttempt { get; init; }
[property: JsonPropertyName("anonymous_submissions")] bool? AnonymousSubmissions
); [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<string>? QuestionTypes { get; init; }
[JsonPropertyName("anonymous_submissions")]
public bool? AnonymousSubmissions { get; init; }
}

View File

@@ -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; }
}

View File

@@ -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<CanvasQuizAnswer>? Answers { get; init; }
}

View File

@@ -11,7 +11,14 @@ public record LocalQuiz
public DateTime DueAt { get; init; } public DateTime DueAt { get; init; }
public bool ShuffleAnswers { get; init; } public bool ShuffleAnswers { get; init; }
public bool OneQuestionAtATime { 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<LocalQuizQuestion> Questions { get; init; } = public IEnumerable<LocalQuizQuestion> Questions { get; init; } =
Enumerable.Empty<LocalQuizQuestion>(); Enumerable.Empty<LocalQuizQuestion>();
} }

View File

@@ -2,6 +2,7 @@ namespace LocalModels;
public record LocalQuizQuestion public record LocalQuizQuestion
{ {
public ulong? CanvasId { get; set; }
public string Id { get; set; } = ""; public string Id { get; set; } = "";
public string Text { get; init; } = string.Empty; public string Text { get; init; } = string.Empty;
public string QuestionType { get; init; } = string.Empty; public string QuestionType { get; init; } = string.Empty;

View File

@@ -2,7 +2,8 @@ namespace LocalModels;
public record LocalQuizQuestionAnswer 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 //correct gets a weight of 100 in canvas
public bool Correct { get; init; } public bool Correct { get; init; }

View File

@@ -30,13 +30,13 @@ public class CanvasAssignmentService
} }
public async Task<LocalAssignment> Create( public async Task<LocalAssignment> Create(
ulong courseId, ulong canvasCourseId,
LocalAssignment localAssignment, LocalAssignment localAssignment,
string htmlDescription string htmlDescription
) )
{ {
Console.WriteLine($"creating assignment: {localAssignment.Name}"); Console.WriteLine($"creating assignment: {localAssignment.Name}");
var url = $"courses/{courseId}/assignments"; var url = $"courses/{canvasCourseId}/assignments";
var request = new RestRequest(url); var request = new RestRequest(url);
var body = new CanvasAssignmentCreationRequest() var body = new CanvasAssignmentCreationRequest()
{ {
@@ -47,7 +47,6 @@ public class CanvasAssignmentService
lock_at = localAssignment.LockAt, lock_at = localAssignment.LockAt,
points_possible = localAssignment.PointsPossible points_possible = localAssignment.PointsPossible
}; };
request.AddHeader("Content-Type", "application/json");
var bodyObj = new { assignment = body }; var bodyObj = new { assignment = body };
request.AddBody(bodyObj); request.AddBody(bodyObj);
var (canvasAssignment, response) = await webRequestor.PostAsync<CanvasAssignment>(request); var (canvasAssignment, response) = await webRequestor.PostAsync<CanvasAssignment>(request);
@@ -56,7 +55,7 @@ public class CanvasAssignmentService
var updatedLocalAssignment = localAssignment with { CanvasId = canvasAssignment.Id }; var updatedLocalAssignment = localAssignment with { CanvasId = canvasAssignment.Id };
await CreateRubric(courseId, updatedLocalAssignment); await CreateRubric(canvasCourseId, updatedLocalAssignment);
return updatedLocalAssignment; return updatedLocalAssignment;
} }
@@ -75,7 +74,6 @@ public class CanvasAssignmentService
lock_at = localAssignment.LockAt, lock_at = localAssignment.LockAt,
points_possible = localAssignment.PointsPossible points_possible = localAssignment.PointsPossible
}; };
request.AddHeader("Content-Type", "application/json");
var bodyObj = new { assignment = body }; var bodyObj = new { assignment = body };
request.AddBody(bodyObj); request.AddBody(bodyObj);
Console.WriteLine(url); Console.WriteLine(url);
@@ -145,7 +143,6 @@ public class CanvasAssignmentService
var creationUrl = $"courses/{courseId}/rubrics"; var creationUrl = $"courses/{courseId}/rubrics";
var rubricCreationRequest = new RestRequest(creationUrl); var rubricCreationRequest = new RestRequest(creationUrl);
rubricCreationRequest.AddBody(body); rubricCreationRequest.AddBody(body);
rubricCreationRequest.AddHeader("Content-Type", "application/json");
var (rubricCreationResponse, _) = await webRequestor.PostAsync<CanvasRubricCreationResponse>( var (rubricCreationResponse, _) = await webRequestor.PostAsync<CanvasRubricCreationResponse>(
rubricCreationRequest rubricCreationRequest
); );
@@ -160,7 +157,6 @@ public class CanvasAssignmentService
var adjustmentUrl = $"courses/{courseId}/assignments/{localAssignment.CanvasId}"; var adjustmentUrl = $"courses/{courseId}/assignments/{localAssignment.CanvasId}";
var pointAdjustmentRequest = new RestRequest(adjustmentUrl); var pointAdjustmentRequest = new RestRequest(adjustmentUrl);
pointAdjustmentRequest.AddBody(assignmentPointCorrectionBody); pointAdjustmentRequest.AddBody(assignmentPointCorrectionBody);
pointAdjustmentRequest.AddHeader("Content-Type", "application/json");
var (_, _) = await webRequestor.PutAsync<CanvasAssignment>(pointAdjustmentRequest); var (_, _) = await webRequestor.PutAsync<CanvasAssignment>(pointAdjustmentRequest);
} }
} }

View File

@@ -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<IEnumerable<CanvasQuiz>> GetAll(ulong courseId)
{
var url = $"courses/{courseId}/quizzes";
var request = new RestRequest(url);
var quizResponse = await utils.PaginatedRequest<IEnumerable<CanvasQuiz>>(request);
return quizResponse.SelectMany(
quizzes =>
quizzes.Select(
a => a with { DueAt = a.DueAt?.ToLocalTime(), LockAt = a.LockAt?.ToLocalTime() }
)
);
}
public async Task<LocalQuiz> 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<CanvasQuiz>(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<LocalQuiz> 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<CanvasQuizQuestion> 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<CanvasQuizQuestion>(request);
if (newQuestion == null)
throw new NullReferenceException("error creating new question, created question is null");
return newQuestion;
}
}

View File

@@ -13,16 +13,19 @@ public class CanvasService
private readonly CanvasServiceUtils utils; private readonly CanvasServiceUtils utils;
public CanvasAssignmentService Assignments { get; } public CanvasAssignmentService Assignments { get; }
public CanvasQuizService Quizzes { get; }
public CanvasService( public CanvasService(
IWebRequestor webRequestor, IWebRequestor webRequestor,
CanvasServiceUtils utils, CanvasServiceUtils utils,
CanvasAssignmentService Assignments CanvasAssignmentService Assignments,
CanvasQuizService Quizzes
) )
{ {
this.webRequestor = webRequestor; this.webRequestor = webRequestor;
this.utils = utils; this.utils = utils;
this.Assignments = Assignments; this.Assignments = Assignments;
this.Quizzes = Quizzes;
} }
public async Task<IEnumerable<EnrollmentTermModel>> GetTerms() public async Task<IEnumerable<EnrollmentTermModel>> GetTerms()
@@ -51,8 +54,8 @@ public class CanvasService
if (data == null) if (data == null)
{ {
System.Console.WriteLine(response.Content); Console.WriteLine(response.Content);
System.Console.WriteLine(response.ResponseUri); Console.WriteLine(response.ResponseUri);
throw new Exception("error getting course from canvas"); throw new Exception("error getting course from canvas");
} }
return data; return data;
@@ -151,7 +154,6 @@ public class CanvasService
var body = new { module_item = new { title = item.Title, position = item.Position } }; var body = new { module_item = new { title = item.Title, position = item.Position } };
var request = new RestRequest(url); var request = new RestRequest(url);
request.AddBody(body); request.AddBody(body);
request.AddHeader("Content-Type", "application/json");
var (newItem, response) = await webRequestor.PutAsync<CanvasModuleItem>(request); var (newItem, response) = await webRequestor.PutAsync<CanvasModuleItem>(request);
if (newItem == null) if (newItem == null)
@@ -179,7 +181,6 @@ public class CanvasService
}; };
var request = new RestRequest(url); var request = new RestRequest(url);
request.AddBody(body); request.AddBody(body);
request.AddHeader("Content-Type", "application/json");
var (newItem, response) = await webRequestor.PostAsync<CanvasModuleItem>(request); var (newItem, response) = await webRequestor.PostAsync<CanvasModuleItem>(request);
if (newItem == null) if (newItem == null)

View File

@@ -30,6 +30,7 @@ public class WebRequestor : IWebRequestor
public async Task<RestResponse> PostAsync(RestRequest request) public async Task<RestResponse> PostAsync(RestRequest request)
{ {
request.AddHeader("Content-Type", "application/json");
var response = await client.ExecutePostAsync(request); var response = await client.ExecutePostAsync(request);
if (!response.IsSuccessful) if (!response.IsSuccessful)
{ {
@@ -43,6 +44,7 @@ public class WebRequestor : IWebRequestor
public async Task<(T?, RestResponse)> PostAsync<T>(RestRequest request) public async Task<(T?, RestResponse)> PostAsync<T>(RestRequest request)
{ {
request.AddHeader("Content-Type", "application/json");
var response = await client.ExecutePostAsync(request); var response = await client.ExecutePostAsync(request);
return (deserialize<T>(response), response); return (deserialize<T>(response), response);
} }

View File

@@ -1,3 +1,46 @@
GET https://snow.instructure.com/api/v1/courses/705168 GET https://snow.instructure.com/api/v1/courses/871954/quizzes
Authorization: Bearer {{$dotenv CANVAS_TOKEN}} 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
}
]
}
}