put a little more work into quizzes

This commit is contained in:
2023-08-16 22:59:08 -06:00
parent 4def2fd689
commit 4fb257e000
12 changed files with 258 additions and 77 deletions

View File

@@ -82,6 +82,11 @@
UpdateQuestion(Question with { Answers = answers });
}
private void handleTextUpdate(ChangeEventArgs e)
{
var newText = e.Value?.ToString() ?? "";
UpdateQuestion(Question with { Text = newText });
}
}
<div class="my-1 p-2 border border-3 rounded">
@@ -99,6 +104,7 @@
id="question_text"
name="question_text"
@bind="text"
@oninput="handleTextUpdate"
/>
</div>
<div class="col-auto">

View File

@@ -21,7 +21,7 @@ public class CoursePlanner
this.canvas = canvas;
}
private Timer _debounceTimer;
private Timer? _debounceTimer;
private int _debounceInterval = 1000;
private LocalCourse? _localCourse { get; set; }
public LocalCourse? LocalCourse
@@ -54,6 +54,7 @@ public class CoursePlanner
private void saveCourseToFile(LocalCourse courseAsOfDebounce)
{
_debounceTimer?.Dispose();
// ignore initial load of course
if (LocalCourse == null)
{
@@ -137,6 +138,8 @@ public class CoursePlanner
LocalCourse = await LocalCourse.SyncAssignmentsWithCanvas(canvasId, CanvasAssignments, canvas);
CanvasAssignments = await canvas.Assignments.GetAll(canvasId);
await syncModuleItemsWithCanvas(canvasId);
CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules);

View File

@@ -1,75 +1,16 @@
using System.Text.RegularExpressions;
using CanvasModel.Assignments;
using CanvasModel.Modules;
using CanvasModel.Quizzes;
using LocalModels;
using Management.Services.Canvas;
namespace Management.Planner;
public static partial class CoursePlannerSyncronizationExtensions
public static partial class AssignmentSyncronizationExtensions
{
internal static async Task<IEnumerable<LocalModule>> EnsureAllModulesExistInCanvas(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasModule> canvasModules,
CanvasService canvas
)
{
var moduleTasks = localCourse.Modules.Select(async module =>
{
var canvasModule = canvasModules.FirstOrDefault(cm => cm.Id == module.CanvasId);
if (canvasModule == null)
{
var newModule = await canvas.CreateModule(canvasId, module.Name);
return module with { CanvasId = newModule.Id };
}
else
return module;
});
var newModules = await Task.WhenAll(moduleTasks);
return newModules ?? throw new Exception("Error ensuring all modules exist in canvas");
}
internal static async Task SortCanvasModules(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasModule> canvasModules,
CanvasService canvas
)
{
var currentCanvasPositions = canvasModules.ToDictionary(m => m.Id, m => m.Position);
foreach (var (localModule, i) in localCourse.Modules.Select((m, i) => (m, i)))
{
var correctPosition = i + 1;
var moduleCanvasId =
localModule.CanvasId ?? throw new Exception("cannot sort module if no module canvas id");
var currentCanvasPosition = currentCanvasPositions[moduleCanvasId];
if (currentCanvasPosition != correctPosition)
{
await canvas.UpdateModule(canvasId, moduleCanvasId, localModule.Name, correctPosition);
}
}
}
internal static async Task<LocalCourse> SyncModulesWithCanvasData(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasModule> canvasModules,
CanvasService canvas
)
{
canvasModules = await canvas.GetModules(canvasId);
return localCourse with
{
Modules = localCourse.Modules.Select(m =>
{
var canvasModule = canvasModules.FirstOrDefault(cm => cm.Name == m.Name);
return canvasModule == null ? m : m with { CanvasId = canvasModule.Id };
})
};
}
internal static async Task<LocalAssignment> SyncToCanvas(
internal static async Task<LocalAssignment> SyncAssignmentToCanvas(
this LocalCourse localCourse,
ulong canvasId,
LocalAssignment localAssignment,
@@ -114,23 +55,29 @@ public static partial class CoursePlannerSyncronizationExtensions
var localHtmlDescription = localAssignment
.GetDescriptionHtml(courseAssignmentTemplates)
.Replace("<hr />", "<hr>")
.Replace("&gt;", "")
.Replace("&lt;", "")
.Replace(">", "")
.Replace("<", "");
.Replace("<", "")
.Replace("&quot;", "")
.Replace("\"", "");
var canvasHtmlDescription = canvasAssignment.Description;
canvasHtmlDescription = CanvasScriptTagRegex().Replace(canvasHtmlDescription, "");
canvasHtmlDescription = CanvasLinkTagRegex().Replace(canvasHtmlDescription, "");
canvasHtmlDescription = canvasHtmlDescription
.Replace("<hr />", "<hr>")
.Replace("&gt;", "")
.Replace("&lt;", "")
.Replace(">", "")
.Replace("<", "");
.Replace("<", "")
.Replace("&quot;", "")
.Replace("\"", "");
var dueDatesSame =
var canvasComparisonDueDate =
canvasAssignment.DueAt != null
&& new DateTime(
? new DateTime(
year: canvasAssignment.DueAt.Value.Year,
month: canvasAssignment.DueAt.Value.Month,
day: canvasAssignment.DueAt.Value.Day,
@@ -138,14 +85,21 @@ public static partial class CoursePlannerSyncronizationExtensions
minute: canvasAssignment.DueAt.Value.Minute,
second: canvasAssignment.DueAt.Value.Second
)
== new DateTime(
: new DateTime();
var localComparisonDueDate =
canvasAssignment.DueAt != null
? new DateTime(
year: localAssignment.DueAt.Year,
month: localAssignment.DueAt.Month,
day: localAssignment.DueAt.Day,
hour: localAssignment.DueAt.Hour,
minute: localAssignment.DueAt.Minute,
second: localAssignment.DueAt.Second
);
)
: new DateTime();
var dueDatesSame =
canvasAssignment.DueAt != null && canvasComparisonDueDate == localComparisonDueDate;
var descriptionSame = canvasHtmlDescription == localHtmlDescription;
var nameSame = canvasAssignment.Name == localAssignment.Name;
@@ -159,6 +113,9 @@ public static partial class CoursePlannerSyncronizationExtensions
{
if (!dueDatesSame)
{
Console.WriteLine(JsonSerializer.Serialize(canvasAssignment));
Console.WriteLine(canvasComparisonDueDate);
Console.WriteLine(localComparisonDueDate);
Console.WriteLine(
$"Due dates different for {localAssignment.Name}, local: {localAssignment.DueAt}, in canvas {canvasAssignment.DueAt}"
);
@@ -219,7 +176,7 @@ public static partial class CoursePlannerSyncronizationExtensions
var moduleTasks = localCourse.Modules.Select(async m =>
{
var assignmentTasks = m.Assignments.Select(
(a) => localCourse.SyncToCanvas(canvasId, a, canvasAssignments, canvas)
(a) => localCourse.SyncAssignmentToCanvas(canvasId, a, canvasAssignments, canvas)
);
var assignments = await Task.WhenAll(assignmentTasks);
return m with { Assignments = assignments };

View File

@@ -0,0 +1,72 @@
using System.Text.RegularExpressions;
using CanvasModel.Assignments;
using CanvasModel.Modules;
using CanvasModel.Quizzes;
using LocalModels;
using Management.Services.Canvas;
namespace Management.Planner;
public static partial class ModuleSyncronizationExtensions
{
internal static async Task<IEnumerable<LocalModule>> EnsureAllModulesExistInCanvas(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasModule> canvasModules,
CanvasService canvas
)
{
var moduleTasks = localCourse.Modules.Select(async module =>
{
var canvasModule = canvasModules.FirstOrDefault(cm => cm.Id == module.CanvasId);
if (canvasModule == null)
{
var newModule = await canvas.CreateModule(canvasId, module.Name);
return module with { CanvasId = newModule.Id };
}
else
return module;
});
var newModules = await Task.WhenAll(moduleTasks);
return newModules ?? throw new Exception("Error ensuring all modules exist in canvas");
}
internal static async Task SortCanvasModules(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasModule> canvasModules,
CanvasService canvas
)
{
var currentCanvasPositions = canvasModules.ToDictionary(m => m.Id, m => m.Position);
foreach (var (localModule, i) in localCourse.Modules.Select((m, i) => (m, i)))
{
var correctPosition = i + 1;
var moduleCanvasId =
localModule.CanvasId ?? throw new Exception("cannot sort module if no module canvas id");
var currentCanvasPosition = currentCanvasPositions[moduleCanvasId];
if (currentCanvasPosition != correctPosition)
{
await canvas.UpdateModule(canvasId, moduleCanvasId, localModule.Name, correctPosition);
}
}
}
internal static async Task<LocalCourse> SyncModulesWithCanvasData(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasModule> canvasModules,
CanvasService canvas
)
{
canvasModules = await canvas.GetModules(canvasId);
return localCourse with
{
Modules = localCourse.Modules.Select(m =>
{
var canvasModule = canvasModules.FirstOrDefault(cm => cm.Name == m.Name);
return canvasModule == null ? m : m with { CanvasId = canvasModule.Id };
})
};
}
}

View File

@@ -0,0 +1,43 @@
using System.Text.RegularExpressions;
using CanvasModel.Assignments;
using CanvasModel.Modules;
using CanvasModel.Quizzes;
using LocalModels;
using Management.Services.Canvas;
namespace Management.Planner;
public static partial class QuizSyncronizationExtensions
{
internal static async Task<LocalQuiz> SyncQuizToCanvas(
this LocalCourse localCourse,
ulong canvasId,
LocalQuiz localQuiz,
IEnumerable<CanvasQuiz> canvasQuizzes,
CanvasService canvas
)
{
// TODO actually sync
return localQuiz;
}
internal static async Task<LocalCourse> SyncQuizzesWithCanvas(
this LocalCourse localCourse,
ulong canvasId,
IEnumerable<CanvasQuiz> canvasQuizzes,
CanvasService canvas
)
{
var moduleTasks = localCourse.Modules.Select(async m =>
{
var quizTasks = m.Quizzes.Select(
(q) => localCourse.SyncQuizToCanvas(canvasId, q, canvasQuizzes, canvas)
);
var quizzes = await Task.WhenAll(quizTasks);
return m with { Quizzes = quizzes };
});
var modules = await Task.WhenAll(moduleTasks);
return localCourse;
}
}

View File

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

View File

@@ -0,0 +1,27 @@
namespace CanvasModel.Quizzes;
public class CanvasQuizPermissions
{
[JsonPropertyName("read")]
public bool Read { get; set; }
[JsonPropertyName("submit")]
public bool Submit { get; set; }
[JsonPropertyName("create")]
public bool Create { get; set; }
[JsonPropertyName("manage")]
public bool Manage { get; set; }
[JsonPropertyName("read_statistics")]
public bool ReadStatistics { get; set; }
[JsonPropertyName("review_grades")]
public bool ReviewGrades { get; set; }
[JsonPropertyName("update")]
public bool Update { get; set; }
}

View File

@@ -3,6 +3,7 @@ namespace LocalModels;
public record LocalModule
{
public string Name { get; init; } = string.Empty;
public string Id { get; init; } = string.Empty;
public IEnumerable<LocalAssignment> Assignments { get; init; } =
Enumerable.Empty<LocalAssignment>();

View File

@@ -19,6 +19,7 @@ public class CanvasAssignmentService
{
var url = $"courses/{courseId}/assignments";
var request = new RestRequest(url);
request.AddParameter("include[]", "overrides");
var assignmentResponse = await utils.PaginatedRequest<IEnumerable<CanvasAssignment>>(request);
return assignmentResponse.SelectMany(
assignments =>
@@ -77,6 +78,8 @@ public class CanvasAssignmentService
request.AddHeader("Content-Type", "application/json");
var bodyObj = new { assignment = body };
request.AddBody(bodyObj);
Console.WriteLine(url);
Console.WriteLine(JsonSerializer.Serialize(bodyObj));
await webRequestor.PutAsync(request);
await CreateRubric(courseId, localAssignment);

View File

@@ -13,6 +13,7 @@ public class WebRequestor : IWebRequestor
?? throw new Exception("CANVAS_TOKEN not in environment");
client = new RestClient(BaseUrl);
client.AddDefaultHeader("Authorization", $"Bearer {token}");
}
public async Task<(T[]?, RestResponse)> GetManyAsync<T>(RestRequest request)
@@ -48,6 +49,7 @@ public class WebRequestor : IWebRequestor
public async Task<RestResponse> PutAsync(RestRequest request)
{
request.AddHeader("Content-Type", "application/json");
var response = await client.ExecutePutAsync(request);
// if (!response.IsSuccessful)
// {
@@ -61,6 +63,7 @@ public class WebRequestor : IWebRequestor
public async Task<(T?, RestResponse)> PutAsync<T>(RestRequest request)
{
request.AddHeader("Content-Type", "application/json");
var response = await client.ExecutePutAsync(request);
return (deserialize<T>(response), response);
}

View File

@@ -108,3 +108,20 @@ Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
"unlock_at":null
}
}
###
PUT https://snow.instructure.com/api/v1/courses/872095/assignments/12676639
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
Content-Type: application/json
{
"assignment": {
"due_at": "2021-09-15T23:59:59Z",
"lock_at": "2023-08-22T05:59:00Z"
}
}
###
GET https://snow.instructure.com/api/v1/courses/872095/assignments/12676639?include[]=overrides
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}

3
requests/quiz.http Normal file
View File

@@ -0,0 +1,3 @@
GET https://snow.instructure.com/api/v1/courses/705168
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}