This commit is contained in:
2024-02-07 16:21:04 -07:00
parent 292c06ecc9
commit 0881acd1f8
77 changed files with 239 additions and 204 deletions

View File

@@ -7,8 +7,8 @@ public class AssignmentMarkdownTests
{
var assignment = new LocalAssignment()
{
Name="test assignment",
Description ="here is the description",
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
@@ -29,13 +29,13 @@ public class AssignmentMarkdownTests
{
var assignment = new LocalAssignment()
{
Name="test assignment",
Description ="here is the description",
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {}
Rubric = new List<RubricItem>() { }
};
var assignmentMarkdown = assignment.ToMarkdown();
@@ -48,8 +48,8 @@ public class AssignmentMarkdownTests
{
var assignment = new LocalAssignment()
{
Name="test assignment",
Description ="here is the description",
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [],
@@ -71,8 +71,8 @@ public class AssignmentMarkdownTests
{
var assignment = new LocalAssignment()
{
Name="test assignment",
Description ="here is the description",
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = null,
SubmissionTypes = [],
@@ -94,7 +94,7 @@ public class AssignmentMarkdownTests
{
var assignment = new LocalAssignment()
{
Name="test assignment",
Name = "test assignment",
Description = "",
DueAt = new DateTime(),
LockAt = new DateTime(),
@@ -116,13 +116,14 @@ public class AssignmentMarkdownTests
{
var assignment = new LocalAssignment()
{
Name="test assignment",
Name = "test assignment",
Description = "test assignment\n---\nsomestuff",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {
Rubric = new List<RubricItem>()
{
}
};

View File

@@ -72,8 +72,10 @@ public class FileStorageTests
[Test]
public async Task CourseSettings_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new() {
Settings = new() {
LocalCourse testCourse = new()
{
Settings = new()
{
AssignmentGroups = [],
Name = "Test Course with settings",
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday],
@@ -96,13 +98,15 @@ public class FileStorageTests
[Test]
public async Task EmptyCourseModules_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new() {
LocalCourse testCourse = new()
{
Settings = new() { Name = "Test Course with modules" },
Modules = [
new() {
Name="test module 1",
Assignments= [],
Quizzes=[]
new()
{
Name = "test module 1",
Assignments = [],
Quizzes = []
}
]
};
@@ -118,26 +122,29 @@ public class FileStorageTests
[Test]
public async Task CourseModules_WithAssignments_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new() {
LocalCourse testCourse = new()
{
Settings = new() { Name = "Test Course with modules and assignments" },
Modules = [
new() {
Name="test module 1 with assignments",
Assignments=[
new () {
Name="test assignment",
Description ="here is the description",
new()
{
Name = "test module 1 with assignments",
Assignments = [
new()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = [
new() {Points = 4, Label="do task 1"},
new() {Points = 2, Label="do task 2"},
new() { Points = 4, Label = "do task 1" },
new() { Points = 2, Label = "do task 2" },
]
}
],
Quizzes=[]
Quizzes = []
}
]
};
@@ -156,14 +163,17 @@ public class FileStorageTests
[Test]
public async Task CourseModules_WithQuizzes_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new() {
LocalCourse testCourse = new()
{
Settings = new() { Name = "Test Course with modules and quiz" },
Modules = [
new() {
Name="test module 1 with quiz",
Assignments=[],
Quizzes=[
new() {
new()
{
Name = "test module 1 with quiz",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
@@ -171,8 +181,9 @@ public class FileStorageTests
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions=[
new () {
Questions = [
new()
{
Text = "test essay",
QuestionType = QuestionType.ESSAY,
Points = 1
@@ -196,8 +207,10 @@ public class FileStorageTests
[Test]
public async Task MarkdownStorage_FullyPopulated_DoesNotLoseData()
{
LocalCourse testCourse = new() {
Settings = new () {
LocalCourse testCourse = new()
{
Settings = new()
{
AssignmentGroups = [],
Name = "Test Course with lots of data",
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday],
@@ -206,24 +219,27 @@ public class FileStorageTests
DefaultDueTime = new() { Hour = 1, Minute = 59 },
},
Modules = [
new() {
Name= "new test module",
new()
{
Name = "new test module",
Assignments = [
new() {
Name="test assignment",
Description ="here is the description",
new()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = [
new() { Points = 4, Label="do task 1" },
new() { Points = 2, Label="do task 2" },
new() { Points = 4, Label = "do task 1" },
new() { Points = 2, Label = "do task 2" },
]
}
],
Quizzes = [
new() {
new()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(),
@@ -233,7 +249,8 @@ public class FileStorageTests
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = [
new() {
new()
{
Text = "test short answer",
QuestionType = QuestionType.SHORT_ANSWER,
Points = 1
@@ -257,8 +274,10 @@ public class FileStorageTests
[Test]
public async Task MarkdownStorage_CanPersistPages()
{
LocalCourse testCourse = new() {
Settings = new () {
LocalCourse testCourse = new()
{
Settings = new()
{
AssignmentGroups = [],
Name = "Test Course with page",
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday],
@@ -267,10 +286,12 @@ public class FileStorageTests
DefaultDueTime = new() { Hour = 1, Minute = 59 },
},
Modules = [
new(){
new()
{
Name = "page test module",
Pages = [
new () {
new()
{
Name = "test page persistence",
DueAt = new DateTime(),
Text = "this is some\n## markdown\n"

View File

@@ -67,7 +67,7 @@ endline
*) true
) false
";
var question = LocalQuizQuestion.ParseMarkdown(questionMarkdown, 0);
var question = LocalQuizQuestion.ParseMarkdown(questionMarkdown, 0);
question.Answers.Count().Should().Be(2);
}
}

View File

@@ -1,3 +1,3 @@
global using NUnit.Framework;
global using FluentAssertions;
global using System.Text.Json;
global using FluentAssertions;
global using NUnit.Framework;

View File

@@ -1,14 +1,14 @@
public static class ConfigurationSetup
{
public static void Canvas(WebApplicationBuilder builder)
{
var canvas_token = builder.Configuration["CANVAS_TOKEN"] ?? throw new Exception("CANVAS_TOKEN is null");
public static void Canvas(WebApplicationBuilder builder)
{
var canvas_token = builder.Configuration["CANVAS_TOKEN"] ?? throw new Exception("CANVAS_TOKEN is null");
var canvas_url = builder.Configuration["CANVAS_URL"];
if (canvas_url == null)
{
Console.WriteLine("CANVAS_URL is null, defaulting to https://snow.instructure.com");
builder.Configuration["CANVAS_URL"] = "https://snow.instructure.com";
}
var canvas_url = builder.Configuration["CANVAS_URL"];
if (canvas_url == null)
{
Console.WriteLine("CANVAS_URL is null, defaulting to https://snow.instructure.com");
builder.Configuration["CANVAS_URL"] = "https://snow.instructure.com";
}
}
}

View File

@@ -45,7 +45,8 @@ public class DroppableQuiz : ComponentBase
.Select(q =>
q.Name + q.Description != Quiz.Name + Quiz.Description
? q :
q with {
q with
{
DueAt = defaultDueTimeDate,
LockAt = q.LockAt > defaultDueTimeDate ? q.LockAt : defaultDueTimeDate
}

View File

@@ -8,19 +8,19 @@ namespace Management.Web.Pages;
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}

View File

@@ -1,24 +1,23 @@
global using System.Text.Json.Serialization;
global using System.Text.Json;
global using System.ComponentModel.DataAnnotations;
global using Management.Services.Canvas;
global using Management.Services;
global using CanvasModel.EnrollmentTerms;
global using CanvasModel.Courses;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using CanvasModel;
global using CanvasModel.Courses;
global using CanvasModel.EnrollmentTerms;
global using LocalModels;
global using Management.Planner;
global using Management.Web.Shared.Components;
global using Management.Services;
global using Management.Services.Canvas;
global using Management.Web.Shared;
global using Management.Web.Shared.Components;
using dotenv.net;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry;
DotEnv.Load();

View File

@@ -1,14 +1,14 @@
using CanvasModel.EnrollmentTerms;
using CanvasModel.Courses;
using CanvasModel;
using LocalModels;
using CanvasModel.Assignments;
using CanvasModel.Modules;
using Management.Services.Canvas;
using System.Text.RegularExpressions;
using CanvasModel.Quizzes;
using Management.Services;
using CanvasModel;
using CanvasModel.Assignments;
using CanvasModel.Courses;
using CanvasModel.EnrollmentTerms;
using CanvasModel.Modules;
using CanvasModel.Pages;
using CanvasModel.Quizzes;
using LocalModels;
using Management.Services;
using Management.Services.Canvas;
namespace Management.Planner;
@@ -181,13 +181,17 @@ public class CoursePlanner
CanvasAssignmentGroups = await canvas.AssignmentGroups.GetAll(canvasCourseId);
LocalCourse = LocalCourse with {Settings = LocalCourse.Settings with {
AssignmentGroups = LocalCourse.Settings.AssignmentGroups.Select(g => {
var canvasGroup = CanvasAssignmentGroups.FirstOrDefault(c => c.Name == g.Name);
return canvasGroup == null
? g
: g with {CanvasId = canvasGroup.Id};
})
LocalCourse = LocalCourse with
{
Settings = LocalCourse.Settings with
{
AssignmentGroups = LocalCourse.Settings.AssignmentGroups.Select(g =>
{
var canvasGroup = CanvasAssignmentGroups.FirstOrDefault(c => c.Name == g.Name);
return canvasGroup == null
? g
: g with { CanvasId = canvasGroup.Id };
})
}
};
}

View File

@@ -112,7 +112,7 @@ public class PageEditorContext(
var canvasModule = getCurrentCanvasModule(Page, planner.LocalCourse);
if(canvasPage != null)
if (canvasPage != null)
{
await canvas.CreatePageModuleItem(
(ulong)courseCanvasId,

View File

@@ -39,14 +39,15 @@ public static partial class ModuleSyncronizationExtensions
{
var canvasModuleItems = await canvas.Modules.GetModuleItems(canvasId, moduleCanvasId);
var moduleItemsInCorrectOrder = canvasModuleItems
.OrderBy(canvasItem => {
.OrderBy(canvasItem =>
{
if(canvasItem.Type == "Page")
if (canvasItem.Type == "Page")
{
var localPage = localModule.Pages.FirstOrDefault(p => p.Name == canvasItem.Title);
Console.WriteLine(JsonSerializer.Serialize(localModule.Pages));
if(localPage != null)
if (localPage != null)
return localPage.DueAt.Date;
}
return canvasItem.ContentDetails?.DueAt?.Date;

View File

@@ -1,3 +1,3 @@
global using System.Text.Json.Serialization;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using Microsoft.Extensions.Logging;

View File

@@ -15,7 +15,7 @@ public record CanvasModule(
[property: JsonPropertyName("items")]
IEnumerable<CanvasModuleItem>? Items,
[property: JsonPropertyName("state")] string? State, // todo make sure this,
// [OptIn]
// [OptIn]
[property: JsonPropertyName("completed_at")]
DateTime? CompletedAt,
[property: JsonPropertyName("publish_final_grade")] bool? PublishFinalGrade,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Pages;
public record CanvasPage (
public record CanvasPage(
[property: JsonPropertyName("page_id")] ulong PageId,
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("title")] string Title,

View File

@@ -6,7 +6,7 @@ using YamlDotNet.Serialization;
namespace LocalModels;
public record LocalAssignment: IModuleItem
public record LocalAssignment : IModuleItem
{
private string _name = "";
public string Name

View File

@@ -1,6 +1,6 @@
namespace LocalModels;
public record LocalCoursePage: IModuleItem
public record LocalCoursePage : IModuleItem
{
public required string Name { get; init; }
public required string Text { get; set; }

View File

@@ -3,7 +3,7 @@ using YamlDotNet.Serialization;
namespace LocalModels;
public record LocalQuiz: IModuleItem
public record LocalQuiz : IModuleItem
{
public required string Name { get; init; }
@@ -171,7 +171,7 @@ Description: {Description}
public class QuizMarkdownParseException : Exception
{
public QuizMarkdownParseException(string message): base(message)
public QuizMarkdownParseException(string message) : base(message)
{
}

View File

@@ -35,7 +35,7 @@ public record LocalQuizQuestion
return $"{questionTypeIndicator}{multilineMarkdownCompatibleText}";
}
else if(QuestionType == "matching")
else if (QuestionType == "matching")
{
return $"^ {answer.Text} - {answer.MatchedText}";
}

View File

@@ -63,10 +63,12 @@ public class CanvasModuleService
if (items == null)
throw new Exception($"Error getting canvas module items for {url}");
return items.Select(i =>
i with {
i with
{
ContentDetails = i.ContentDetails == null
? null
: i.ContentDetails with {
: i.ContentDetails with
{
DueAt = i.ContentDetails.DueAt?.ToLocalTime(),
LockAt = i.ContentDetails.LockAt?.ToLocalTime(),
}

View File

@@ -117,14 +117,16 @@ public class CanvasQuizService(
await Task.WhenAll(tasks);
}
private async Task hackFixQuestionOrdering(ulong canvasCourseId, ulong canvasQuizId, IEnumerable<(CanvasQuizQuestion question, int position)> questionAndPositions )
private async Task hackFixQuestionOrdering(ulong canvasCourseId, ulong canvasQuizId, IEnumerable<(CanvasQuizQuestion question, int position)> questionAndPositions)
{
using var activity = DiagnosticsConfig.Source.StartActivity("hack fixing question ordering with reorder");
activity?.SetCustomProperty("canvasQuizId", canvasQuizId);
activity?.SetTag("canvas syncronization", true);
var order = questionAndPositions.OrderBy(t => t.position).Select(tuple => {
return new {
var order = questionAndPositions.OrderBy(t => t.position).Select(tuple =>
{
return new
{
type = "question",
id = tuple.question.Id.ToString(),
};
@@ -182,9 +184,10 @@ public class CanvasQuizService(
private static object[] getAnswers(LocalQuizQuestion q)
{
if(q.QuestionType == QuestionType.MATCHING)
if (q.QuestionType == QuestionType.MATCHING)
return q.Answers
.Select(a => new {
.Select(a => new
{
answer_match_left = a.Text,
answer_match_right = a.MatchedText
})

View File

@@ -1,11 +1,11 @@
using Microsoft.Extensions.Logging;
using CanvasModel;
using CanvasModel.Assignments;
using CanvasModel.Courses;
using CanvasModel.EnrollmentTerms;
using CanvasModel.Modules;
using RestSharp;
using CanvasModel.Pages;
using Microsoft.Extensions.Logging;
using RestSharp;
namespace Management.Services.Canvas;

View File

@@ -47,7 +47,7 @@ public class FileStorageManager
public IEnumerable<string> GetEmptyDirectories()
{
if(!Directory.Exists(_basePath))
if (!Directory.Exists(_basePath))
throw new DirectoryNotFoundException($"Cannot get empty directories, {_basePath} does not exist");
return Directory

View File

@@ -25,7 +25,7 @@ public class CourseMarkdownLoader
})
.Select(async d => await LoadCourseByPath(d))
);
return courses.OrderBy(c=>c.Settings.Name);
return courses.OrderBy(c => c.Settings.Name);
}
public async Task<LocalCourse> LoadCourseByPath(string courseDirectory)

View File

@@ -45,12 +45,15 @@ public class WebRequestor : IWebRequestor
var response = await client.ExecutePostAsync(request);
if (isRateLimited(response)) {
if(retryCount < rateLimitRetryCount){
if (isRateLimited(response))
{
if (retryCount < rateLimitRetryCount)
{
logger.LogInformation($"hit rate limit on post, retry count is {retryCount} / {rateLimitRetryCount}, retrying");
Console.WriteLine($"hit rate limit on post, retry count is {retryCount} / {rateLimitRetryCount}, retrying");
Thread.Sleep(rateLimitSleepInterval);
return await rateLimitAwarePostAsync(request, retryCount + 1);}
return await rateLimitAwarePostAsync(request, retryCount + 1);
}
}
if (!response.IsSuccessful)
@@ -63,7 +66,7 @@ public class WebRequestor : IWebRequestor
private static bool isRateLimited(RestResponse response)
{
if(response.Content == null)
if (response.Content == null)
return false;
return response.StatusCode == HttpStatusCode.Forbidden
&& response.Content.Contains("403 Forbidden (Rate Limit Exceeded)");
@@ -110,7 +113,7 @@ public class WebRequestor : IWebRequestor
{
if (e.StatusCode == HttpStatusCode.Forbidden) // && response.Content == "403 Forbidden (Rate Limit Exceeded)"
{
if(retryCount < rateLimitRetryCount)
if (retryCount < rateLimitRetryCount)
{
logger.LogInformation($"hit rate limit in delete, retry count is {retryCount} / {rateLimitRetryCount}, retrying");
Console.WriteLine($"hit rate limit in delete, retry count is {retryCount} / {rateLimitRetryCount}, retrying");