Updates to encompass benchmarks

This commit is contained in:
ivan-pelly
2026-03-07 16:10:55 -08:00
parent 69e96403f4
commit 3d531298e2
65 changed files with 2505 additions and 86 deletions
+148
View File
@@ -130,6 +130,39 @@ public class StudentController : BaseController
});
}
[HttpGet("{idStudent:guid}/benchmarks")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult<StudentBenchmarkSummary>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ResponseResult<StudentBenchmarkSummary>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ResponseResult<StudentBenchmarkSummary>>> GetBenchmarks(Guid idStudent)
{
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
if (error is not null)
{
return error;
}
var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role);
if (!students.Select(s => s.StudentId).Contains(idStudent))
{
return NotFound(new ResponseResult<StudentBenchmarkSummary>
{
Success = false,
Message = "Student not found."
});
}
var summary = await _studentRepository.GetBenchmarkSummaryAsync(idStudent);
return Ok(new ResponseResult<StudentBenchmarkSummary>
{
Success = true,
Message = "Benchmarks retrieved successfully.",
Data = summary
});
}
[HttpPost("{idStudent:guid}/goals")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult<StudentGoalItem>), StatusCodes.Status201Created)]
@@ -205,6 +238,38 @@ public class StudentController : BaseController
});
}
[HttpPut("{idStudent:guid}/goals/{idGoal:guid}")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ResponseResult<object>>> UpdateGoal(Guid idStudent, Guid idGoal, [FromBody] UpdateGoalDto dto)
{
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
if (error is not null)
{
return error;
}
var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role);
if (!students.Select(s => s.StudentId).Contains(idStudent))
{
return NotFound(new ResponseResult<object>
{
Success = false,
Message = "Student not found."
});
}
var updated = await _studentRepository.UpdateGoalAsync(idGoal, dto);
return Ok(new ResponseResult<object>
{
Success = true,
Message = updated ? "Goal updated successfully." : "No changes were applied."
});
}
[HttpPost("{idStudent:guid}/progress-event")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)]
@@ -411,4 +476,87 @@ public class StudentController : BaseController
Message = "Student deleted."
});
}
[HttpPost("{idStudent:guid}/benchmarks")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult<StudentBenchmarkItem>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ResponseResult<StudentBenchmarkItem>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ResponseResult<StudentBenchmarkItem>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ResponseResult<StudentBenchmarkItem>>> CreateBenchmark(Guid idStudent, [FromBody] CreateBenchmarkDto dto)
{
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
if (error is not null)
{
return error;
}
var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role);
if (!students.Select(s => s.StudentId).Contains(idStudent))
{
return NotFound(new ResponseResult<StudentBenchmarkItem>
{
Success = false,
Message = "Student not found."
});
}
if (!PermissionService.IsAllowed(role, EntityType.Benchmark, PermissionAction.Create, isMine: true))
{
return BadRequest(new ResponseResult<StudentBenchmarkItem>
{
Success = false,
Message = "Unable to create benchmark. - Permission Matrix"
});
}
var created = await _studentRepository.InsertBenchmarkAsync(dto.GoalId, userId, dto);
if (created is null)
{
return BadRequest(new ResponseResult<StudentBenchmarkItem>
{
Success = false,
Message = "Unable to create benchmark."
});
}
return StatusCode(StatusCodes.Status201Created, new ResponseResult<StudentBenchmarkItem>
{
Success = true,
Message = "Benchmark created successfully.",
Data = created
});
}
[HttpPut("{idStudent:guid}/benchmarks/{idBenchmark:guid}")]
[Authorize(Roles = $"{UserRoles.Teacher}")]
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ResponseResult<object>>> UpdateBenchmark(Guid idStudent, Guid idBenchmark, [FromBody] UpdateBenchmarkDto dto)
{
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
if (error is not null)
{
return error;
}
var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role);
if (!students.Select(s => s.StudentId).Contains(idStudent))
{
return NotFound(new ResponseResult<object>
{
Success = false,
Message = "Student not found."
});
}
var updated = await _studentRepository.UpdateBenchmarkAsync(idBenchmark, dto.Benchmark);
return Ok(new ResponseResult<object>
{
Success = true,
Message = updated ? "Changes applied successfully." : "No changes were applied."
});
}
}
@@ -0,0 +1,7 @@
namespace WinStudentGoalTracker.DataAccess;
public class CreateBenchmarkDto
{
public Guid GoalId { get; set; }
public string Benchmark { get; set; } = string.Empty;
}
@@ -0,0 +1,6 @@
namespace WinStudentGoalTracker.DataAccess;
public class UpdateBenchmarkDto
{
public string Benchmark { get; set; } = string.Empty;
}
@@ -0,0 +1,8 @@
namespace WinStudentGoalTracker.DataAccess;
public class UpdateGoalDto
{
public string? Title { get; set; }
public string? Description { get; set; }
public string? Category { get; set; }
}
@@ -0,0 +1,13 @@
namespace WinStudentGoalTracker.DataAccess;
public class dbStudentBenchmarkRow
{
public string? StudentIdentifier { get; set; }
public required Guid BenchmarkId { get; set; }
public required Guid GoalId { get; set; }
public string? GoalTitle { get; set; }
public string? Benchmark { get; set; }
public string? CreatedByName { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
@@ -9,4 +9,5 @@ public class dbStudentGoalRow
public string? Description { get; set; }
public string? Category { get; set; }
public int ProgressEventCount { get; set; }
public int BenchmarkCount { get; set; }
}
@@ -194,9 +194,119 @@ public class StudentRepository
Title = r.Title,
Description = r.Description,
Category = r.Category,
ProgressEventCount = r.ProgressEventCount
ProgressEventCount = r.ProgressEventCount,
BenchmarkCount = r.BenchmarkCount
}).ToList()
};
}
// *****************************************************************
// Updates a goal's title, description, and category.
// *****************************************************************
public async Task<bool> UpdateGoalAsync(Guid goalId, UpdateGoalDto dto)
{
using var db = Connection;
var rowsAffected = await db.ExecuteScalarAsync<int>(
"sp_Goal_Update",
new
{
p_id_goal = goalId.ToString(),
p_id_goal_parent = (string?)null,
p_id_student = (string?)null,
p_id_user_created = (string?)null,
p_title = dto.Title,
p_description = dto.Description,
p_category = dto.Category
},
commandType: CommandType.StoredProcedure);
return rowsAffected > 0;
}
// *****************************************************************
// Returns all benchmarks for a student, grouped under a summary
// with the student identifier. Returns null if student not found.
// *****************************************************************
public async Task<StudentBenchmarkSummary?> GetBenchmarkSummaryAsync(Guid idStudent)
{
using var db = Connection;
var rows = await db.QueryAsync<dbStudentBenchmarkRow>(
"sp_Benchmark_GetByStudentId",
new { p_id_student = idStudent.ToString() },
commandType: CommandType.StoredProcedure);
var list = rows.ToList();
if (list.Count == 0)
{
var student = await GetByIdAsync(idStudent);
if (student is null) return null;
return new StudentBenchmarkSummary
{
StudentIdentifier = student.Identifier,
Benchmarks = []
};
}
return new StudentBenchmarkSummary
{
StudentIdentifier = list[0].StudentIdentifier,
Benchmarks = list.Select(r => new StudentBenchmarkItem
{
BenchmarkId = r.BenchmarkId,
GoalId = r.GoalId,
GoalTitle = r.GoalTitle,
Benchmark = r.Benchmark,
CreatedByName = r.CreatedByName,
CreatedAt = r.CreatedAt,
UpdatedAt = r.UpdatedAt
}).ToList()
};
}
// *****************************************************************
// Inserts a new benchmark and returns the created benchmark item.
// *****************************************************************
public async Task<StudentBenchmarkItem?> InsertBenchmarkAsync(Guid goalId, Guid userId, CreateBenchmarkDto dto)
{
var newId = Guid.NewGuid();
using var db = Connection;
var row = await db.QuerySingleOrDefaultAsync(
"sp_Benchmark_Insert",
new
{
p_id_benchmark = newId.ToString(),
p_id_goal = goalId.ToString(),
p_id_user_created = userId.ToString(),
p_benchmark = dto.Benchmark
},
commandType: CommandType.StoredProcedure);
if (row is null) return null;
return new StudentBenchmarkItem
{
BenchmarkId = newId,
GoalId = goalId,
Benchmark = dto.Benchmark,
CreatedAt = DateTime.UtcNow
};
}
// *****************************************************************
// Updates a benchmark's text and returns whether rows were affected.
// *****************************************************************
public async Task<bool> UpdateBenchmarkAsync(Guid benchmarkId, string benchmarkText)
{
using var db = Connection;
var rowsAffected = await db.ExecuteScalarAsync<int>(
"sp_Benchmark_Update",
new
{
p_id_benchmark = benchmarkId.ToString(),
p_benchmark = benchmarkText
},
commandType: CommandType.StoredProcedure);
return rowsAffected > 0;
}
}
@@ -0,0 +1,12 @@
namespace WinStudentGoalTracker.Models;
public class StudentBenchmarkItem
{
public Guid BenchmarkId { get; set; }
public Guid GoalId { get; set; }
public string? GoalTitle { get; set; }
public string? Benchmark { get; set; }
public string? CreatedByName { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
@@ -0,0 +1,7 @@
namespace WinStudentGoalTracker.Models;
public class StudentBenchmarkSummary
{
public string? StudentIdentifier { get; set; }
public List<StudentBenchmarkItem> Benchmarks { get; set; } = [];
}
@@ -8,4 +8,5 @@ public class StudentGoalItem
public string? Description { get; set; }
public string? Category { get; set; }
public int ProgressEventCount { get; set; }
public int BenchmarkCount { get; set; }
}
+2 -1
View File
@@ -8,10 +8,11 @@ public static class EntityType
public const string Student = "student";
public const string Goal = "goal";
public const string ProgressEvent = "progress_event";
public const string Benchmark = "benchmark";
public static string? TryParse(string value) =>
All.Contains(value) ? value : null;
public static readonly IReadOnlyList<string> All =
[SchoolDistrict, Program, User, Student, Goal, ProgressEvent];
[SchoolDistrict, Program, User, Student, Goal, ProgressEvent, Benchmark];
}
+35
View File
@@ -60,6 +60,13 @@ public static class PermissionMatrix
[PermissionAction.Update] = Allow,
[PermissionAction.Delete] = Allow,
},
[EntityType.Benchmark] = new()
{
[PermissionAction.Create] = Allow,
[PermissionAction.Read] = Allow,
[PermissionAction.Update] = Allow,
[PermissionAction.Delete] = Allow,
},
},
// ──────────────────────────────────────────────
@@ -109,6 +116,13 @@ public static class PermissionMatrix
[PermissionAction.Update] = Allow,
[PermissionAction.Delete] = Allow,
},
[EntityType.Benchmark] = new()
{
[PermissionAction.Create] = Allow,
[PermissionAction.Read] = Allow,
[PermissionAction.Update] = Allow,
[PermissionAction.Delete] = Allow,
},
},
// ──────────────────────────────────────────────
@@ -158,6 +172,13 @@ public static class PermissionMatrix
[PermissionAction.Update] = Allow,
[PermissionAction.Delete] = MineOnly,
},
[EntityType.Benchmark] = new()
{
[PermissionAction.Create] = Allow,
[PermissionAction.Read] = Allow,
[PermissionAction.Update] = Allow,
[PermissionAction.Delete] = MineOnly,
},
},
// ──────────────────────────────────────────────
@@ -207,6 +228,13 @@ public static class PermissionMatrix
[PermissionAction.Update] = MineOnly,
[PermissionAction.Delete] = MineOnly,
},
[EntityType.Benchmark] = new()
{
[PermissionAction.Create] = MineOnly,
[PermissionAction.Read] = MineOnly,
[PermissionAction.Update] = MineOnly,
[PermissionAction.Delete] = MineOnly,
},
},
// ──────────────────────────────────────────────
@@ -256,6 +284,13 @@ public static class PermissionMatrix
[PermissionAction.Update] = MineOnly,
[PermissionAction.Delete] = Deny,
},
[EntityType.Benchmark] = new()
{
[PermissionAction.Create] = Deny,
[PermissionAction.Read] = MineOnly,
[PermissionAction.Update] = Deny,
[PermissionAction.Delete] = Deny,
},
},
};