i liked syncing more before i cared about order

This commit is contained in:
2023-08-01 22:41:10 -06:00
parent 19bbfea099
commit 56c8128ace
16 changed files with 421 additions and 101 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ bin/
*.env
storage/
tmp.json
tmp*.json

View File

@@ -95,6 +95,13 @@
>
Sync With Canvas
</button>
<a
class="btn btn-outline-secondary"
target="_blank"
href="@($"{Environment.GetEnvironmentVariable("CANVAS_URL")}/courses/{planner.LocalCourse.CanvasId}")"
>
View In Canvas
</a>
@if(planner.LoadingCanvasData)
{

View File

@@ -6,6 +6,7 @@ global using CanvasModel.EnrollmentTerms;
global using CanvasModel.Courses;
global using CanvasModel;
global using LocalModels;
global using Management.Planner;
global using Management.Web.Shared.Components;
global using Management.Web.Shared.Course;
@@ -17,6 +18,16 @@ DotEnv.Load();
var builder = WebApplication.CreateBuilder(args);
var canvas_token = Environment.GetEnvironmentVariable("CANVAS_TOKEN");
if (canvas_token == null)
throw new Exception("CANVAS_TOKEN is null");
var canvas_url = Environment.GetEnvironmentVariable("CANVAS_URL");
if (canvas_url == null)
{
Console.WriteLine("CANVAS_URL is null, defaulting to https://snow.instructure.com");
Environment.SetEnvironmentVariable("CANVAS_URL", "https://snow.instructure.com");
}
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

View File

@@ -16,7 +16,6 @@
}
<div class="text-center">
@if (localCourses != null)
{
<h3 >Stored Courses</h3>

View File

@@ -31,7 +31,7 @@
private string name { get; set; } = String.Empty;
private bool lockAtDueDate { get; set; }
private IEnumerable<RubricItem> rubric { get; set; } = Enumerable.Empty<RubricItem>();
private IEnumerable<SubmissionType> submissionTypes { get; set; } = Enumerable.Empty<SubmissionType>();
private IEnumerable<string> submissionTypes { get; set; } = Enumerable.Empty<string>();
protected override void OnParametersSet()
{

View File

@@ -1,11 +1,13 @@
@using System.Reflection
@code
{
[Parameter, EditorRequired]
public IEnumerable<SubmissionType> Types { get; set; } = Enumerable.Empty<SubmissionType>();
public IEnumerable<string> Types { get; set; } = Enumerable.Empty<string>();
[Parameter, EditorRequired]
public Action<IEnumerable<SubmissionType>> SetTypes { get; set; } = (_) => {};
private string getLabel(SubmissionType type)
public Action<IEnumerable<string>> SetTypes { get; set; } = (_) => {};
private string getLabel(string type)
{
return type.ToString().Replace("_", "") + "switch";
}
@@ -14,7 +16,7 @@
<h5>Submission Types</h5>
<div class="row">
@foreach (var submissionType in (SubmissionType[])Enum.GetValues(typeof(SubmissionType)))
@foreach (var submissionType in SubmissionType.AllTypes)
{
<div class="col-4">
<div class="form-check form-switch">

View File

@@ -28,7 +28,7 @@
lock_at = null,
due_at = DateTime.Now,
points_possible = 10,
submission_types = new SubmissionType[] { SubmissionType.online_text_entry }
submission_types = new string[] { SubmissionType.online_text_entry }
};
if(planner.LocalCourse != null)

View File

@@ -6,6 +6,18 @@
[Parameter]
[EditorRequired]
public LocalAssignment Assignment { get; set; } = new();
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private void dropOnDate(DateTime dropDate)
{

View File

@@ -7,6 +7,8 @@ using CanvasModel.Modules;
using Management.Services.Canvas;
using System.Text.RegularExpressions;
namespace Management.Planner;
public class CoursePlanner
{
private readonly YamlManager yamlManager;
@@ -32,7 +34,8 @@ public class CoursePlanner
return;
}
var verifiedCourse = cleanupCourse(value);
var verifiedCourse = value.GeneralCourseCleanup();
// ignore initial load of course
if (_localCourse != null)
{
@@ -44,53 +47,34 @@ public class CoursePlanner
}
public event Action? StateHasChanged;
private LocalCourse cleanupCourse(LocalCourse incomingCourse)
{
var modulesWithUniqueAssignments = incomingCourse.Modules.Select(
module => module with { Assignments = module.Assignments.DistinctBy(a => a.id) }
);
public IEnumerable<CanvasAssignment>? CanvasAssignments { get; internal set; }
public IEnumerable<CanvasModule>? CanvasModules { get; internal set; }
public Dictionary<ulong, IEnumerable<CanvasModuleItem>>? CanvasModulesItems { get; internal set; }
return incomingCourse with
{
Modules = modulesWithUniqueAssignments
};
}
private IEnumerable<CanvasAssignment>? canvasAssignments = null;
public IEnumerable<CanvasAssignment>? CanvasAssignments
{
get => canvasAssignments;
set
{
canvasAssignments = value;
StateHasChanged?.Invoke();
}
}
private IEnumerable<CanvasModule>? canvasModules = null;
public IEnumerable<CanvasModule>? CanvasModules
{
get => canvasModules;
set
{
canvasModules = value;
StateHasChanged?.Invoke();
}
}
public async Task LoadCanvasData()
public async Task<(
IEnumerable<CanvasAssignment> CanvasAssignments,
IEnumerable<CanvasModule> CanvasModules,
Dictionary<ulong, IEnumerable<CanvasModuleItem>> CanvasModulesItems
)> LoadCanvasData()
{
LoadingCanvasData = true;
StateHasChanged?.Invoke();
Thread.Sleep(1000);
var canvasId =
LocalCourse?.CanvasId ?? throw new Exception("no canvas id found for selected course");
CanvasAssignments = await canvas.Assignments.GetAll(canvasId);
CanvasModules = await canvas.GetModules(canvasId);
var assignmentsTask = canvas.Assignments.GetAll(canvasId);
var modulesTask = canvas.GetModules(canvasId);
CanvasAssignments = await assignmentsTask;
CanvasModules = await modulesTask;
CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules);
// Console.WriteLine(JsonSerializer.Serialize(CanvasModulesItems));
LoadingCanvasData = false;
StateHasChanged?.Invoke();
return (CanvasAssignments, CanvasModules, CanvasModulesItems);
}
public async Task SyncWithCanvas()
@@ -107,22 +91,31 @@ public class CoursePlanner
LoadingCanvasData = true;
StateHasChanged?.Invoke();
var (canvasAssignments, canvasModules, canvasModuleItems) = await LoadCanvasData();
LocalCourse = LocalCourse.deleteCanvasIdsThatNoLongerExist(canvasModules, canvasAssignments);
var canvasId =
LocalCourse.CanvasId ?? throw new Exception("no course canvas id to sync with canvas");
await ensureAllModulesCreated(canvasId);
await reloadModules_UpdateLocalModulesWithNewId(canvasId);
await ensureAllModulesExistInCanvas(canvasId);
CanvasModules = await canvas.GetModules(canvasId);
await sortCanvasModules(canvasId);
CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules);
await syncModulesWithCanvasData(canvasId);
await syncAssignmentsWithCanvas(canvasId);
CanvasAssignments = await canvas.Assignments.GetAll(canvasId);
CanvasModules = await canvas.GetModules(canvasId);
await syncModuleItemsWithCanvas(canvasId);
CanvasModulesItems = await canvas.GetAllModulesItems(canvasId, CanvasModules);
LoadingCanvasData = false;
StateHasChanged?.Invoke();
Console.WriteLine("done syncing with canvas\n");
}
private async Task reloadModules_UpdateLocalModulesWithNewId(ulong canvasId)
private async Task syncModulesWithCanvasData(ulong canvasId)
{
if (LocalCourse == null)
return;
@@ -138,7 +131,7 @@ public class CoursePlanner
};
}
private async Task ensureAllModulesCreated(ulong canvasId)
private async Task ensureAllModulesExistInCanvas(ulong canvasId)
{
if (LocalCourse == null || CanvasModules == null)
return;
@@ -153,6 +146,27 @@ public class CoursePlanner
}
}
private async Task sortCanvasModules(ulong canvasId)
{
if (LocalCourse == null)
throw new Exception("cannot sort modules, no course selected");
if (CanvasModules == null)
throw new Exception("cannot sort modules, no canvas modules loaded");
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);
}
}
}
private async Task syncAssignmentsWithCanvas(ulong canvasId)
{
if (
@@ -205,11 +219,7 @@ public class CoursePlanner
}
else
{
return await canvas.Assignments.Create(
canvasId,
localAssignment,
localHtmlDescription
);
return await canvas.Assignments.Create(canvasId, localAssignment, localHtmlDescription);
}
}
@@ -284,6 +294,94 @@ public class CoursePlanner
|| !submissionTypesSame;
}
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.due_at)
.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()
{
LocalCourse = null;

View File

@@ -0,0 +1,83 @@
using CanvasModel.Assignments;
using CanvasModel.Modules;
using LocalModels;
namespace Management.Planner;
public static class CoursePlannerExtensions
{
public static LocalCourse GeneralCourseCleanup(this LocalCourse incomingCourse)
{
var modulesWithUniqueAssignments = incomingCourse.Modules.Select(
module =>
module with
{
Assignments = module.Assignments.OrderBy(a => a.due_at).DistinctBy(a => a.id)
}
);
return incomingCourse with
{
Modules = modulesWithUniqueAssignments
};
}
public static LocalCourse deleteCanvasIdsThatNoLongerExist(
this LocalCourse localCourse,
IEnumerable<CanvasModule> canvasModules,
IEnumerable<CanvasAssignment> canvasAssignments
)
{
Console.WriteLine("checking canvas ids still exist");
var correctedModules = localCourse.Modules
.Select((m) => m.validateCanvasIds(canvasModules, canvasAssignments))
.ToArray();
return localCourse with
{
Modules = correctedModules
};
}
private static LocalModule validateCanvasIds(
this LocalModule module,
IEnumerable<CanvasModule> canvasModules,
IEnumerable<CanvasAssignment> canvasAssignments
)
{
var moduleIdInCanvas = canvasModules.FirstOrDefault(m => m.Id == module.CanvasId) != null;
var moduleWithAssignments = module with
{
Assignments = module.Assignments
.Select((a) => a.validateAssignmentForCanvasId(canvasAssignments))
.ToArray()
};
if (!moduleIdInCanvas)
{
Console.WriteLine(
$"no id in canvas for module, removing old canvas id: {moduleWithAssignments.Name}"
);
return moduleWithAssignments with { CanvasId = null };
}
return moduleWithAssignments;
}
private static LocalAssignment validateAssignmentForCanvasId(
this LocalAssignment assignment,
IEnumerable<CanvasAssignment> canvasAssignments
)
{
var assignmentIdInCanvas =
canvasAssignments.FirstOrDefault(ca => ca.Id == assignment.canvasId) != null;
if (!assignmentIdInCanvas)
{
Console.WriteLine(
$"no id in canvas for assignment, removing old canvas id: {assignment.name}"
);
return assignment with { canvasId = null };
}
return assignment;
}
}

View File

@@ -3,7 +3,7 @@ namespace CanvasModel.Modules;
public record CanvasModuleItem(
[property: JsonPropertyName("id")] ulong Id,
[property: JsonPropertyName("module_id")] ulong ModuleId,
[property: JsonPropertyName("position")] uint Position,
[property: JsonPropertyName("position")] int Position,
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("indent")] uint? Indent,
[property: JsonPropertyName("type")] string Type,

View File

@@ -8,24 +8,37 @@ public record RubricItem
public int Points { get; set; } = 0;
}
public enum SubmissionType
public static class SubmissionType
{
online_text_entry,
online_upload,
online_quiz,
on_paper,
discussion_topic,
external_tool,
online_url,
media_recording,
student_annotation,
none,
public static readonly string online_text_entry = "online_text_entry";
public static readonly string online_upload = "online_upload";
public static readonly string online_quiz = "online_quiz";
public static readonly string on_paper = "on_paper";
public static readonly string discussion_topic = "discussion_topic";
public static readonly string external_tool = "external_tool";
public static readonly string online_url = "online_url";
public static readonly string media_recording = "media_recording";
public static readonly string student_annotation = "student_annotation";
public static readonly string none = "none";
public static readonly IEnumerable<string> AllTypes = new string[]
{
SubmissionType.online_text_entry,
SubmissionType.online_upload,
SubmissionType.online_quiz,
SubmissionType.on_paper,
SubmissionType.discussion_topic,
SubmissionType.external_tool,
SubmissionType.online_url,
SubmissionType.media_recording,
SubmissionType.student_annotation,
SubmissionType.none,
};
}
public record LocalAssignment
{
public string id { get; init; } = "";
public ulong? canvasId = null;
public ulong? canvasId { get; init; } = null;
public string name { get; init; } = "";
public string description { get; init; } = "";
public bool use_template { get; init; } = false;
@@ -37,7 +50,7 @@ public record LocalAssignment
public DateTime? lock_at { get; init; }
public DateTime due_at { get; init; }
public int points_possible { get; init; }
public IEnumerable<SubmissionType> submission_types { get; init; } = new SubmissionType[] { };
public IEnumerable<string> submission_types { get; init; } = new string[] { };
public string GetRubricHtml()
{

View File

@@ -52,12 +52,14 @@ public class CanvasAssignmentService
if (canvasAssignment == null)
throw new Exception("created canvas assignment was null");
await CreateRubric(courseId, localAssignment);
return localAssignment with
var updatedLocalAssignment = localAssignment with
{
canvasId = canvasAssignment.Id
};
await CreateRubric(courseId, updatedLocalAssignment);
return updatedLocalAssignment;
}
public async Task Update(ulong courseId, LocalAssignment localAssignment, string htmlDescription)

View File

@@ -4,6 +4,7 @@ using CanvasModel.Courses;
using CanvasModel.EnrollmentTerms;
using CanvasModel.Modules;
using RestSharp;
namespace Management.Services.Canvas;
public class CanvasService
@@ -75,6 +76,53 @@ public class CanvasService
await webRequestor.PostAsync(request);
}
public async Task UpdateModule(ulong courseId, ulong moduleId, string name, int position)
{
Console.WriteLine($"Updating Module: {name}");
var url = $"courses/{courseId}/modules/{moduleId}";
var body = new { module = new { name = name, position = position } };
var request = new RestRequest(url);
request.AddBody(body);
await webRequestor.PutAsync(request);
}
public async Task<IEnumerable<CanvasModuleItem>> GetModuleItems(ulong courseId, ulong moduleId)
{
var url = $"courses/{courseId}/modules/{moduleId}/items";
var request = new RestRequest(url);
var (items, response) = await webRequestor.GetAsync<IEnumerable<CanvasModuleItem>>(request);
if (items == null)
throw new Exception($"Error getting canvas module items for {url}");
return items;
}
public async Task<Dictionary<ulong, IEnumerable<CanvasModuleItem>>> GetAllModulesItems(
ulong courseId,
IEnumerable<CanvasModule> modules
)
{
var itemsTasks = modules.Select(
async (m) =>
{
var items = await GetModuleItems(courseId, m.Id);
return (m, items);
}
);
var output = new Dictionary<ulong, IEnumerable<CanvasModuleItem>>();
var itemTasksResult = await Task.WhenAll(itemsTasks);
foreach (var (module, items) in itemTasksResult)
{
if (module == null || items == null)
throw new Exception(
"i'm not sure how we got here, but module and items are null after looking up module items"
);
output[module.Id] = items;
}
return output;
}
public async Task<IEnumerable<EnrollmentTermModel>> GetCurrentTermsFor(
DateTime? _queryDate = null
)
@@ -90,4 +138,50 @@ public class CanvasService
return currentTerms;
}
public async Task UpdateModuleItem(
ulong canvasCourseId,
ulong canvasModuleId,
CanvasModuleItem item
)
{
Console.WriteLine($"updating module item {item.Title}");
var url = $"courses/{canvasCourseId}/modules/{canvasModuleId}/items/{item.Id}";
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<CanvasModuleItem>(request);
if (newItem == null)
throw new Exception("something went wrong updating module item");
}
public async Task CreateModuleItem(
ulong canvasCourseId,
ulong canvasModuleId,
string title,
string type,
ulong contentId
)
{
Console.WriteLine($"creating new module item {title}");
var url = $"courses/{canvasCourseId}/modules/{canvasModuleId}/items";
var body = new
{
module_item = new
{
title = title,
type = type.ToString(),
content_id = contentId,
}
};
var request = new RestRequest(url);
request.AddBody(body);
request.AddHeader("Content-Type", "application/json");
var (newItem, response) = await webRequestor.PostAsync<CanvasModuleItem>(request);
if (newItem == null)
throw new Exception("something went wrong updating module item");
}
}

