working on canvas sync

This commit is contained in:
2023-07-29 20:33:00 -06:00
parent 7c857a3887
commit 2cca727b4d
26 changed files with 362 additions and 83 deletions

1
.gitignore vendored
View File

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

View File

@@ -70,13 +70,20 @@
{
<div class="mb-3">
<button
@onclick="@(() => planner.LocalCourse = null)"
@onclick="planner.Clear"
class="btn btn-primary"
>
Select New Course
</button>
<CourseSettings />
<AssignmentTemplateManagement />
<button
class="btn btn-outline-primary"
@onclick="planner.SyncWithCanvas"
>
Sync With Canvas
</button>
</div>
<CourseDetails />
}

View File

@@ -21,9 +21,9 @@ builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<IWebRequestor, WebRequestor>();
builder.Services.AddScoped<CanvasService, CanvasService>();
builder.Services.AddSingleton<YamlManager>();
builder.Services.AddSingleton<CoursePlanner>();
builder.Services.AddSingleton<AssignmentDragContainer>();
builder.Services.AddScoped<YamlManager>();
builder.Services.AddScoped<CoursePlanner>();
builder.Services.AddScoped<AssignmentDragContainer>();
var app = builder.Build();

View File

@@ -3,6 +3,7 @@
@using Management.Web.Shared.Module
@using Management.Web.Shared.Semester
@inject CanvasService canvas
@inject CoursePlanner planner
@code
@@ -11,6 +12,22 @@
{
planner.StateHasChanged += reload;
}
protected override async Task OnInitializedAsync()
{
if(
planner.CanvasAssignments == null
&& planner.LocalCourse != null
&& planner.LocalCourse.CanvasId != null
)
{
var canvasId = planner.LocalCourse.CanvasId ?? throw new Exception("no canvas id found for selected course");
planner.CanvasAssignments = await canvas.GetAssignments(canvasId);
planner.CanvasModules = await canvas.GetModules(canvasId);
System.Console.WriteLine(JsonSerializer.Serialize(planner.CanvasAssignments));
}
await base.OnInitializedAsync();
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
@@ -21,8 +38,6 @@
}
}
<div class="row">
<div class="col overflow-y-auto border rounded " style="max-height: 95vh;">
@if (planner.LocalCourse != null)

View File

@@ -48,6 +48,7 @@
minute: planner.LocalCourse.DefaultDueTime.Minute,
second: 0
);
var moduleWithUpdatedAssignment = currentModule with
{
Assignments = currentModule.Assignments.Select(a =>
@@ -114,6 +115,13 @@
dragContainer.DropCallback = null;
}
private bool isSyncedWithCanvas =>
planner
.CanvasAssignments?
.FirstOrDefault(
a => a.Id == Assignment.canvasId
) != null;
}
<div
@@ -130,7 +138,15 @@
<h4 class="text-center">
@Assignment.name
</h4>
</div>
@if(isSyncedWithCanvas)
{
<div>Synced With Canvas</div>
}
else
{
<div>Not Synced with Canvas</div>
}
</div>
<div class="card-text">
<div class="row">
<div class="col">

View File

@@ -44,6 +44,12 @@
}
dragContainer.DropCallback?.Invoke(null, Module);
}
private bool isSyncedWithCanvas => planner
.CanvasModules?
.FirstOrDefault(
cm => cm.Id == Module.CanvasId
) != null;
}
<div
@@ -53,18 +59,29 @@
@ondragleave="OnDragLeave"
ondragover="event.preventDefault();"
>
<h2 class="@("accordion-header ")">
<h2 class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="@("#" + accordionId)"
aria-expanded="true"
aria-controls="@accordionId"
>
@Module.Name
<div class="w-100 d-flex justify-content-between pe-3">
<div>
@Module.Name
</div>
@if(isSyncedWithCanvas)
{
<div>in canvas</div>
}
else
{
<div>not in canvas</div>
}
</div>
</button>
</h2>
<div
id="@accordionId"

View File

