From 2aacab8f868ae4d9b884b8d30f123720ab7e6886 Mon Sep 17 00:00:00 2001 From: Matthew Chenette Date: Tue, 5 Aug 2025 09:49:51 -0500 Subject: [PATCH 1/2] updated gitignore --- .gitignore | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.gitignore b/.gitignore index 82f9275..2655201 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,71 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +### DOTNET SECTION ### +## A streamlined .gitignore for modern .NET projects +## including temporary files, build results, and +## files generated by popular .NET tools. If you are +## developing with Visual Studio, the VS .gitignore +## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## has more thorough IDE-specific entries. +## +## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg + +# Others +~$* +*~ +CodeCoverage/ + +# MSBuild Binary and Structured Log +*.binlog + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# SonarQube +.sonar/ +.scannerwork/ +.sonarqube/ +sonar-project.properties +.sonarlint/ +.sonarwork/ +reports/ + +copilot.sln \ No newline at end of file From 5ee20a220319b3cf6fbb187eae4d2f41b84334db Mon Sep 17 00:00:00 2001 From: Matthew Chenette Date: Tue, 5 Aug 2025 09:53:02 -0500 Subject: [PATCH 2/2] added dotnet app --- DotnetApp.Tests/DotnetApp.Tests.csproj | 22 ++ DotnetApp.Tests/Models/TestItemTest.cs | 149 ++++++++++ .../Services/InMemoryTaskServiceTests.cs | 102 +++++++ DotnetApp/DotnetApp.csproj | 14 + DotnetApp/Models/TaskItem.cs | 128 +++++++++ DotnetApp/Program.cs | 44 +++ DotnetApp/Services/CsvTaskService.cs | 154 ++++++++++ DotnetApp/Services/ITaskService.cs | 45 +++ DotnetApp/Services/InMemoryTaskService.cs | 59 ++++ DotnetApp/wwwroot/index.html | 272 ++++++++++++++++++ 10 files changed, 989 insertions(+) create mode 100644 DotnetApp.Tests/DotnetApp.Tests.csproj create mode 100644 DotnetApp.Tests/Models/TestItemTest.cs create mode 100644 DotnetApp.Tests/Services/InMemoryTaskServiceTests.cs create mode 100644 DotnetApp/DotnetApp.csproj create mode 100644 DotnetApp/Models/TaskItem.cs create mode 100644 DotnetApp/Program.cs create mode 100644 DotnetApp/Services/CsvTaskService.cs create mode 100644 DotnetApp/Services/ITaskService.cs create mode 100644 DotnetApp/Services/InMemoryTaskService.cs create mode 100644 DotnetApp/wwwroot/index.html diff --git a/DotnetApp.Tests/DotnetApp.Tests.csproj b/DotnetApp.Tests/DotnetApp.Tests.csproj new file mode 100644 index 0000000..7d69ca6 --- /dev/null +++ b/DotnetApp.Tests/DotnetApp.Tests.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/DotnetApp.Tests/Models/TestItemTest.cs b/DotnetApp.Tests/Models/TestItemTest.cs new file mode 100644 index 0000000..0ccff25 --- /dev/null +++ b/DotnetApp.Tests/Models/TestItemTest.cs @@ -0,0 +1,149 @@ +using System; +using Xunit; +using DotnetApp.Models; + +namespace DotnetApp.Models.Tests +{ + public class TaskItemTest + { + [Fact] + public void CalculateTaskScore_ShouldReturnCorrectScore_ForPriorityZero() + { + // Arrange + var task = new TaskItem + { + Priority = 0, + Status = "pending", + CreatedAt = DateTime.UtcNow.AddDays(-1), + IsCompleted = false, + Title = "Test Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(1, score); + } + + [Fact] + public void CalculateTaskScore_ShouldReturnCorrectScore_ForPriorityOneAndPendingStatus() + { + // Arrange + var task = new TaskItem + { + Priority = 1, + Status = "pending", + CreatedAt = DateTime.UtcNow.AddDays(-1), + IsCompleted = false, + Title = "Test Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(13, score); + } + + [Fact] + public void CalculateTaskScore_ShouldReturnCorrectScore_ForPriorityTwoAndInProgressStatus() + { + // Arrange + var task = new TaskItem + { + Priority = 2, + Status = "in-progress", + CreatedAt = DateTime.UtcNow.AddDays(-8), + IsCompleted = false, + Title = "Test Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(10, score); + } + + [Fact] + public void CalculateTaskScore_ShouldDoubleScore_ForPendingStatusAndOldTask() + { + // Arrange + var task = new TaskItem + { + Priority = 2, + Status = "pending", + CreatedAt = DateTime.UtcNow.AddDays(-15), + IsCompleted = false, + Title = "Test Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(15, score); + } + + [Fact] + public void CalculateTaskScore_ShouldSubtractScore_ForCompletedInProgressTask() + { + // Arrange + var task = new TaskItem + { + Priority = 2, + Status = "in-progress", + CreatedAt = DateTime.UtcNow.AddDays(-1), + IsCompleted = true, + Title = "Test Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(0, score); + } + + [Fact] + public void CalculateTaskScore_ShouldAddScore_ForLongWordsInTitle() + { + // Arrange + var task = new TaskItem + { + Priority = 3, + Status = "in-progress", + CreatedAt = DateTime.UtcNow.AddDays(-1), + IsCompleted = false, + Title = "ThisIsAVeryLongWord Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(2, score); + } + + [Fact] + public void CalculateTaskScore_ShouldReturnZero_ForNegativeScore() + { + // Arrange + var task = new TaskItem + { + Priority = 3, + Status = "completed", + CreatedAt = DateTime.UtcNow.AddDays(-1), + IsCompleted = true, + Title = "Test Task" + }; + + // Act + var score = task.CalculateTaskScore(); + + // Assert + Assert.Equal(1, score); + } + } +} \ No newline at end of file diff --git a/DotnetApp.Tests/Services/InMemoryTaskServiceTests.cs b/DotnetApp.Tests/Services/InMemoryTaskServiceTests.cs new file mode 100644 index 0000000..b4ff870 --- /dev/null +++ b/DotnetApp.Tests/Services/InMemoryTaskServiceTests.cs @@ -0,0 +1,102 @@ +using System.Linq; +using DotnetApp.Models; +using DotnetApp.Services; +using Xunit; + +namespace DotnetApp.Tests.Services +{ + public class InMemoryTaskServiceTests + { + [Fact] + public void CreateTask_AssignsIdAndStoresTask() + { + var service = new InMemoryTaskService(); + var task = new TaskItem { Title = "Test Task" }; + + service.CreateTask(task); + + Assert.NotEqual(0, task.Id); + var all = service.GetAllTasks().ToList(); + Assert.Contains(all, t => t.Id == task.Id && t.Title == "Test Task"); + } + + [Fact] + public void GetTaskById_ReturnsCorrectTaskOrNull() + { + var service = new InMemoryTaskService(); + var task = new TaskItem { Title = "GetById" }; + service.CreateTask(task); + var id = task.Id; + + var found = service.GetTaskById(id); + Assert.NotNull(found); + Assert.Equal("GetById", found.Title); + + var missing = service.GetTaskById(id + 999); + Assert.Null(missing); + } + + [Fact] + public void UpdateTask_NonExisting_ReturnsFalse() + { + var service = new InMemoryTaskService(); + var updated = new TaskItem { Title = "Nope" }; + + var result = service.UpdateTask(999, updated); + Assert.False(result); + } + + [Fact] + public void UpdateTask_Existing_ReturnsTrueAndUpdates() + { + var service = new InMemoryTaskService(); + var task = new TaskItem { Title = "Original" }; + service.CreateTask(task); + var id = task.Id; + + var updated = new TaskItem { Title = "Updated" }; + var result = service.UpdateTask(id, updated); + + Assert.True(result); + Assert.Equal(id, updated.Id); + var fetched = service.GetTaskById(id); + Assert.Equal("Updated", fetched.Title); + } + + [Fact] + public void DeleteTask_ReturnsTrueOnceAndRemoves() + { + var service = new InMemoryTaskService(); + var task = new TaskItem { Title = "ToDelete" }; + service.CreateTask(task); + var id = task.Id; + + var first = service.DeleteTask(id); + var second = service.DeleteTask(id); + + Assert.True(first); + Assert.False(second); + Assert.Null(service.GetTaskById(id)); + } + + [Fact] + public void GetAllTasks_Empty_ReturnsEmpty() + { + var service = new InMemoryTaskService(); + var all = service.GetAllTasks().ToList(); + Assert.Empty(all); + } + + [Fact] + public void CreateTask_SequentialIds() + { + var service = new InMemoryTaskService(); + var t1 = new TaskItem { Title = "First" }; + service.CreateTask(t1); + var t2 = new TaskItem { Title = "Second" }; + service.CreateTask(t2); + + Assert.Equal(t1.Id + 1, t2.Id); + } + } +} diff --git a/DotnetApp/DotnetApp.csproj b/DotnetApp/DotnetApp.csproj new file mode 100644 index 0000000..00ccec3 --- /dev/null +++ b/DotnetApp/DotnetApp.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + DotnetApp + enable + enable + + + + + + diff --git a/DotnetApp/Models/TaskItem.cs b/DotnetApp/Models/TaskItem.cs new file mode 100644 index 0000000..ea15aaf --- /dev/null +++ b/DotnetApp/Models/TaskItem.cs @@ -0,0 +1,128 @@ +using System.Text.Json.Serialization; + +namespace DotnetApp.Models +{ + public class TaskItem + { + public int Id { get; set; } + public string Title { get; set; } = default!; + public string? Description { get; set; } + public bool IsCompleted { get; set; } + + [JsonPropertyName("priority")] + public int Priority { get; set; } = 3; + + [JsonPropertyName("status")] + public string Status { get; set; } = "pending"; + + [JsonPropertyName("created_at")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public int CalculateTaskScore() + { + int score = 0; + + score += CalculatePriorityScore(); + score += CalculateStatusScore(score); + + return Math.Max(0, score); + } + + private int CalculatePriorityScore() + { + int score = 0; + + if (Priority <= 0) + { + score += 1; + } + else if (Priority == 1) + { + score += 10; + if (Status == "pending") + { + score += 3; + } + } + else if (Priority == 2) + { + score += 5; + if (Status == "in-progress" && !IsCompleted) + { + score += 2; + if ((DateTime.UtcNow - CreatedAt).TotalDays > 7) + { + score += 3; + } + } + } + else + { + score += 1; + } + + return score; + } + + private int CalculateStatusScore(int currentScore) + { + int score = 0; + + switch (Status.ToLower()) + { + case "pending": + score += CalculatePendingScore(currentScore); + break; + case "in-progress": + score += CalculateInProgressScore(); + break; + default: + if (!IsCompleted && Priority < 3) + { + score += 3; + } + break; + } + + return score; + } + + private int CalculatePendingScore(int currentScore) + { + int score = 0; + + if ((DateTime.UtcNow - CreatedAt).TotalDays > 14) + { + score += currentScore * 2; + if (Priority < 3) + { + score += 5; + } + } + + return score; + } + + private int CalculateInProgressScore() + { + int score = 0; + + if (IsCompleted) + { + score -= 5; + } + else + { + foreach (var word in Title.Split(' ')) + { + if (word.Length > 10) + { + score += 1; + } + } + } + + return score; + } + } +} diff --git a/DotnetApp/Program.cs b/DotnetApp/Program.cs new file mode 100644 index 0000000..586449a --- /dev/null +++ b/DotnetApp/Program.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Linq; +using Microsoft.Extensions.FileProviders; +using DotnetApp.Services; +using DotnetApp.Models; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Serve UI from wwwroot instead of external templates folder +app.UseDefaultFiles(); +app.UseStaticFiles(); + +// Replace simple GET /tasks with optional status query +app.MapGet("/tasks", (string? status, ITaskService service) => +{ + var tasks = service.GetAllTasks(); + if (!string.IsNullOrEmpty(status)) + tasks = tasks.Where(t => t.Status == status); + return Results.Ok(tasks); +}); +app.MapGet("/tasks/{id}", (int id, ITaskService service) => + service.GetTaskById(id) is TaskItem task ? Results.Ok(task) : Results.NotFound()); +app.MapPost("/tasks", (TaskItem task, ITaskService service) => +{ + service.CreateTask(task); + return Results.Created($"/tasks/{task.Id}", task); +}); +// Update returns the modified task JSON instead of NoContent +app.MapPut("/tasks/{id}", (int id, TaskItem updatedTask, ITaskService service) => +{ + updatedTask.Id = id; + return service.UpdateTask(id, updatedTask) + ? Results.Ok(updatedTask) + : Results.NotFound(); +}); +app.MapDelete("/tasks/{id}", (int id, ITaskService service) => + service.DeleteTask(id) ? Results.NoContent() : Results.NotFound()); + +await app.RunAsync(); diff --git a/DotnetApp/Services/CsvTaskService.cs b/DotnetApp/Services/CsvTaskService.cs new file mode 100644 index 0000000..8f9e5f2 --- /dev/null +++ b/DotnetApp/Services/CsvTaskService.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using DotnetApp.Models; + +namespace DotnetApp.Services +{ + /// + /// Provides a CSV-based implementation of the interface. + /// + public class CsvTaskService : ITaskService + { + private readonly string _filePath; + private readonly object _lock = new object(); + private int _nextId; + + /// + /// Initializes a new instance of the class. + /// + public CsvTaskService() + { + _filePath = Path.Combine(AppContext.BaseDirectory, "tasks.csv"); + if (!File.Exists(_filePath)) + { + File.WriteAllText(_filePath, "Id,Title,Description,IsCompleted,Status,Priority,CreatedAt\n"); + } + var tasks = ReadAll(); + _nextId = tasks.Any() ? tasks.Max(t => t.Id) : 0; + } + + /// + /// Reads all tasks from the CSV file. + /// + /// A list of all task items. + private List ReadAll() + { + var lines = File.ReadAllLines(_filePath); + return lines + .Skip(1) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Select(line => + { + var parts = line.Split(','); + return new TaskItem + { + Id = int.Parse(parts[0]), + Title = parts[1], + Description = string.IsNullOrEmpty(parts[2]) ? null : parts[2], + IsCompleted = bool.Parse(parts[3]), + Status = parts[4], + Priority = int.Parse(parts[5]), + CreatedAt = DateTime.Parse(parts[6], null, DateTimeStyles.RoundtripKind) + }; + }) + .ToList(); + } + + /// + /// Writes all tasks to the CSV file. + /// + /// The tasks to write. + private void WriteAll(IEnumerable tasks) + { + var lines = new List { "Id,Title,Description,IsCompleted,Status,Priority,CreatedAt" }; + lines.AddRange(tasks.Select(t => + string.Join(",", + t.Id, + Escape(t.Title), + Escape(t.Description), + t.IsCompleted, + t.Status, + t.Priority, + t.CreatedAt.ToString("O") + ) + )); + File.WriteAllLines(_filePath, lines); + } + + /// + /// Escapes a string value for CSV compatibility. + /// + /// The string value to escape. + /// The escaped string. + private string Escape(string? value) => value?.Replace("\"", "\"\"") ?? string.Empty; + + /// + /// Retrieves all tasks from the CSV file. + /// + /// A collection of all task items. + public IEnumerable GetAllTasks() => ReadAll(); + + /// + /// Retrieves a task by its unique identifier from the CSV file. + /// + /// The unique identifier of the task. + /// The task item if found; otherwise, null. + public TaskItem? GetTaskById(int id) => ReadAll().FirstOrDefault(t => t.Id == id); + + /// + /// Creates a new task and appends it to the CSV file. + /// + /// The task item to create. + public void CreateTask(TaskItem task) + { + lock (_lock) + { + task.Id = ++_nextId; + var tasks = ReadAll(); + tasks.Add(task); + WriteAll(tasks); + } + } + + /// + /// Updates an existing task in the CSV file. + /// + /// The unique identifier of the task to update. + /// The updated task item. + /// True if the update was successful; otherwise, false. + public bool UpdateTask(int id, TaskItem updatedTask) + { + lock (_lock) + { + var tasks = ReadAll(); + var existing = tasks.FirstOrDefault(t => t.Id == id); + if (existing == null) return false; + updatedTask.Id = id; + tasks.Remove(existing); + tasks.Add(updatedTask); + WriteAll(tasks); + return true; + } + } + + /// + /// Deletes a task from the CSV file by its unique identifier. + /// + /// The unique identifier of the task to delete. + /// True if the deletion was successful; otherwise, false. + public bool DeleteTask(int id) + { + lock (_lock) + { + var tasks = ReadAll(); + var removed = tasks.RemoveAll(t => t.Id == id) > 0; + if (!removed) return false; + WriteAll(tasks); + return true; + } + } + } +} diff --git a/DotnetApp/Services/ITaskService.cs b/DotnetApp/Services/ITaskService.cs new file mode 100644 index 0000000..6df4d7c --- /dev/null +++ b/DotnetApp/Services/ITaskService.cs @@ -0,0 +1,45 @@ +namespace DotnetApp.Services +{ + using DotnetApp.Models; + using System.Collections.Generic; + + /// + /// Defines the contract for task management services. + /// + public interface ITaskService + { + /// + /// Retrieves all tasks. + /// + /// A collection of all task items. + IEnumerable GetAllTasks(); + + /// + /// Retrieves a task by its unique identifier. + /// + /// The unique identifier of the task. + /// The task item if found; otherwise, null. + TaskItem? GetTaskById(int id); + + /// + /// Creates a new task. + /// + /// The task item to create. + void CreateTask(TaskItem task); + + /// + /// Updates an existing task. + /// + /// The unique identifier of the task to update. + /// The updated task item. + /// True if the update was successful; otherwise, false. + bool UpdateTask(int id, TaskItem updatedTask); + + /// + /// Deletes a task by its unique identifier. + /// + /// The unique identifier of the task to delete. + /// True if the deletion was successful; otherwise, false. + bool DeleteTask(int id); + } +} diff --git a/DotnetApp/Services/InMemoryTaskService.cs b/DotnetApp/Services/InMemoryTaskService.cs new file mode 100644 index 0000000..ee448bf --- /dev/null +++ b/DotnetApp/Services/InMemoryTaskService.cs @@ -0,0 +1,59 @@ +namespace DotnetApp.Services +{ + using System.Collections.Concurrent; + using DotnetApp.Models; + + /// + /// Provides an in-memory implementation of the interface. + /// + public class InMemoryTaskService : ITaskService + { + private readonly ConcurrentDictionary _tasks = new(); + private int _nextId = 1; + + /// + /// Retrieves all tasks stored in memory. + /// + /// A collection of all task items. + public IEnumerable GetAllTasks() => _tasks.Values; + + /// + /// Retrieves a task by its unique identifier. + /// + /// The unique identifier of the task. + /// The task item if found; otherwise, null. + public TaskItem? GetTaskById(int id) => _tasks.TryGetValue(id, out var task) ? task : null; + + /// + /// Creates a new task and stores it in memory. + /// + /// The task item to create. + public void CreateTask(TaskItem task) + { + var id = System.Threading.Interlocked.Increment(ref _nextId); + task.Id = id; + _tasks[id] = task; + } + + /// + /// Updates an existing task in memory. + /// + /// The unique identifier of the task to update. + /// The updated task item. + /// True if the update was successful; otherwise, false. + public bool UpdateTask(int id, TaskItem updatedTask) + { + if (!_tasks.ContainsKey(id)) return false; + updatedTask.Id = id; + _tasks[id] = updatedTask; + return true; + } + + /// + /// Deletes a task from memory by its unique identifier. + /// + /// The unique identifier of the task to delete. + /// True if the deletion was successful; otherwise, false. + public bool DeleteTask(int id) => _tasks.TryRemove(id, out _); + } +} diff --git a/DotnetApp/wwwroot/index.html b/DotnetApp/wwwroot/index.html new file mode 100644 index 0000000..b60a42a --- /dev/null +++ b/DotnetApp/wwwroot/index.html @@ -0,0 +1,272 @@ + + + + + + Task Manager + + + +

Task Manager

+ + +
+

Add New Task

+ + + + +
+ + +
+ + +
+ + +

Tasks

+
+ +
Loading tasks...
+
+ + + + \ No newline at end of file