diff --git a/Management.Test/Markdown/CouresDifferencesTests.cs b/Management.Test/Markdown/CouresDifferencesTests.cs new file mode 100644 index 0000000..ab6bb57 --- /dev/null +++ b/Management.Test/Markdown/CouresDifferencesTests.cs @@ -0,0 +1,208 @@ + + +using LocalModels; + +public class CoursesDifferencesTests +{ + [Fact] + public void CanDetectNewSettings() + { + LocalCourse oldCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [] + }; + LocalCourse newCourse = new() + { + Settings = new() { Name = "new course name" }, + Modules = [] + }; + var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse); + + differences.Modules.Should().BeEmpty(); + differences.Settings.Should().NotBeNull(); + differences.Settings?.Name.Should().Be("new course name"); + } + + [Fact] + public void CanDetectNewModule() + { + + LocalCourse oldCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [] + }; + LocalCourse newCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [ + new() + { + Name = "new module", + } + ] + }; + var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse); + + differences.Modules.Should().NotBeNull(); + differences.Modules?.Count().Should().Be(1); + differences.Modules?.First().Name.Should().Be("new module"); + } + + [Fact] + public void CanDetectChangedAssignment() + { + LocalCourse oldCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [ + new() + { + Name = "new module", + Assignments = [ + new() + { + Name = "test assignment", + Description = "", + DueAt = new DateTime(), + SubmissionTypes = [], + Rubric = [] + } + ] + }] + }; + LocalCourse newCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [ + new() + { + Name = "new module", + Assignments = [ + new() + { + Name = "test assignment", + Description = "new description", + DueAt = new DateTime(), + SubmissionTypes = [], + Rubric = [] + } + ] + } + ] + }; + var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse); + + differences.Modules.Should().NotBeNull(); + differences.Modules?.Count().Should().Be(1); + differences.Modules?.First().Assignments.First().Description.Should().Be("new description"); + } + + [Fact] + public void CanProperlyIgnoreUnchangedModules() + { + var commonDate = new DateTime(); + LocalCourse oldCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [new() + { + Name = "new module", + Assignments = [ + new() + { + Name = "test assignment", + Description = "", + DueAt = commonDate, + SubmissionTypes = [], + Rubric = [] + } + ] + }] + }; + LocalCourse newCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [new() + { + Name = "new module", + Assignments = [ + new() + { + Name = "test assignment", + Description = "", + DueAt = commonDate, + SubmissionTypes = [], + Rubric = [] + } + ] + }] + }; + var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse); + + differences.Modules.Should().BeEmpty(); + } + + [Fact] + public void OnlyChangedAssignmentRepresented() + { + var commonDate = new DateTime(); + LocalCourse oldCourse = new() + { + Settings = new() { Name = "Test Course" }, + Modules = [new() + { + Name = "new module", + Assignments = [ + new() + { + Name = "test assignment", + Description = "", + DueAt = commonDate, + SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD], + Rubric = [ new() {Points = 1, Label = "rubric"} ], + }, + new() + { + Name = "test assignment 2", + Description = "", + DueAt = commonDate, + SubmissionTypes = [], + Rubric = [], + } + ] + }] + }; + LocalCourse newCourse = oldCourse with { + Modules = [ + new() + { + Name = "new module", + Assignments = [ + new() + { + Name = "test assignment", + Description = "", + DueAt = commonDate, + SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD], + Rubric = [ new() {Points = 1, Label = "rubric"} ], + }, + new() + { + Name = "test assignment 2 with a new name", + Description = "", + DueAt = commonDate, + SubmissionTypes = [], + Rubric = [] + } + ] + } + ] + }; + var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse); + + differences.Modules.First().Assignments.Count().Should().Be(1); + differences.Modules.First().Assignments.First().Name.Should().Be("test assignment 2 with a new name"); + } +} \ No newline at end of file diff --git a/Management.Web/Pages/Course/Module/ModuleDetail.razor b/Management.Web/Pages/Course/Module/ModuleDetail.razor index 91220a6..f7a007d 100644 --- a/Management.Web/Pages/Course/Module/ModuleDetail.razor +++ b/Management.Web/Pages/Course/Module/ModuleDetail.razor @@ -168,7 +168,7 @@ { } *@ - @foreach(var item in Module.SortedModuleItems) + @foreach(var item in Module.GetSortedModuleItems()) { @(item switch { diff --git a/Management/Models/Local/Assignment/LocalAssignment.cs b/Management/Models/Local/Assignment/LocalAssignment.cs index d61c306..f016182 100644 --- a/Management/Models/Local/Assignment/LocalAssignment.cs +++ b/Management/Models/Local/Assignment/LocalAssignment.cs @@ -1,7 +1,7 @@ namespace LocalModels; -public record LocalAssignment : IModuleItem +public sealed record LocalAssignment : IModuleItem { private string _name = ""; public string Name @@ -37,4 +37,13 @@ public record LocalAssignment : IModuleItem public string RubricToMarkdown() => this.AssignmentRubricToMarkdown(); public static LocalAssignment ParseMarkdown(string input) => LocalAssignmentMarkdownParser.ParseMarkdown(input); public static IEnumerable ParseRubricMarkdown(string rawMarkdown) => LocalAssignmentMarkdownParser.ParseRubricMarkdown(rawMarkdown); + + + public bool Equals(LocalAssignment? otherAssignment) + { + return ToMarkdown() == otherAssignment?.ToMarkdown(); + } + + public override int GetHashCode() => ToMarkdown().GetHashCode(); + } diff --git a/Management/Models/Local/LocalCourse.cs b/Management/Models/Local/LocalCourse.cs index 374c3a1..f4ad997 100644 --- a/Management/Models/Local/LocalCourse.cs +++ b/Management/Models/Local/LocalCourse.cs @@ -3,7 +3,7 @@ namespace LocalModels; public record LocalCourse { public IEnumerable Modules { get; init; } = Enumerable.Empty(); - public required LocalCourseSettings Settings { get; set; } + public required LocalCourseSettings Settings { get; init; } } public record SimpleTimeOnly diff --git a/Management/Models/Local/LocalModules.cs b/Management/Models/Local/LocalModules.cs index 5b87e6d..c0b0f23 100644 --- a/Management/Models/Local/LocalModules.cs +++ b/Management/Models/Local/LocalModules.cs @@ -1,6 +1,6 @@ namespace LocalModels; -public record LocalModule +public sealed record LocalModule { public string Name { get; init; } = string.Empty; public string Notes { get; set; } = string.Empty; @@ -8,10 +8,57 @@ public record LocalModule public IEnumerable Quizzes { get; init; } = []; public IEnumerable Pages { get; init; } = []; - public IEnumerable SortedModuleItems => + public IEnumerable GetSortedModuleItems() => Enumerable.Empty() .Concat(Assignments) .Concat(Quizzes) .Concat(Pages) .OrderBy(i => i.DueAt); + + public bool Equals(LocalModule? otherModule) + { + var areEqual = + string.Equals(Name, otherModule?.Name, StringComparison.OrdinalIgnoreCase) + && string.Equals(Notes, otherModule?.Notes, StringComparison.OrdinalIgnoreCase) + && CompareCollections(Assignments.OrderBy(x => x.Name), otherModule?.Assignments.OrderBy(x => x.Name)) + && CompareCollections(Quizzes.OrderBy(x => x.Name), otherModule?.Quizzes.OrderBy(x => x.Name)) + && CompareCollections(Pages.OrderBy(x => x.Name), otherModule?.Pages.OrderBy(x => x.Name)); + return areEqual; + } + + private static bool CompareCollections(IEnumerable first, IEnumerable? second) + { + var firstList = first.ToList(); + var secondList = second?.ToList(); + + if (firstList.Count != secondList?.Count) + return false; + + for (int i = 0; i < firstList.Count; i++) + { + if (!Equals(firstList[i], secondList[i])) + return false; + } + + return true; + } + + public override int GetHashCode() + { + HashCode hash = new HashCode(); + hash.Add(Name, StringComparer.OrdinalIgnoreCase); + hash.Add(Notes, StringComparer.OrdinalIgnoreCase); + AddRangeToHash(hash, Assignments.OrderBy(x => x.Name)); + AddRangeToHash(hash, Quizzes.OrderBy(x => x.Name)); + AddRangeToHash(hash, Pages.OrderBy(x => x.Name)); + return hash.ToHashCode(); + } + + private void AddRangeToHash(HashCode hash, IEnumerable items) + { + foreach (var item in items) + { + hash.Add(item); + } + } } diff --git a/Management/Services/Files/CourseDifferences.cs b/Management/Services/Files/CourseDifferences.cs new file mode 100644 index 0000000..672aba6 --- /dev/null +++ b/Management/Services/Files/CourseDifferences.cs @@ -0,0 +1,40 @@ +using LocalModels; + +public static class CourseDifferences +{ + public static NewCourseChanges GetNewChanges(LocalCourse newCourse, LocalCourse oldCourse) + { + if (newCourse == oldCourse) + return new NewCourseChanges(); + + var differentModules = newCourse.Modules + .Where(newModule => + !oldCourse.Modules.Any(oldModule => oldModule.Equals(newModule)) + ) + .Select(newModule => + { + var oldModule = oldCourse.Modules.FirstOrDefault(m => m.Name == newModule.Name); + if (oldModule == null) + return newModule; + + var newAssignments = newModule.Assignments.Where( + newAssignment => !oldModule.Assignments.Any(oldAssignment => newAssignment == oldAssignment) + ); + return newModule with { Assignments = newAssignments }; + }) + .ToArray(); + + return new NewCourseChanges + { + Settings = newCourse.Settings, + Modules = differentModules, + }; + } + +} + +public record NewCourseChanges +{ + public IEnumerable Modules { get; init; } = []; + public LocalCourseSettings? Settings { get; init; } +} \ No newline at end of file