View File

@@ -2,7 +2,7 @@ using RestSharp;
public class WebRequestor : IWebRequestor
{
private const string BaseUrl = "https://snow.instructure.com/api/v1/";
private string BaseUrl = Environment.GetEnvironmentVariable("CANVAS_URL") + "/api/v1/";
private string token;
private RestClient client;
@@ -49,13 +49,13 @@ public class WebRequestor : IWebRequestor
public async Task<RestResponse> PutAsync(RestRequest request)
{
var response = await client.ExecutePutAsync(request);
if (!response.IsSuccessful)
{
System.Console.WriteLine(response.Content);
System.Console.WriteLine(response.ResponseUri);
System.Console.WriteLine("error with response");
throw new Exception("error with response");
}
// if (!response.IsSuccessful)
// {
// System.Console.WriteLine(response.Content);
// System.Console.WriteLine(response.ResponseUri);
// System.Console.WriteLine("error with response");
// throw new Exception("error with response");
// }
return response;
}
@@ -77,8 +77,6 @@ public class WebRequestor : IWebRequestor
Console.WriteLine(JsonSerializer.Serialize(response.Request?.Parameters));
throw new Exception($"error with response to {response.ResponseUri} {response.StatusCode}");
}
try
{
try
{
var data = JsonSerializer.Deserialize<T>(response.Content!);
@@ -96,7 +94,6 @@ public class WebRequestor : IWebRequestor
Console.WriteLine(response.Content);
throw exception;
}
}
catch (JsonException ex)
{
System.Console.WriteLine(response.ResponseUri);

View File

@@ -9,7 +9,6 @@ public class YamlManager
var serializer = new SerializerBuilder().Build();
var yaml = serializer.Serialize(course);
// System.Console.WriteLine(yaml);
return yaml;
}
@@ -17,8 +16,8 @@ public class YamlManager
{
var deserializer = new DeserializerBuilder().Build();
var person = deserializer.Deserialize<LocalCourse>(rawCourse);
return person;
var course = deserializer.Deserialize<LocalCourse>(rawCourse);
return course;
}
public async Task SaveCourseAsync(LocalCourse course)
@@ -44,7 +43,9 @@ public class YamlManager
var fileNames = Directory.GetFiles(path);
var courses = await Task.WhenAll(
fileNames.Select(async n => ParseCourse(await File.ReadAllTextAsync($"../storage/{n}")))
fileNames
.Where(name => name.EndsWith(".yml"))
.Select(async n => ParseCourse(await File.ReadAllTextAsync($"../storage/{n}")))
);
return courses;
}