@@ -2,14 +2,18 @@ using CanvasModel.EnrollmentTerms;
using CanvasModel.Courses;
using CanvasModel;
using LocalModels;
using CanvasModel.Assignments;
using CanvasModel.Modules;
public class CoursePlanner
{
private readonly YamlManager yamlManager;
private readonly CanvasService canvas;
public CoursePlanner(YamlManager yamlManager)
public CoursePlanner(YamlManager yamlManager, CanvasService canvas)
{
this.yamlManager = yamlManager;
this.canvas = canvas;
}
private LocalCourse? _localCourse { get; set; }
@@ -43,6 +47,100 @@ public class CoursePlanner
module => module with { Assignments = module.Assignments.DistinctBy(a => a.id) }
);
return incomingCourse with { Modules = modulesWithUniqueAssignments };
return incomingCourse with
{
Modules = modulesWithUniqueAssignments
};
}
public IEnumerable<CanvasAssignment>? CanvasAssignments { get; set; } = null;
public IEnumerable<CanvasModule>? CanvasModules { get; set; } = null;
public async Task SyncWithCanvas()
{
if (
LocalCourse == null
|| LocalCourse.CanvasId == null
|| CanvasAssignments == null
|| CanvasModules == null
)
return;
var canvasId =
LocalCourse.CanvasId ?? throw new Exception("no course canvas id to sync with canvas");
await ensureAllModulesCreated(canvasId);
await reloadModules_UpdateLocalModulesWithNewId(canvasId);
await ensureAllAssignmentsCreated_updateIds(canvasId);
}
private async Task reloadModules_UpdateLocalModulesWithNewId(ulong canvasId)
{
if (LocalCourse == null)
return;
CanvasModules = await canvas.GetModules(canvasId);
LocalCourse = 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 };
})
};
}
private async Task ensureAllModulesCreated(ulong canvasId)
{
if (LocalCourse == null || CanvasModules == null)
return;
foreach (var module in LocalCourse.Modules)
{
var canvasModule = CanvasModules.FirstOrDefault(cm => cm.Id == module.CanvasId);
if (canvasModule == null)
{
await canvas.CreateModule(canvasId, module.Name);
}
}
}
private async Task ensureAllAssignmentsCreated_updateIds(ulong canvasId)
{
if (
LocalCourse == null
|| LocalCourse.CanvasId == null
|| CanvasAssignments == null
|| CanvasModules == null
)
return;
var moduleTasks = LocalCourse.Modules.Select(async m =>
{
var assignmentTasks = m.Assignments.Select(ensureAssignmentInCanvas_returnUpdated);
var assignments = await Task.WhenAll(assignmentTasks);
return m with { Assignments = assignments };
});
var modules = await Task.WhenAll(moduleTasks);
LocalCourse = LocalCourse with
{
Modules = modules
};
}
private async Task<LocalAssignment> ensureAssignmentInCanvas_returnUpdated(
LocalAssignment localAssignment
)
{
// var canvasAssignment = await canvas.
return localAssignment;
}
public void Clear()
{
LocalCourse = null;
CanvasAssignments = null;
CanvasModules = null;
}
}

View File

