1c7e704ad3
Implements a flexible validation framework for student rankings, team assignments, and team composition with administrator-configurable rules and thresholds. Validation System: - Reflection-based rule discovery eliminates manual registration - Base classes (RequiredEventTypeRuleBase, EventCountThresholdRuleBase) reduce code duplication by ~250 lines - 12 validation rules covering event requirements, counts, and team constraints - Configurable severity levels (Warning/Error) per rule type - ValidationService with caching for optimal performance - Rules apply contextually (StudentRanking, StudentAssignment, TeamComposition) Configuration & Admin UI: - ValidationSettings admin page for editing thresholds and severity levels - ChapterSettings admin page for editing chapter information - Settings stored in Data/appsettings.json for runtime configuration - JSON config works in both development and production environments - Auto-creates Data directory and config template on first run User Experience: - ValidationWarnings component displays inline warnings/errors - Integrated in Event Ranking, Registration, and Team Assignment pages - Color-coded severity indicators (warning yellow, error red) - Includes "Too Many Regional Events" rule (max 3 recommended) Technical Improvements: - Template Method pattern for rule base classes - Singleton rule instances with lazy initialization - Configuration loaded via IConfiguration with fallback to defaults - Safe JSON updates preserve other appsettings sections
193 lines
6.7 KiB
C#
193 lines
6.7 KiB
C#
using Core.Entities;
|
|
using System.Text.Json;
|
|
|
|
namespace Core.Validation;
|
|
|
|
/// <summary>
|
|
/// Configuration for validation thresholds and rules
|
|
/// </summary>
|
|
public class ValidationConfiguration
|
|
{
|
|
// Event count thresholds
|
|
/// <summary>
|
|
/// Minimum recommended number of events per student (Warning if below)
|
|
/// </summary>
|
|
public int MinRecommendedEvents { get; set; } = 2;
|
|
|
|
/// <summary>
|
|
/// Maximum recommended number of events per student (Warning if above)
|
|
/// </summary>
|
|
public int MaxRecommendedEvents { get; set; } = 4;
|
|
|
|
/// <summary>
|
|
/// Minimum critical number of events per student (Error if below)
|
|
/// </summary>
|
|
public int MinCriticalEvents { get; set; } = 1;
|
|
|
|
/// <summary>
|
|
/// Maximum critical number of events per student (Error if above)
|
|
/// </summary>
|
|
public int MaxCriticalEvents { get; set; } = 6;
|
|
|
|
/// <summary>
|
|
/// Maximum recommended number of regional events per student (Warning if above)
|
|
/// </summary>
|
|
public int MaxRegionalEvents { get; set; } = 3;
|
|
|
|
// Required event types
|
|
/// <summary>
|
|
/// Whether to require students to have at least one regional event
|
|
/// </summary>
|
|
public bool RequireRegionalEvent { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Whether to require students to have at least one on-site activity
|
|
/// </summary>
|
|
public bool RequireOnSiteActivity { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Whether to require students to have at least one individual event
|
|
/// </summary>
|
|
public bool RequireIndividualEvent { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Whether to require team-based events to have an assigned captain
|
|
/// </summary>
|
|
public bool RequireTeamCaptain { get; set; } = true;
|
|
|
|
// Severity levels for each rule type
|
|
/// <summary>
|
|
/// Severity level for "No Regional Event" warnings
|
|
/// </summary>
|
|
public ValidationSeverity NoRegionalEventSeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Severity level for "No On-Site Activity" warnings
|
|
/// </summary>
|
|
public ValidationSeverity NoOnSiteActivitySeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Severity level for "No Individual Event" warnings
|
|
/// </summary>
|
|
public ValidationSeverity NoIndividualEventSeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Severity level for team size warnings
|
|
/// </summary>
|
|
public ValidationSeverity TeamSizeSeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Severity level for event count warnings
|
|
/// </summary>
|
|
public ValidationSeverity EventCountSeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Severity level for missing captain warnings
|
|
/// </summary>
|
|
public ValidationSeverity MissingCaptainSeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Severity level for too many regional events warnings
|
|
/// </summary>
|
|
public ValidationSeverity TooManyRegionalEventsSeverity { get; set; } = ValidationSeverity.Warning;
|
|
|
|
/// <summary>
|
|
/// Default configuration matching current app behavior
|
|
/// </summary>
|
|
public static ValidationConfiguration Default => new()
|
|
{
|
|
RequireRegionalEvent = true,
|
|
RequireOnSiteActivity = true,
|
|
RequireIndividualEvent = false,
|
|
MinRecommendedEvents = 2,
|
|
MaxRecommendedEvents = 4,
|
|
MinCriticalEvents = 1,
|
|
MaxCriticalEvents = 6,
|
|
MaxRegionalEvents = 3,
|
|
RequireTeamCaptain = true,
|
|
NoRegionalEventSeverity = ValidationSeverity.Warning,
|
|
NoOnSiteActivitySeverity = ValidationSeverity.Warning,
|
|
NoIndividualEventSeverity = ValidationSeverity.Warning,
|
|
TeamSizeSeverity = ValidationSeverity.Warning,
|
|
EventCountSeverity = ValidationSeverity.Warning,
|
|
MissingCaptainSeverity = ValidationSeverity.Warning,
|
|
TooManyRegionalEventsSeverity = ValidationSeverity.Warning
|
|
};
|
|
|
|
/// <summary>
|
|
/// Create validation configuration from assignment parameters
|
|
/// </summary>
|
|
/// <param name="parameters">Assignment parameters to convert</param>
|
|
/// <returns>Validation configuration matching the assignment parameters</returns>
|
|
public static ValidationConfiguration FromAssignmentParameters(AssignmentParameters parameters)
|
|
{
|
|
return new ValidationConfiguration
|
|
{
|
|
RequireRegionalEvent = parameters.RequireRegional,
|
|
RequireOnSiteActivity = parameters.RequireOnSite,
|
|
MinRecommendedEvents = parameters.EventsLowerBound,
|
|
MaxRecommendedEvents = parameters.EventsUpperBound,
|
|
MinCriticalEvents = 1,
|
|
MaxCriticalEvents = 6,
|
|
RequireTeamCaptain = true,
|
|
NoRegionalEventSeverity = ValidationSeverity.Warning,
|
|
NoOnSiteActivitySeverity = ValidationSeverity.Warning,
|
|
NoIndividualEventSeverity = ValidationSeverity.Warning,
|
|
TeamSizeSeverity = ValidationSeverity.Warning,
|
|
EventCountSeverity = ValidationSeverity.Warning,
|
|
MissingCaptainSeverity = ValidationSeverity.Warning
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize validation configuration from JSON string
|
|
/// </summary>
|
|
/// <param name="json">JSON string containing configuration</param>
|
|
/// <returns>ValidationConfiguration instance or Default if deserialization fails</returns>
|
|
public static ValidationConfiguration FromJson(string json)
|
|
{
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<ValidationConfiguration>(json) ?? Default;
|
|
}
|
|
catch
|
|
{
|
|
return Default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load validation configuration from a JSON file
|
|
/// </summary>
|
|
/// <param name="path">Path to the JSON configuration file</param>
|
|
/// <returns>ValidationConfiguration instance or Default if file doesn't exist or loading fails</returns>
|
|
public static async Task<ValidationConfiguration> LoadFromFileAsync(string path)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
return Default;
|
|
|
|
var json = await File.ReadAllTextAsync(path);
|
|
return FromJson(json);
|
|
}
|
|
catch
|
|
{
|
|
return Default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save this validation configuration to a JSON file
|
|
/// </summary>
|
|
/// <param name="path">Path where the JSON file should be saved</param>
|
|
public async Task SaveToFileAsync(string path)
|
|
{
|
|
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true
|
|
});
|
|
await File.WriteAllTextAsync(path, json);
|
|
}
|
|
}
|