From 96c47faeb4479157ac7b1409e02d62ace5add268 Mon Sep 17 00:00:00 2001 From: Jonathan Allen Date: Thu, 4 Jan 2024 12:50:23 -0700 Subject: [PATCH 1/2] Add user secrets id to project file --- .gitignore | 3 ++- Management.Web/Management.Web.csproj | 3 ++- Management.Web/Management.Web.csproj.user | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 Management.Web/Management.Web.csproj.user diff --git a/.gitignore b/.gitignore index 013c941..03614c2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ bin/ *.env storage/ tmp.json -tmp*.json \ No newline at end of file +tmp*.json +.vs/ \ No newline at end of file diff --git a/Management.Web/Management.Web.csproj b/Management.Web/Management.Web.csproj index cfd2ba2..ae2f6c5 100644 --- a/Management.Web/Management.Web.csproj +++ b/Management.Web/Management.Web.csproj @@ -1,4 +1,4 @@ - + @@ -18,5 +18,6 @@ net8.0 enable enable + 6dc43700-9593-43ca-bda7-4fa2c4e7abc7 diff --git a/Management.Web/Management.Web.csproj.user b/Management.Web/Management.Web.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/Management.Web/Management.Web.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file From 9354eedb58b15a29d568e868a9e103a694e86af7 Mon Sep 17 00:00:00 2001 From: Jonathan Allen Date: Thu, 4 Jan 2024 14:37:29 -0700 Subject: [PATCH 2/2] Always use \n, not Environment.NewLine --- .vscode/launch.json | 35 ++++++++++++++++ .vscode/tasks.json | 41 +++++++++++++++++++ Management.Test/Markdown/FileStorageTests.cs | 20 ++++++--- .../Markdown/Quiz/MultipleChoiceTests.cs | 2 +- Management.Web/Pages/Course.razor | 3 +- Management.Web/Program.cs | 8 ++-- Management/Management.csproj | 1 + .../Local/Assignment/LocalAssignment.cs | 34 +++++++-------- Management/Models/Local/Quiz/LocalQuiz.cs | 4 +- .../Models/Local/Quiz/LocalQuizQuestion.cs | 10 ++--- .../Services/Files/FileConfiguration.cs | 7 ++-- .../Services/Files/FileStorageManager.cs | 5 ++- .../Services/Files/LoadMarkdownCourse.cs | 8 ++-- .../Services/Files/SaveMarkdownCourse.cs | 4 +- Management/Services/WebRequestor.cs | 11 +++-- 15 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ec27518 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Management.Web/bin/Debug/net8.0/Management.Web.dll", + "args": [], + "cwd": "${workspaceFolder}/Management.Web", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0917445 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/canvasManagement.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/canvasManagement.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/canvasManagement.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Management.Test/Markdown/FileStorageTests.cs b/Management.Test/Markdown/FileStorageTests.cs index 07b0e32..0d20c10 100644 --- a/Management.Test/Markdown/FileStorageTests.cs +++ b/Management.Test/Markdown/FileStorageTests.cs @@ -1,5 +1,8 @@ +using System.Configuration; using LocalModels; using Management.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using NUnit.Framework.Internal; @@ -37,11 +40,16 @@ public class FileStorageTests var fileManagerLogger = new MyLogger(NullLogger.Instance); var markdownLoaderLogger = new MyLogger(NullLogger.Instance); var markdownSaverLogger = new MyLogger(NullLogger.Instance); - + var otherLogger = NullLoggerFactory.Instance.CreateLogger(); Environment.SetEnvironmentVariable("storageDirectory", storageDirectory); - var markdownLoader = new CourseMarkdownLoader(markdownLoaderLogger); - var markdownSaver = new MarkdownCourseSaver(markdownSaverLogger); - fileManager = new FileStorageManager(fileManagerLogger, markdownLoader, markdownSaver); + var config = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + var fileConfiguration = new FileConfiguration(config); + + var markdownLoader = new CourseMarkdownLoader(markdownLoaderLogger, fileConfiguration); + var markdownSaver = new MarkdownCourseSaver(markdownSaverLogger, fileConfiguration); + fileManager = new FileStorageManager(fileManagerLogger, markdownLoader, markdownSaver, otherLogger, fileConfiguration); } [Test] @@ -139,7 +147,9 @@ public class FileStorageTests var loadedCourses = await fileManager.LoadSavedMarkdownCourses(); var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name); - loadedCourse.Modules.First().Assignments.Should().BeEquivalentTo(testCourse.Modules.First().Assignments); + var actualAssignments = loadedCourse.Modules.First().Assignments; + var expectedAssignments = testCourse.Modules.First().Assignments; + actualAssignments.Should().BeEquivalentTo(expectedAssignments); } diff --git a/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs b/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs index 265b449..1cf1c8b 100644 --- a/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs +++ b/Management.Test/Markdown/Quiz/MultipleChoiceTests.cs @@ -32,7 +32,7 @@ lines Answers = new LocalQuizQuestionAnswer[] { new LocalQuizQuestionAnswer() { Correct = true, Text = "true" }, - new LocalQuizQuestionAnswer() { Correct = false, Text = "false" + Environment.NewLine +Environment.NewLine + "endline" }, + new LocalQuizQuestionAnswer() { Correct = false, Text = "false\n\nendline" }, } } } diff --git a/Management.Web/Pages/Course.razor b/Management.Web/Pages/Course.razor index 38f5505..2bf30cb 100644 --- a/Management.Web/Pages/Course.razor +++ b/Management.Web/Pages/Course.razor @@ -12,6 +12,7 @@ @inject CanvasService canvas @inject CoursePlanner planner @inject NavigationManager navigtion +@inject IConfiguration config @code { [Parameter] @@ -55,7 +56,7 @@ + href="@($"{config["CANVAS_URL"]}/courses/{planner.LocalCourse.Settings.CanvasId}")"> View In Canvas
diff --git a/Management.Web/Program.cs b/Management.Web/Program.cs index 674bc6a..0760424 100644 --- a/Management.Web/Program.cs +++ b/Management.Web/Program.cs @@ -23,14 +23,14 @@ DotEnv.Load(); var builder = WebApplication.CreateBuilder(args); -var canvas_token = Environment.GetEnvironmentVariable("CANVAS_TOKEN"); +var canvas_token = builder.Configuration["CANVAS_TOKEN"]; if (canvas_token == null) throw new Exception("CANVAS_TOKEN is null"); -var canvas_url = Environment.GetEnvironmentVariable("CANVAS_URL"); +var canvas_url = builder.Configuration["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"); + builder.Configuration["CANVAS_URL"] = "https://snow.instructure.com"; } const string serviceName = "canvas-management"; @@ -94,6 +94,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); + builder.Services.AddSignalR(e => { e.MaximumReceiveMessageSize = 102400000; diff --git a/Management/Management.csproj b/Management/Management.csproj index 508fbaf..82b1c84 100644 --- a/Management/Management.csproj +++ b/Management/Management.csproj @@ -8,6 +8,7 @@ + diff --git a/Management/Models/Local/Assignment/LocalAssignment.cs b/Management/Models/Local/Assignment/LocalAssignment.cs index b19c4fa..919920e 100644 --- a/Management/Models/Local/Assignment/LocalAssignment.cs +++ b/Management/Models/Local/Assignment/LocalAssignment.cs @@ -62,9 +62,9 @@ public record LocalAssignment var settingsString = input.Split("---")[0]; var (name, localAssignmentGroupName, submissionTypes, dueAt, lockAt) = parseSettings(settingsString); - var description = input.Split("---" + Environment.NewLine)[1].Split("## Rubric")[0]; + var description = input.Split("---\n")[1].Split("## Rubric")[0]; - var rubricString = input.Split("## Rubric" + Environment.NewLine)[1]; + var rubricString = input.Split("## Rubric\n")[1]; var rubric = ParseRubricMarkdown(rubricString); return new LocalAssignment() { @@ -100,15 +100,17 @@ public record LocalAssignment private static List parseSubmissionTypes(string input) { + input = input.Replace("\r\n", "\n"); List submissionTypes = new List(); // Define a regular expression pattern to match the bulleted list items string startOfTypePattern = @"- (.+)"; Regex regex = new Regex(startOfTypePattern); - var inputAfterSubmissionTypes = input.Split("SubmissionTypes:" + Environment.NewLine)[1]; + var words = input.Split("SubmissionTypes:"); + var inputAfterSubmissionTypes = words[1]; - string[] lines = inputAfterSubmissionTypes.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + string[] lines = inputAfterSubmissionTypes.Split("\n", StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines) { @@ -143,10 +145,10 @@ public record LocalAssignment var settingsMarkdown = settingsToMarkdown(); var rubricMarkdown = RubricToMarkdown(); var assignmentMarkdown = - settingsMarkdown + Environment.NewLine - + "---" + Environment.NewLine + Environment.NewLine - + Description + Environment.NewLine - + Environment.NewLine + "## Rubric" + Environment.NewLine + Environment.NewLine + settingsMarkdown + "\n" + + "---\n\n" + + Description + "\n\n" + + "## Rubric\n\n" + rubricMarkdown; return assignmentMarkdown; @@ -158,7 +160,7 @@ public record LocalAssignment foreach (var item in Rubric) { var pointLabel = item.Points > 1 ? "pts" : "pt"; - builder.Append($"- {item.Points}{pointLabel}: {item.Label}" + Environment.NewLine); + builder.Append($"- {item.Points}{pointLabel}: {item.Label}" + "\n"); } return builder.ToString(); } @@ -166,14 +168,14 @@ public record LocalAssignment private string settingsToMarkdown() { var builder = new StringBuilder(); - builder.Append($"Name: {Name}" + Environment.NewLine); - builder.Append($"LockAt: {LockAt}" + Environment.NewLine); - builder.Append($"DueAt: {DueAt}" + Environment.NewLine); - builder.Append($"AssignmentGroupName: {LocalAssignmentGroupName}" + Environment.NewLine); - builder.Append($"SubmissionTypes:" + Environment.NewLine); + builder.Append($"Name: {Name}" + "\n"); + builder.Append($"LockAt: {LockAt}" + "\n"); + builder.Append($"DueAt: {DueAt}" + "\n"); + builder.Append($"AssignmentGroupName: {LocalAssignmentGroupName}" + "\n"); + builder.Append($"SubmissionTypes:" + "\n"); foreach (var submissionType in SubmissionTypes) { - builder.Append($"- {submissionType}" + Environment.NewLine); + builder.Append($"- {submissionType}" + "\n"); } return builder.ToString(); } @@ -182,7 +184,7 @@ public record LocalAssignment { if (rawMarkdown.Trim() == string.Empty) return []; - var lines = rawMarkdown.Trim().Split(Environment.NewLine); + var lines = rawMarkdown.Trim().Split("\n"); var items = lines.Select(parseIndividualRubricItemMarkdown).ToArray(); return items; } diff --git a/Management/Models/Local/Quiz/LocalQuiz.cs b/Management/Models/Local/Quiz/LocalQuiz.cs index c56cc68..de3c81f 100644 --- a/Management/Models/Local/Quiz/LocalQuiz.cs +++ b/Management/Models/Local/Quiz/LocalQuiz.cs @@ -38,7 +38,7 @@ public record LocalQuiz public string ToMarkdown() { var questionMarkdownArray = Questions.Select(q => q.ToMarkdown()).ToArray(); - var questionDelimiter = Environment.NewLine + Environment.NewLine + "---" + Environment.NewLine + Environment.NewLine; + var questionDelimiter = "\n\n---\n\n"; var questionMarkdown = string.Join(questionDelimiter, questionMarkdownArray); return $@"Name: {Name} @@ -57,7 +57,7 @@ Description: {Description} public static LocalQuiz ParseMarkdown(string input) { - var splitInput = input.Split("---" + Environment.NewLine); + var splitInput = input.Split("---\n"); var settings = splitInput[0]; var quizWithoutQuestions = getQuizWithOnlySettings(settings); diff --git a/Management/Models/Local/Quiz/LocalQuizQuestion.cs b/Management/Models/Local/Quiz/LocalQuizQuestion.cs index d1bbf78..6c3b4be 100644 --- a/Management/Models/Local/Quiz/LocalQuizQuestion.cs +++ b/Management/Models/Local/Quiz/LocalQuizQuestion.cs @@ -14,7 +14,7 @@ public record LocalQuizQuestion public string ToMarkdown() { var answerArray = Answers.Select(getAnswerMarkdown); - var answersText = string.Join(Environment.NewLine, answerArray); + var answersText = string.Join("\n", answerArray); var questionTypeIndicator = QuestionType == "essay" || QuestionType == "short_answer" ? QuestionType : ""; return $@"Points: {Points} @@ -25,7 +25,7 @@ public record LocalQuizQuestion private string getAnswerMarkdown(LocalQuizQuestionAnswer answer, int index) { var multilineMarkdownCompatibleText = answer.Text.StartsWith("```") - ? Environment.NewLine + answer.Text + ? "\n" + answer.Text : answer.Text; if (QuestionType == "multiple_answers") @@ -53,7 +53,7 @@ public record LocalQuizQuestion public static LocalQuizQuestion ParseMarkdown(string input, int questionIndex) { - var lines = input.Trim().Split(Environment.NewLine); + var lines = input.Trim().Split("\n"); var firstLineIsPoints = lines.First().Contains("points: ", StringComparison.CurrentCultureIgnoreCase); var textHasPoints = lines.Length > 0 @@ -84,7 +84,7 @@ public record LocalQuizQuestion ) .ToArray() : linesWithoutAnswers; - var description = string.Join(Environment.NewLine, descriptionLines); + var description = string.Join("\n", descriptionLines); @@ -164,7 +164,7 @@ public record LocalQuizQuestion } if (acc.Count != 0) // Append to the previous line if there is one - acc[^1] += Environment.NewLine + line; + acc[^1] += "\n" + line; else acc.Add(line); diff --git a/Management/Services/Files/FileConfiguration.cs b/Management/Services/Files/FileConfiguration.cs index e94a26e..f1bd4b5 100644 --- a/Management/Services/Files/FileConfiguration.cs +++ b/Management/Services/Files/FileConfiguration.cs @@ -1,10 +1,11 @@ using Management.Services; +using Microsoft.Extensions.Configuration; -public class FileConfiguration +public class FileConfiguration(IConfiguration config) { - public static string GetBasePath() + public string GetBasePath() { - string? storageDirectory = Environment.GetEnvironmentVariable("storageDirectory"); + string? storageDirectory = config["storageDirectory"]; var basePath = storageDirectory ?? Path.GetFullPath("../storage"); if (!Directory.Exists(basePath)) diff --git a/Management/Services/Files/FileStorageManager.cs b/Management/Services/Files/FileStorageManager.cs index 0e9bba0..564f438 100644 --- a/Management/Services/Files/FileStorageManager.cs +++ b/Management/Services/Files/FileStorageManager.cs @@ -15,7 +15,8 @@ public class FileStorageManager MyLogger logger, CourseMarkdownLoader courseMarkdownLoader, MarkdownCourseSaver saveMarkdownCourse, - ILogger otherLogger + ILogger otherLogger, + FileConfiguration fileConfig ) { using var activity = DiagnosticsConfig.Source.StartActivity("loading storage directory"); @@ -23,7 +24,7 @@ public class FileStorageManager _courseMarkdownLoader = courseMarkdownLoader; _saveMarkdownCourse = saveMarkdownCourse; _otherLogger = otherLogger; - _basePath = FileConfiguration.GetBasePath(); + _basePath = fileConfig.GetBasePath(); this.logger.Log("Using storage directory: " + _basePath); } diff --git a/Management/Services/Files/LoadMarkdownCourse.cs b/Management/Services/Files/LoadMarkdownCourse.cs index b21b4da..c3146e5 100644 --- a/Management/Services/Files/LoadMarkdownCourse.cs +++ b/Management/Services/Files/LoadMarkdownCourse.cs @@ -6,10 +6,10 @@ public class CourseMarkdownLoader private readonly MyLogger logger; private readonly string _basePath; - public CourseMarkdownLoader(MyLogger logger) + public CourseMarkdownLoader(MyLogger logger, FileConfiguration fileConfig) { this.logger = logger; - _basePath = FileConfiguration.GetBasePath(); + _basePath = fileConfig.GetBasePath(); } public async Task> LoadSavedMarkdownCourses() @@ -101,7 +101,7 @@ public class CourseMarkdownLoader var assignmentPromises = assignmentFiles .Select(async filePath => { - var rawFile = await File.ReadAllTextAsync(filePath); + var rawFile = (await File.ReadAllTextAsync(filePath)).Replace("\r\n", "\n"); return LocalAssignment.ParseMarkdown(rawFile); }) .ToArray(); @@ -122,7 +122,7 @@ public class CourseMarkdownLoader var quizPromises = quizFiles .Select(async path => { - var rawQuiz = await File.ReadAllTextAsync(path); + var rawQuiz = (await File.ReadAllTextAsync(path)).Replace("\r\n", "\n"); return LocalQuiz.ParseMarkdown(rawQuiz); }); diff --git a/Management/Services/Files/SaveMarkdownCourse.cs b/Management/Services/Files/SaveMarkdownCourse.cs index 4d79d67..a73f766 100644 --- a/Management/Services/Files/SaveMarkdownCourse.cs +++ b/Management/Services/Files/SaveMarkdownCourse.cs @@ -2,10 +2,10 @@ using System.Threading.Tasks.Sources; using LocalModels; namespace Management.Services; -public class MarkdownCourseSaver(MyLogger logger) +public class MarkdownCourseSaver(MyLogger logger, FileConfiguration fileConfig) { private readonly MyLogger _logger = logger; - private readonly string _basePath = FileConfiguration.GetBasePath(); + private readonly string _basePath = fileConfig.GetBasePath(); public async Task Save(LocalCourse course, LocalCourse? previouslyStoredCourse) { diff --git a/Management/Services/WebRequestor.cs b/Management/Services/WebRequestor.cs index 2ae60af..d0fe298 100644 --- a/Management/Services/WebRequestor.cs +++ b/Management/Services/WebRequestor.cs @@ -1,19 +1,22 @@ +using Microsoft.Extensions.Configuration; using RestSharp; public class WebRequestor : IWebRequestor { - private string BaseUrl = Environment.GetEnvironmentVariable("CANVAS_URL") + "/api/v1/"; + private string BaseUrl = ""; private string token; private RestClient client; + private readonly IConfiguration _config; - public WebRequestor() + public WebRequestor(IConfiguration config) { + _config = config; token = - Environment.GetEnvironmentVariable("CANVAS_TOKEN") + _config["CANVAS_TOKEN"] ?? throw new Exception("CANVAS_TOKEN not in environment"); + BaseUrl = _config["CANVAS_URL"] + "/api/v1/"; client = new RestClient(BaseUrl); client.AddDefaultHeader("Authorization", $"Bearer {token}"); - } public async Task<(T[]?, RestResponse)> GetManyAsync(RestRequest request)