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