@@ -3,7 +3,7 @@ using CanvasModel.Submissions;
namespace CanvasModel.Assignments;
public record AssignmentModel
public record CanvasAssignment
(
[property: JsonPropertyName("id")]
@@ -88,7 +88,7 @@ public record AssignmentModel
DateTime? UnlockAt = null,
[property: JsonPropertyName("all_dates")]
IEnumerable<AssignmentDateModel>? AllDates = null,
IEnumerable<CanvasAssignmentDate>? AllDates = null,
[property: JsonPropertyName("allowed_extensions")]
IEnumerable<string>? AllowedExtensions = null,
@@ -100,13 +100,13 @@ public record AssignmentModel
bool? VeriCiteEnabled = null,
[property: JsonPropertyName("turnitin_settings")]
TurnitinSettingsModel? TurnitinSettings = null,
CanvasTurnitinSettings? TurnitinSettings = null,
[property: JsonPropertyName("grade_group_students_individually")]
bool? GradeGroupStudentsIndividually = null,
[property: JsonPropertyName("external_tool_tag_attributes")]
ExternalToolTagAttributesModel? ExternalToolTagAttributes = null,
CanvasExternalToolTagAttributes? ExternalToolTagAttributes = null,
[property: JsonPropertyName("peer_review_count")]
uint? PeerReviewCount = null,
@@ -124,7 +124,7 @@ public record AssignmentModel
uint? NeedsGradingCount = null,
[property: JsonPropertyName("needs_grading_count_be_section")]
IEnumerable<NeedsGradingCountModel>? NeedsGradingCountBySection = null,
IEnumerable<CanvasNeedsGradingCount>? NeedsGradingCountBySection = null,
[property: JsonPropertyName("post_to_sis")]
bool? PostToSis = null,
@@ -148,7 +148,7 @@ public record AssignmentModel
ulong? GradingStandardId = null,
[property: JsonPropertyName("lock_info")]
LockInfoModel? LockInfo = null,
CanvasLockInfo? LockInfo = null,
[property: JsonPropertyName("lock_explanation")]
string? LockExplanation = null,
@@ -181,13 +181,13 @@ public record AssignmentModel
object? RubricSettings = null,
[property: JsonPropertyName("rubric")]
IEnumerable<RubricCriteriaModel>? Rubric = null,
IEnumerable<CanvasRubricCriteria>? Rubric = null,
[property: JsonPropertyName("assignment_visibility")]
IEnumerable<ulong>? AssignmentVisibility = null,
[property: JsonPropertyName("overrides")]
IEnumerable<AssignmentOverrideModel>? Overrides = null,
IEnumerable<CanvasAssignmentOverride>? Overrides = null,
[property: JsonPropertyName("omit_from_final_grade")]
bool? OmitFromFinalGrade = null,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record AssignmentDateModel
public record CanvasAssignmentDate
(
[property: JsonPropertyName("title")]
string Title,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record AssignmentOverrideModel
public record CanvasAssignmentOverride
(
[property: JsonPropertyName("id")]

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record ExternalToolTagAttributesModel
public record CanvasExternalToolTagAttributes
(
[property: JsonPropertyName("url")]
string Url,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record LockInfoModel
public record CanvasLockInfo
(
[property: JsonPropertyName("asset_string")]
string AssetString,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record NeedsGradingCountModel
public record CanvasNeedsGradingCount
(
[property: JsonPropertyName("section_id")]
string SectionId,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record RubricCriteriaModel
public record CanvasRubricCriteria
(
[property: JsonPropertyName("id")]
string Id,
@@ -23,7 +23,7 @@ public record RubricCriteriaModel
bool? CriterionUseRange = null,
[property: JsonPropertyName("ratings")]
IEnumerable<RubricRatingModel>? Ratings = null,
IEnumerable<CanvasRubricRating>? Ratings = null,
[property: JsonPropertyName("ignore_for_scoring")]
bool? IgnoreForScoring = null

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record RubricRatingModel
public record CanvasRubricRating
(
[property: JsonPropertyName("points")]
double Points,

View File

@@ -1,6 +1,6 @@
namespace CanvasModel.Assignments;
public record TurnitinSettingsModel
public record CanvasTurnitinSettings
(
[property: JsonPropertyName("originality_report_visibility")]
string OriginalityReportVisibility,

View File

@@ -1,11 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace CanvasModel;
public record CourseModule(
[property: JsonPropertyName("id")] ulong Id,
[property: JsonPropertyName("name")] string Name
// [property: JsonPropertyName("start_at")] DateTime StartAt,
// [property: JsonPropertyName("end_at")] DateTime EndAt,
// [property: JsonPropertyName("description")] string Description
);

View File

@@ -0,0 +1,23 @@
namespace CanvasModel.Modules;
public record CanvasModule(
[property: JsonPropertyName("id")] ulong Id,
[property: JsonPropertyName("workflow_state")] string WorkflowState,
[property: JsonPropertyName("position")] uint Position,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("unlock_at")] DateTime? UnlockAt,
[property: JsonPropertyName("require_sequential_progress")] bool? RequireSequentialProgress,
[property: JsonPropertyName("prerequisite_module_ids")] IEnumerable<ulong>? PrerequisiteModuleIds,
[property: JsonPropertyName("items_count")] uint ItemsCount,
[property: JsonPropertyName("items_url")] string ItemsUrl,
// [OptIn]
// [Enigmatic] // can be null if "the module is deemed too large", even if opted-in
[property: JsonPropertyName("items")]
IEnumerable<CanvasModuleItem>? Items,
[property: JsonPropertyName("state")] string? State, // todo make sure this,
// [OptIn]
[property: JsonPropertyName("completed_at")]
DateTime? CompletedAt,
[property: JsonPropertyName("publish_final_grade")] bool? PublishFinalGrade,
[property: JsonPropertyName("published")] bool? Published
);

View File

@@ -0,0 +1,7 @@
namespace CanvasModel.Modules;
public record CanvasCompletionRequirement(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("min_score")] double? MinScore,
[property: JsonPropertyName("completed")] bool? Completed
);

View File

@@ -0,0 +1,20 @@
namespace CanvasModel.Modules;
public record CanvasModuleItem(
[property: JsonPropertyName("id")] ulong Id,
[property: JsonPropertyName("module_id")] ulong ModuleId,
[property: JsonPropertyName("position")] uint Position,
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("indent")] uint? Indent,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("content_id")] ulong? ContentId,
[property: JsonPropertyName("html_url")] string HtmlUrl,
[property: JsonPropertyName("url")] string? Url,
[property: JsonPropertyName("page_url")] string? PageUrl,
[property: JsonPropertyName("external_url")] string? ExternalUrl,
[property: JsonPropertyName("new_tab")] bool NewTab,
// [OptIn]
[property: JsonPropertyName("completion_requirement")]
CanvasCompletionRequirement? CompletionRequirement,
[property: JsonPropertyName("published")] bool? Published
);

View File

@@ -33,7 +33,7 @@ public record SubmissionModel
string LatePolicyStatus,
[property: JsonPropertyName("assignment")]
AssignmentModel? Assignment = null,
CanvasAssignment? Assignment = null,
[property: JsonPropertyName("course")]
CourseModel? Course = null,

View File

@@ -90,7 +90,7 @@ public record ActivityStreamObjectModel
ulong? AssignmentId = null,
[property: JsonPropertyName("assignment")]
AssignmentModel? Assignment = null,
CanvasAssignment? Assignment = null,
[property: JsonPropertyName("course")]
CourseModel? Course = null,

View File

@@ -5,4 +5,6 @@ public record LocalModule
public string Name { get; init; } = string.Empty;
public IEnumerable<LocalAssignment> Assignments { get; init; } =
Enumerable.Empty<LocalAssignment>();
public ulong? CanvasId { get; set; } = null;
}

View File

@@ -1,6 +1,8 @@
using CanvasModel;
using CanvasModel.Assignments;
using CanvasModel.Courses;
using CanvasModel.EnrollmentTerms;
using CanvasModel.Modules;
using RestSharp;
public interface ICanvasService
@@ -13,11 +15,9 @@ public class CanvasService : ICanvasService
{
private const string BaseUrl = "https://snow.instructure.com/api/v1/";
private readonly IWebRequestor webRequestor;
private string courseid { get; }
public CanvasService(IWebRequestor webRequestor)
{
courseid = "774898";
this.webRequestor = webRequestor;
}
@@ -54,11 +54,26 @@ public class CanvasService : ICanvasService
return data;
}
public async Task<IEnumerable<CourseModule>> GetModules(ulong courseId)
public async Task<IEnumerable<CanvasAssignment>> GetAssignments(ulong courseId)
{
var url = $"courses/{courseId}/assignments ";
var request = new RestRequest(url);
var assignmentResponse = await PaginatedRequest<IEnumerable<CanvasAssignment>>(request);
return assignmentResponse.SelectMany(c => c);
}
// public async Task<CanvasAssignment> CreateAssignment(
// )
// {
// }
public async Task<IEnumerable<CanvasModule>> GetModules(ulong courseId)
{
var url = $"courses/{courseId}/modules";
var request = new RestRequest(url);
var modules = await PaginatedRequest<IEnumerable<CourseModule>>(request);
var modules = await PaginatedRequest<IEnumerable<CanvasModule>>(request);
return modules.SelectMany(c => c).ToArray();
}

104
requests/assignment.http Normal file
View File

@@ -0,0 +1,104 @@
###
get https://snow.instructure.com/api/v1/courses
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
###
GET https://snow.instructure.com/api/v1/courses/705168
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
###
POST https://snow.instructure.com/api/v1/courses/872095/assignments
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
{"assignment":{"name:":"New Assignment","description:":"This is a new assignment"}}
###
POST https://snow.instructure.com/api/v1/courses/705168/assignments
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
{
"assignment": {
"due_at":null,
"points_possible":0,
"assignment_group_id":"1805234",
"post_to_sis":false,
"graders_anonymous_to_graders":false,
"grader_comments_visible_to_graders":true,
"grader_names_visible_to_final_grader":true,
"annotatable_attachment_id":"",
"secure_params":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsdGlfYXNzaWdubWVudF9pZCI6ImIxZWNhNjFjLWJlZWQtNGJjMC1hNTBiLWJkYmNiOTA3MjNiOSIsImx0aV9hc3NpZ25tZW50X2Rlc2NyaXB0aW9uIjpudWxsfQ.jzOL4gSSsNkq7m3S7kl9RbbqsdjK7FzhhepqyHWuTAk",
"lti_context_id":null,
"course_id":"872095",
"name":"test1",
"submission_types":["online_text_entry"],
"has_submitted_submissions":false,
"due_date_required":false,
"max_name_length":255,
"allowed_attempts":"-1",
"in_closed_grading_period":false,
"graded_submissions_exist":false,
"omit_from_final_grade":"0",
"hide_in_gradebook":false,
"turnitin_enabled":"0",
"turnitin_settings": {
"s_paper_check":true,
"originality_report_visibility":"immediate",
"internet_check":true,
"exclude_biblio":true,
"exclude_quoted":true,
"journal_check":true,
"exclude_small_matches_type":null,
"exclude_small_matches_value":0,
"submit_papers_to":true
},
"is_quiz_assignment":false,
"can_duplicate":true,
"original_course_id":null,
"original_assignment_id":null,
"original_lti_resource_link_id":null,
"original_assignment_name":null,
"original_quiz_id":null,
"workflow_state":"unpublished",
"important_dates":false,
"assignment_overrides":[],
"publishable":true,
"hidden":false,
"unpublishable":true,
"grading_type":"points",
"submission_type":"online",
"only_visible_to_overrides":false,
"description":"",
"grading_standard_id":"",
"allowed_extensions":null,
"vericite_enabled":"0",
"external_tool_tag_attributes": {
"external_data":"",
"custom_params":"",
"line_item":"",
"link_settings": {
"selection_width":"",
"selection_height":""
},
"url":"",
"content_type":"",
"content_id":"",
"new_tab":"0"
},
"similarityDetectionTool":"None",
"configuration_tool_type":"",
"grade_group_students_individually":false,
"group_category_id":null,
"peer_reviews":"0",
"automatic_peer_reviews":"0",
"peer_review_count":0,
"peer_reviews_assign_at":null,
"intra_group_peer_reviews":"0",
"anonymous_peer_reviews":"0",
"notify_of_update":"0",
"lock_at":null,
"unlock_at":null
}
}

View File

@@ -1,35 +0,0 @@
[
{
"id": 753,
"name": "Summer 2023",
"sis_term_id": null,
"sis_import_id": null,
"start_at": "2023-05-10T06:00:00Z",
"end_at": "2023-07-29T05:59:59Z",
"grading_period_group_id": null,
"workflow_state": "active",
"overrides": null
},
{
"id": 751,
"name": "Fall 2023",
"sis_term_id": null,
"sis_import_id": null,
"start_at": "2023-08-21T06:00:00Z",
"end_at": "2023-12-15T06:59:59Z",
"grading_period_group_id": null,
"workflow_state": "active",
"overrides": null
},
{
"id": 773,
"name": "Spring 2024",
"sis_term_id": null,
"sis_import_id": null,
"start_at": "2024-01-02T00:00:00Z",
"end_at": "2024-04-26T00:00:00Z",
"grading_period_group_id": null,
"workflow_state": "active",
"overrides": null
}
]