Compare commits

...
35 Commits
Author SHA1 Message Date
poprhythm 9668ec162d Add Event Calendar feature with event occurrences service integration
Introduced a new Event Calendar component that displays scheduled events using the Heron.MudCalendar. Implemented IEventOccurrenceService to fetch event occurrences, and added mock data for initial testing. Updated navigation menu to include a link to the Event Calendar.
2025-12-27 13:25:17 -05:00
poprhythm 5c1e0b7444 Update padding for MudPaper components across various features
Increased padding from 'pa-4' to 'pa-6' for MudPaper components in Events, MeetingSchedule, Students, and Teams features to ensure consistent styling and improved visual spacing.
2025-12-27 10:43:15 -05:00
poprhythm d19326781a Add loading states to MudDataGrid components
Implemented loading indicators and progress colors for all MudDataGrid instances across Events, Students, Teams, and Registration components to enhance user experience during data loading operations.
2025-12-27 10:39:16 -05:00
poprhythm cb362d6754 Add micro-interactions and transitions for page and table elements
Wrap page content in a new div for animation effects and implement fade-in transitions for table rows.
2025-12-27 10:35:40 -05:00
poprhythm 872a1cae64 Clean up test filenames 2025-12-27 10:29:07 -05:00
poprhythm dbe2b259f6 Clean up test data 2025-12-27 10:25:15 -05:00
poprhythm 58cae98357 Wrap pages in consistent container styling
Remove Back to List
2025-12-26 23:45:32 -05:00
poprhythm 4c05188867 Improve Form Layout 2025-12-26 18:56:24 -05:00
poprhythm 057aa31bf8 Enhance Event Rank Color System 2025-12-26 16:54:16 -05:00
poprhythm ed9487a5ad Consistent Typography in Details pages. 2025-12-26 16:35:09 -05:00
poprhythm 75a64faff0 Improve Table Density and Spacing 2025-12-26 16:02:55 -05:00
poprhythm 768b46da2d Start of some style improvments.
Logout button now matches.
2025-12-26 15:53:48 -05:00
poprhythm e17d15aaff Hover color on the delete buttons 2025-12-26 14:09:49 -05:00
poprhythm f395dba043 Address compiler warnings 2025-12-26 13:58:41 -05:00
poprhythm f2389fa1c1 Moved crud operations to the title field 2025-12-26 10:35:37 -05:00
poprhythm 19a7164b8b Discard in-memory changes when editing 2025-12-26 10:04:37 -05:00
poprhythm 77b5683804 tracks form changes and warns users before navigation 2025-12-25 23:55:04 -05:00
poprhythm 059a16b958 Move action buttons after the forms. 2025-12-25 23:42:18 -05:00
poprhythm 023e6c289a Use PageHeader throughout the system 2025-12-25 21:53:15 -05:00
poprhythm 1e36a5661b PageHeader component introduced. 2025-12-25 20:10:06 -05:00
poprhythm fa13039203 Add class for pre-wrap-text 2025-12-25 20:04:29 -05:00
poprhythm aedf168e8a Update icons for event attributes 2025-12-25 20:03:50 -05:00
poprhythm 1f5aba7f04 Add goals page 2025-12-25 20:03:19 -05:00
poprhythm c77d2117cd Add testing for validation 2025-12-14 11:08:04 -05:00
poprhythmandClaude Sonnet 4.5 73ad730b38 Add comprehensive validation system with runtime configuration
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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 22:15:26 -05:00
poprhythm 1c7e704ad3 Add comprehensive validation system with runtime configuration
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
2025-12-13 22:15:16 -05:00
poprhythm 215b9dccca Don't display captain for individual events 2025-12-12 14:05:29 -05:00
poprhythm b465235096 Add some local storage to save settings between
page reloads.
2025-12-12 12:48:44 -05:00
poprhythm aeafdcee1a Add a student/team list used for registration 2025-12-12 10:35:37 -05:00
poprhythm 27dc995bb8 Improvements for home page and formatting 2025-12-11 14:17:33 -05:00
poprhythm 8289b7139a Improve Event Ranking Editor 2025-12-07 00:00:57 -05:00
poprhythm abd4840ec1 Improve home page layout 2025-12-06 23:47:27 -05:00
poprhythm 5ea3289d39 Fix Logged In message on the navbar 2025-12-06 23:08:47 -05:00
poprhythm 1c5017bf51 Do not allow team assignment when other teams exist already 2025-12-06 23:08:06 -05:00
poprhythm 56dbd549fd Update settings for production deployment 2025-12-05 14:28:23 -05:00
112 changed files with 7287 additions and 1200 deletions
+4
View File
@@ -30,3 +30,7 @@ auth-secrets.json
docker-compose.yml docker-compose.yml
docker-compose.override.yml docker-compose.override.yml
/WebApp/logs/* /WebApp/logs/*
/WebApp/DataProtectionKeys/*
# Runtime data directory
/WebApp/Data/*
@@ -4,7 +4,7 @@ namespace Core.Calculation;
public class EventAssignmentThresholds public class EventAssignmentThresholds
{ {
public EventDefinition Event { get; set; } public EventDefinition Event { get; set; } = null!;
public int TeamCount { get; set; } public int TeamCount { get; set; }
public int LowerBound { get; set; } public int LowerBound { get; set; }
public int UpperBound { get; set; } public int UpperBound { get; set; }
+4 -4
View File
@@ -10,22 +10,22 @@ public class TeamScheduleTimeSlot
/// <summary> /// <summary>
/// Gets or sets the name of this time slot. /// Gets or sets the name of this time slot.
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; } = null!;
/// <summary> /// <summary>
/// Gets or sets the teams scheduled in this time slot. /// Gets or sets the teams scheduled in this time slot.
/// </summary> /// </summary>
public Team[] Teams; public Team[] Teams = null!;
/// <summary> /// <summary>
/// Gets or sets the students who are not scheduled in any team during this time slot. /// Gets or sets the students who are not scheduled in any team during this time slot.
/// </summary> /// </summary>
public Student[] UnscheduledStudents; public Student[] UnscheduledStudents = null!;
/// <summary> /// <summary>
/// Gets or sets the students who have overlapping team meetings in this time slot. /// Gets or sets the students who have overlapping team meetings in this time slot.
/// </summary> /// </summary>
public IEnumerable<(Student student, IEnumerable<Team> teams)> StudentOverlaps; public IEnumerable<(Student student, IEnumerable<Team> teams)> StudentOverlaps = null!;
/// <summary> /// <summary>
/// Checks if a student has overlapping team meetings in this time slot. /// Checks if a student has overlapping team meetings in this time slot.
+2 -2
View File
@@ -9,7 +9,7 @@ public class EventDefinition
[Required] [Required]
[StringLength(100, MinimumLength = 2)] [StringLength(100, MinimumLength = 2)]
[Display(Name = "Event Name")] [Display(Name = "Event Name")]
public string Name { get; set; } public string Name { get; set; } = null!;
[Required] [Required]
[StringLength(40, MinimumLength = 2)] [StringLength(40, MinimumLength = 2)]
@@ -72,7 +72,7 @@ public class EventDefinition
[Required] [Required]
[StringLength(256)] [StringLength(256)]
public string Eligibility { get; set; } public string Eligibility { get; set; } = null!;
[StringLength(4096)] [StringLength(4096)]
public string? Theme { get; set; } public string? Theme { get; set; }
[StringLength(1024)] [StringLength(1024)]
+4 -4
View File
@@ -3,12 +3,12 @@ namespace Core.Entities
{ {
public class EventOccurrence public class EventOccurrence
{ {
public string Name { get; set; } public string Name { get; set; } = null!;
public string Time { get; set; } public string Time { get; set; } = null!;
public string Date { get; set; } public string Date { get; set; } = null!;
public DateTime StartTime { get; set; } public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; } public DateTime? EndTime { get; set; }
public string Location { get; set; } public string Location { get; set; } = null!;
public bool SignupSubmitPickup => public bool SignupSubmitPickup =>
Name.Contains("Sign-up") || Name.Contains("Sign-up") ||
+1 -1
View File
@@ -2,7 +2,7 @@
public class PartialTeam : Team public class PartialTeam : Team
{ {
public IList<Student> OmittedStudents { get; set; } public IList<Student> OmittedStudents { get; set; } = null!;
public override Team CloneWithOmittedStudents(IEnumerable<Student> studentsToOmit) public override Team CloneWithOmittedStudents(IEnumerable<Student> studentsToOmit)
{ {
+3 -3
View File
@@ -10,12 +10,12 @@ public class Student : IEquatable<Student>
[Required] [Required]
[StringLength(50,MinimumLength = 2)] [StringLength(50,MinimumLength = 2)]
[Display(Name = "First Name")] [Display(Name = "First Name")]
public string FirstName { get; set; } public string FirstName { get; set; } = null!;
[Required] [Required]
[StringLength(50, MinimumLength = 2)] [StringLength(50, MinimumLength = 2)]
[Display(Name = "Last Name")] [Display(Name = "Last Name")]
public string LastName { get; set; } public string LastName { get; set; } = null!;
[Range(5,12)] [Range(5,12)]
[Display(Name = "Grade")] [Display(Name = "Grade")]
@@ -44,7 +44,7 @@ public class Student : IEquatable<Student>
[Display(Name = "Officer Role")] [Display(Name = "Officer Role")]
public OfficerRole? OfficerRole { get; set; } public OfficerRole? OfficerRole { get; set; }
public List<Team> Teams { get; set; } = null; public List<Team> Teams { get; set; } = null!;
public List<EventDefinition> RankedEvents { get; } = []; public List<EventDefinition> RankedEvents { get; } = [];
public List<StudentEventRanking> EventRankings { get; } = []; public List<StudentEventRanking> EventRankings { get; } = [];
+1 -1
View File
@@ -2,7 +2,7 @@
public class StudentEventStatistics public class StudentEventStatistics
{ {
public Student Student { get; set; } public Student Student { get; set; } = null!;
public List<EventDefinition> Events { get; set; } = []; public List<EventDefinition> Events { get; set; } = [];
public int? TotalLevelOfEffort => Events.Sum(e => e.LevelOfEffort); public int? TotalLevelOfEffort => Events.Sum(e => e.LevelOfEffort);
+1 -1
View File
@@ -6,7 +6,7 @@ public class Team
public int Id { get; set; } public int Id { get; set; }
[Required] [Required]
public EventDefinition Event { get; set; } public EventDefinition Event { get; set; } = null!;
public List<Student> Students { get; set; } = []; public List<Student> Students { get; set; } = [];
+9 -5
View File
@@ -15,7 +15,7 @@ public class AssignmentRequirementParser : CsvParserBase
CsvReader.Read(); CsvReader.Read();
CsvReader.ReadHeader(); CsvReader.ReadHeader();
var studentColumns = var studentColumns =
CsvReader.HeaderRecord.Select(h => h.Trim()).Where(h => !string.IsNullOrEmpty(h)).ToArray(); CsvReader.HeaderRecord!.Select(h => h.Trim()).Where(h => !string.IsNullOrEmpty(h)).ToArray();
var studentArray = var studentArray =
studentColumns studentColumns
@@ -28,23 +28,27 @@ public class AssignmentRequirementParser : CsvParserBase
var evt = events.FirstOrDefault(e => e.ShortName == eventShortName); var evt = events.FirstOrDefault(e => e.ShortName == eventShortName);
if (evt == null) if (evt == null)
throw new Exception($"Could not find eventDefinition named {eventShortName}"); throw new Exception($"Could not find eventDefinition named {eventShortName}");
for (int i = 0; i <= studentArray.Length; i++) for (int i = 0; i < studentArray.Length; i++)
{ {
var student = studentArray[i];
if (student == null)
continue;
var field = CsvReader.GetField(i + 1); var field = CsvReader.GetField(i + 1);
switch (field) switch (field)
{ {
case "x": case "x":
case "X": case "X":
assumptions.Add(new AssignmentRequirement(evt, studentArray[i], Requirement.Exclude)); assumptions.Add(new AssignmentRequirement(evt, student, Requirement.Exclude));
break; break;
case "i": case "i":
case "I": case "I":
assumptions.Add(new AssignmentRequirement(evt, studentArray[i], Requirement.Include)); assumptions.Add(new AssignmentRequirement(evt, student, Requirement.Include));
break; break;
default: default:
break; break;
} }
} }
} }
+4 -4
View File
@@ -53,8 +53,8 @@ public class EventDefinitionParser : CsvParserBase
var competitiveEvent = new EventDefinition var competitiveEvent = new EventDefinition
{ {
Name = name.Trim(), Name = name.Trim(),
ShortName = shortName.Trim(), ShortName = shortName?.Trim() ?? string.Empty,
EventFormat = format, EventFormat = format,
ChapterEligibilityCountState = stateTeams, ChapterEligibilityCountState = stateTeams,
MinTeamSize = min, MinTeamSize = min,
@@ -64,10 +64,10 @@ public class EventDefinitionParser : CsvParserBase
//RegionalPresubmit = regionalPresubmit.Trim() == "TRUE", //RegionalPresubmit = regionalPresubmit.Trim() == "TRUE",
Notes = regionalNotes, Notes = regionalNotes,
Documentation= documentation, Documentation= documentation,
Presubmission = statePresubmission.Trim() == "TRUE", Presubmission = statePresubmission?.Trim() == "TRUE",
//StatePretesting = statePretesting.Trim() == "TRUE", //StatePretesting = statePretesting.Trim() == "TRUE",
//StatePreliminaryRound = statePreliminary.Trim() == "TRUE", //StatePreliminaryRound = statePreliminary.Trim() == "TRUE",
Eligibility = eligibility, Eligibility = eligibility ?? string.Empty,
Theme = theme, Theme = theme,
Description = description, Description = description,
LevelOfEffort = levelOfEffort LevelOfEffort = levelOfEffort
+1 -1
View File
@@ -31,7 +31,7 @@ public class EventOccurrenceParser
public IDictionary<EventDefinition, List<EventOccurrence>> Parse() public IDictionary<EventDefinition, List<EventOccurrence>> Parse()
{ {
var occurrences = new Dictionary<EventDefinition, List<EventOccurrence>>(); var occurrences = new Dictionary<EventDefinition, List<EventOccurrence>>();
EventDefinition currentEventDefinition = null; EventDefinition? currentEventDefinition = null;
var lines = File.ReadLines(_txtFile.FullName); var lines = File.ReadLines(_txtFile.FullName);
foreach (var line in lines) foreach (var line in lines)
+23
View File
@@ -0,0 +1,23 @@
namespace Core.Validation;
/// <summary>
/// Interface for all validation rules
/// </summary>
/// <typeparam name="TEntity">Type of entity being validated (Student, Team, StudentEventStatistics, etc.)</typeparam>
public interface IValidationRule<TEntity>
{
/// <summary>
/// Execute the validation rule against an entity
/// </summary>
/// <param name="entity">Entity to validate</param>
/// <param name="config">Validation configuration</param>
/// <returns>Validation warning if rule is violated, null otherwise</returns>
ValidationWarning? Validate(TEntity entity, ValidationConfiguration config);
/// <summary>
/// Determines if this rule applies to the given context
/// </summary>
/// <param name="context">Validation context</param>
/// <returns>True if the rule should be executed in this context</returns>
bool AppliesTo(ValidationContext context);
}
@@ -0,0 +1,88 @@
using Core.Entities;
namespace Core.Validation.Rules.BaseRules;
/// <summary>
/// Base class for validation rules that check event counts against thresholds for student rankings
/// Similar to EventCountThresholdRuleBase but works with Student entities instead of StudentEventStatistics
/// </summary>
public abstract class EventCountThresholdRankingRuleBase : IValidationRule<Student>
{
/// <summary>
/// Get the actual count to validate
/// </summary>
protected abstract int GetCount(Student student);
/// <summary>
/// Get the threshold value from configuration
/// </summary>
protected abstract int GetThreshold(ValidationConfiguration config);
/// <summary>
/// Check if the count violates the threshold
/// </summary>
protected abstract bool ViolatesThreshold(int count, int threshold);
/// <summary>
/// Get the severity from configuration
/// </summary>
protected abstract ValidationSeverity GetSeverity(ValidationConfiguration config);
/// <summary>
/// The validation warning code
/// </summary>
protected abstract string Code { get; }
/// <summary>
/// Build the display message
/// </summary>
protected abstract string GetMessage(int count, int threshold);
/// <summary>
/// Icon identifier for the warning (can be null)
/// </summary>
protected virtual string? IconIdentifier => null;
/// <summary>
/// Build additional metadata beyond the standard fields
/// </summary>
protected virtual Dictionary<string, object> BuildAdditionalMetadata(Student student, ValidationConfiguration config)
{
return new Dictionary<string, object>();
}
public ValidationWarning? Validate(Student entity, ValidationConfiguration config)
{
var count = GetCount(entity);
var threshold = GetThreshold(config);
if (!ViolatesThreshold(count, threshold))
return null;
var metadata = new Dictionary<string, object>
{
{ "StudentId", entity.Id },
{ "StudentName", entity.FirstNameLastName }
};
// Add any additional metadata from derived class
foreach (var kvp in BuildAdditionalMetadata(entity, config))
{
metadata[kvp.Key] = kvp.Value;
}
return new ValidationWarning
{
Code = Code,
Message = GetMessage(count, threshold),
Severity = GetSeverity(config),
Context = ValidationContext.StudentRanking,
IconIdentifier = IconIdentifier,
Metadata = metadata
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.StudentRanking ||
context == ValidationContext.StudentRegistration;
}
@@ -0,0 +1,108 @@
using Core.Entities;
namespace Core.Validation.Rules.BaseRules;
/// <summary>
/// Base class for validation rules that check event counts against thresholds
/// Eliminates duplication across TooManyEvents, TooFewEvents, and TooManyRegionalEvents rules
/// </summary>
public abstract class EventCountThresholdRuleBase : IValidationRule<StudentEventStatistics>
{
/// <summary>
/// Get the actual count to validate
/// </summary>
protected abstract int GetCount(StudentEventStatistics statistics);
/// <summary>
/// Get the threshold value from configuration
/// </summary>
protected abstract int GetThreshold(ValidationConfiguration config);
/// <summary>
/// Check if the count violates the threshold
/// </summary>
protected abstract bool ViolatesThreshold(int count, int threshold);
/// <summary>
/// Get the base severity from configuration (can be overridden for critical thresholds)
/// </summary>
protected abstract ValidationSeverity GetBaseSeverity(ValidationConfiguration config);
/// <summary>
/// Optionally check for critical threshold that escalates to Error severity
/// Returns null if no critical threshold applies
/// </summary>
protected virtual int? GetCriticalThreshold(ValidationConfiguration config) => null;
/// <summary>
/// Check if count violates critical threshold (for escalation to Error)
/// </summary>
protected virtual bool ViolatesCriticalThreshold(int count, int criticalThreshold) => false;
/// <summary>
/// The validation warning code
/// </summary>
protected abstract string Code { get; }
/// <summary>
/// Build the display message
/// </summary>
protected abstract string GetMessage(int count, int threshold);
/// <summary>
/// Icon identifier for the warning (can be null)
/// </summary>
protected virtual string? IconIdentifier => null;
/// <summary>
/// Build additional metadata beyond the standard fields
/// </summary>
protected virtual Dictionary<string, object> BuildAdditionalMetadata(StudentEventStatistics statistics, ValidationConfiguration config)
{
return new Dictionary<string, object>();
}
public ValidationWarning? Validate(StudentEventStatistics entity, ValidationConfiguration config)
{
var count = GetCount(entity);
var threshold = GetThreshold(config);
if (!ViolatesThreshold(count, threshold))
return null;
// Check for critical threshold escalation
var severity = GetBaseSeverity(config);
var criticalThreshold = GetCriticalThreshold(config);
if (criticalThreshold.HasValue && ViolatesCriticalThreshold(count, criticalThreshold.Value))
{
severity = ValidationSeverity.Error;
}
var metadata = new Dictionary<string, object>
{
{ "StudentId", entity.Student.Id },
{ "StudentName", entity.Student.FirstNameLastName },
{ "EventCount", entity.EventCount }
};
// Add any additional metadata from derived class
foreach (var kvp in BuildAdditionalMetadata(entity, config))
{
metadata[kvp.Key] = kvp.Value;
}
return new ValidationWarning
{
Code = Code,
Message = GetMessage(count, threshold),
Severity = severity,
Context = ValidationContext.StudentAssignment,
IconIdentifier = IconIdentifier,
Metadata = metadata
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.StudentAssignment ||
context == ValidationContext.StudentRegistration;
}
@@ -0,0 +1,68 @@
using Core.Entities;
namespace Core.Validation.Rules.BaseRules;
/// <summary>
/// Base class for validation rules that check if a student has been assigned a required event type
/// Eliminates duplication across assignment validation rules
/// </summary>
public abstract class RequiredEventTypeAssignmentRuleBase : IValidationRule<StudentEventStatistics>
{
/// <summary>
/// Check if this event type is required based on configuration
/// </summary>
protected abstract bool IsRequired(ValidationConfiguration config);
/// <summary>
/// Check if the student has been assigned this event type
/// </summary>
protected abstract bool HasEventType(StudentEventStatistics statistics);
/// <summary>
/// Get the severity level for this rule from configuration
/// </summary>
protected abstract ValidationSeverity GetSeverity(ValidationConfiguration config);
/// <summary>
/// The validation warning code (e.g., "NO_REGIONAL_EVENT_ASSIGNED")
/// </summary>
protected abstract string Code { get; }
/// <summary>
/// The display message for the warning
/// </summary>
protected abstract string Message { get; }
/// <summary>
/// Icon identifier for the warning (e.g., "RegionalEvent")
/// </summary>
protected abstract string IconIdentifier { get; }
public ValidationWarning? Validate(StudentEventStatistics entity, ValidationConfiguration config)
{
if (!IsRequired(config))
return null;
if (HasEventType(entity))
return null;
return new ValidationWarning
{
Code = Code,
Message = Message,
Severity = GetSeverity(config),
Context = ValidationContext.StudentAssignment,
IconIdentifier = IconIdentifier,
Metadata = new Dictionary<string, object>
{
{ "StudentId", entity.Student.Id },
{ "StudentName", entity.Student.FirstNameLastName },
{ "EventCount", entity.EventCount }
}
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.StudentAssignment ||
context == ValidationContext.StudentRegistration;
}
@@ -0,0 +1,67 @@
using Core.Entities;
namespace Core.Validation.Rules.BaseRules;
/// <summary>
/// Base class for validation rules that check if a student has ranked a required event type
/// Eliminates duplication across NoRegionalEvent, NoOnSiteActivity, and NoIndividualEvent rules
/// </summary>
public abstract class RequiredEventTypeRuleBase : IValidationRule<Student>
{
/// <summary>
/// Check if this event type is required based on configuration
/// </summary>
protected abstract bool IsRequired(ValidationConfiguration config);
/// <summary>
/// Check if the student has ranked this event type
/// </summary>
protected abstract bool HasEventType(Student student);
/// <summary>
/// Get the severity level for this rule from configuration
/// </summary>
protected abstract ValidationSeverity GetSeverity(ValidationConfiguration config);
/// <summary>
/// The validation warning code (e.g., "NO_REGIONAL_EVENT")
/// </summary>
protected abstract string Code { get; }
/// <summary>
/// The display message for the warning
/// </summary>
protected abstract string Message { get; }
/// <summary>
/// Icon identifier for the warning (e.g., "RegionalEvent")
/// </summary>
protected abstract string IconIdentifier { get; }
public ValidationWarning? Validate(Student entity, ValidationConfiguration config)
{
if (!IsRequired(config))
return null;
if (HasEventType(entity))
return null;
return new ValidationWarning
{
Code = Code,
Message = Message,
Severity = GetSeverity(config),
Context = ValidationContext.StudentRanking,
IconIdentifier = IconIdentifier,
Metadata = new Dictionary<string, object>
{
{ "StudentId", entity.Id },
{ "StudentName", entity.FirstNameLastName }
}
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.StudentRanking ||
context == ValidationContext.StudentRegistration;
}
@@ -0,0 +1,22 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentAssignmentRules;
/// <summary>
/// Validation rule that checks if a student has been assigned at least one on-site activity
/// </summary>
public class NoOnSiteActivityAssignmentRule : RequiredEventTypeAssignmentRuleBase
{
protected override bool IsRequired(ValidationConfiguration config) => config.RequireOnSiteActivity;
protected override bool HasEventType(StudentEventStatistics statistics) => statistics.HasOnSiteActivity;
protected override ValidationSeverity GetSeverity(ValidationConfiguration config) => config.NoOnSiteActivitySeverity;
protected override string Code => "NO_ONSITE_ACTIVITY_ASSIGNED";
protected override string Message => "No On-Site Activity";
protected override string IconIdentifier => "OnSiteActivity";
}
@@ -0,0 +1,22 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentAssignmentRules;
/// <summary>
/// Validation rule that checks if a student has been assigned at least one regional event
/// </summary>
public class NoRegionalEventAssignmentRule : RequiredEventTypeAssignmentRuleBase
{
protected override bool IsRequired(ValidationConfiguration config) => config.RequireRegionalEvent;
protected override bool HasEventType(StudentEventStatistics statistics) => statistics.HasRegionalEvent;
protected override ValidationSeverity GetSeverity(ValidationConfiguration config) => config.NoRegionalEventSeverity;
protected override string Code => "NO_REGIONAL_EVENT_ASSIGNED";
protected override string Message => "No Regional Event";
protected override string IconIdentifier => "RegionalEvent";
}
@@ -0,0 +1,36 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentAssignmentRules;
/// <summary>
/// Validation rule that checks if a student has too few event assignments
/// </summary>
public class TooFewEventsRule : EventCountThresholdRuleBase
{
protected override int GetCount(StudentEventStatistics statistics) => statistics.EventCount;
protected override int GetThreshold(ValidationConfiguration config) => config.MinRecommendedEvents;
protected override bool ViolatesThreshold(int count, int threshold) => count < threshold;
protected override ValidationSeverity GetBaseSeverity(ValidationConfiguration config) => config.EventCountSeverity;
protected override int? GetCriticalThreshold(ValidationConfiguration config) => config.MinCriticalEvents;
protected override bool ViolatesCriticalThreshold(int count, int criticalThreshold) => count < criticalThreshold;
protected override string Code => "TOO_FEW_EVENTS";
protected override string GetMessage(int count, int threshold) =>
$"Student has {count} events (min recommended: {threshold})";
protected override Dictionary<string, object> BuildAdditionalMetadata(StudentEventStatistics statistics, ValidationConfiguration config)
{
return new Dictionary<string, object>
{
{ "MinRecommended", config.MinRecommendedEvents },
{ "MinCritical", config.MinCriticalEvents }
};
}
}
@@ -0,0 +1,36 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentAssignmentRules;
/// <summary>
/// Validation rule that checks if a student has too many event assignments
/// </summary>
public class TooManyEventsRule : EventCountThresholdRuleBase
{
protected override int GetCount(StudentEventStatistics statistics) => statistics.EventCount;
protected override int GetThreshold(ValidationConfiguration config) => config.MaxRecommendedEvents;
protected override bool ViolatesThreshold(int count, int threshold) => count > threshold;
protected override ValidationSeverity GetBaseSeverity(ValidationConfiguration config) => config.EventCountSeverity;
protected override int? GetCriticalThreshold(ValidationConfiguration config) => config.MaxCriticalEvents;
protected override bool ViolatesCriticalThreshold(int count, int criticalThreshold) => count > criticalThreshold;
protected override string Code => "TOO_MANY_EVENTS";
protected override string GetMessage(int count, int threshold) =>
$"Student has {count} events (max recommended: {threshold})";
protected override Dictionary<string, object> BuildAdditionalMetadata(StudentEventStatistics statistics, ValidationConfiguration config)
{
return new Dictionary<string, object>
{
{ "MaxRecommended", config.MaxRecommendedEvents },
{ "MaxCritical", config.MaxCriticalEvents }
};
}
}
@@ -0,0 +1,36 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentAssignmentRules;
/// <summary>
/// Validation rule that checks if a student has too many regional events in their assignments
/// </summary>
public class TooManyRegionalEventsAssignmentRule : EventCountThresholdRuleBase
{
protected override int GetCount(StudentEventStatistics statistics) =>
statistics.Events.Count(e => e.RegionalEvent);
protected override int GetThreshold(ValidationConfiguration config) => config.MaxRegionalEvents;
protected override bool ViolatesThreshold(int count, int threshold) => count > threshold;
protected override ValidationSeverity GetBaseSeverity(ValidationConfiguration config) =>
config.TooManyRegionalEventsSeverity;
protected override string Code => "TOO_MANY_REGIONAL_EVENTS";
protected override string GetMessage(int count, int threshold) =>
$"Student has {count} regional events (max recommended: {threshold})";
protected override string? IconIdentifier => "RegionalEvent";
protected override Dictionary<string, object> BuildAdditionalMetadata(StudentEventStatistics statistics, ValidationConfiguration config)
{
return new Dictionary<string, object>
{
{ "RegionalEventCount", GetCount(statistics) },
{ "MaxRegionalEvents", config.MaxRegionalEvents }
};
}
}
@@ -0,0 +1,22 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentRankingRules;
/// <summary>
/// Validation rule that checks if a student has ranked at least one individual event
/// </summary>
public class NoIndividualEventRule : RequiredEventTypeRuleBase
{
protected override bool IsRequired(ValidationConfiguration config) => config.RequireIndividualEvent;
protected override bool HasEventType(Student student) => student.RankedEvents.Any(e => e.EventFormat == EventFormat.Individual);
protected override ValidationSeverity GetSeverity(ValidationConfiguration config) => config.NoIndividualEventSeverity;
protected override string Code => "NO_INDIVIDUAL_EVENT";
protected override string Message => "No Individual Event";
protected override string IconIdentifier => "IndividualEvent";
}
@@ -0,0 +1,22 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentRankingRules;
/// <summary>
/// Validation rule that checks if a student has ranked at least one on-site activity
/// </summary>
public class NoOnSiteActivityRule : RequiredEventTypeRuleBase
{
protected override bool IsRequired(ValidationConfiguration config) => config.RequireOnSiteActivity;
protected override bool HasEventType(Student student) => student.RankedEvents.Any(e => e.OnSiteActivity);
protected override ValidationSeverity GetSeverity(ValidationConfiguration config) => config.NoOnSiteActivitySeverity;
protected override string Code => "NO_ONSITE_ACTIVITY";
protected override string Message => "No On-Site Activity";
protected override string IconIdentifier => "OnSiteActivity";
}
@@ -0,0 +1,22 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentRankingRules;
/// <summary>
/// Validation rule that checks if a student has ranked at least one regional event
/// </summary>
public class NoRegionalEventRule : RequiredEventTypeRuleBase
{
protected override bool IsRequired(ValidationConfiguration config) => config.RequireRegionalEvent;
protected override bool HasEventType(Student student) => student.RankedEvents.Any(e => e.RegionalEvent);
protected override ValidationSeverity GetSeverity(ValidationConfiguration config) => config.NoRegionalEventSeverity;
protected override string Code => "NO_REGIONAL_EVENT";
protected override string Message => "No Regional Event";
protected override string IconIdentifier => "RegionalEvent";
}
@@ -0,0 +1,36 @@
using Core.Entities;
using Core.Validation.Rules.BaseRules;
namespace Core.Validation.Rules.StudentRankingRules;
/// <summary>
/// Validation rule that checks if a student has too many regional events in their rankings
/// </summary>
public class TooManyRegionalEventsRule : EventCountThresholdRankingRuleBase
{
protected override int GetCount(Student student) =>
student.RankedEvents.Count(e => e.RegionalEvent);
protected override int GetThreshold(ValidationConfiguration config) => config.MaxRegionalEvents;
protected override bool ViolatesThreshold(int count, int threshold) => count > threshold;
protected override ValidationSeverity GetSeverity(ValidationConfiguration config) =>
config.TooManyRegionalEventsSeverity;
protected override string Code => "TOO_MANY_REGIONAL_EVENTS";
protected override string GetMessage(int count, int threshold) =>
$"Student has {count} regional events (max recommended: {threshold})";
protected override string? IconIdentifier => "RegionalEvent";
protected override Dictionary<string, object> BuildAdditionalMetadata(Student student, ValidationConfiguration config)
{
return new Dictionary<string, object>
{
{ "RegionalEventCount", GetCount(student) },
{ "MaxRegionalEvents", config.MaxRegionalEvents }
};
}
}
@@ -0,0 +1,40 @@
using Core.Entities;
namespace Core.Validation.Rules.TeamRules;
/// <summary>
/// Validation rule that checks if a team-based event has an assigned captain
/// </summary>
public class MissingCaptainRule : IValidationRule<Team>
{
public ValidationWarning? Validate(Team entity, ValidationConfiguration config)
{
if (!config.RequireTeamCaptain)
return null;
// Individual events don't need captains
if (entity.Event.EventFormat == EventFormat.Individual)
return null;
if (entity.Captain != null)
return null;
return new ValidationWarning
{
Code = "MISSING_CAPTAIN",
Message = "Team has no captain assigned",
Severity = config.MissingCaptainSeverity,
Context = ValidationContext.Team,
IconIdentifier = "Captain",
Metadata = new Dictionary<string, object>
{
{ "TeamId", entity.Id },
{ "EventName", entity.Event.Name },
{ "TeamSize", entity.Students.Count }
}
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.Team;
}
@@ -0,0 +1,42 @@
using Core.Entities;
namespace Core.Validation.Rules.TeamRules;
/// <summary>
/// Validation rule that checks if a team has more members than the maximum allowed
/// </summary>
public class TeamSizeTooLargeRule : IValidationRule<Team>
{
public ValidationWarning? Validate(Team entity, ValidationConfiguration config)
{
// Individual events don't have team size requirements
if (entity.Event.EventFormat == EventFormat.Individual)
return null;
var actualSize = entity.Students.Count;
var maxSize = entity.Event.MaxTeamSize;
if (actualSize <= maxSize)
return null;
return new ValidationWarning
{
Code = "TEAM_SIZE_TOO_LARGE",
Message = $"Team has {actualSize} members (max: {maxSize})",
Severity = ValidationSeverity.Error, // Always error - hard constraint
Context = ValidationContext.Team,
IconIdentifier = null,
Metadata = new Dictionary<string, object>
{
{ "TeamId", entity.Id },
{ "EventName", entity.Event.Name },
{ "ActualSize", actualSize },
{ "MinSize", entity.Event.MinTeamSize },
{ "MaxSize", maxSize }
}
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.Team;
}
@@ -0,0 +1,42 @@
using Core.Entities;
namespace Core.Validation.Rules.TeamRules;
/// <summary>
/// Validation rule that checks if a team has fewer members than the minimum required
/// </summary>
public class TeamSizeTooSmallRule : IValidationRule<Team>
{
public ValidationWarning? Validate(Team entity, ValidationConfiguration config)
{
// Individual events don't have team size requirements
if (entity.Event.EventFormat == EventFormat.Individual)
return null;
var actualSize = entity.Students.Count;
var minSize = entity.Event.MinTeamSize;
if (actualSize >= minSize)
return null;
return new ValidationWarning
{
Code = "TEAM_SIZE_TOO_SMALL",
Message = $"Team has {actualSize} members (min: {minSize})",
Severity = config.TeamSizeSeverity,
Context = ValidationContext.Team,
IconIdentifier = null,
Metadata = new Dictionary<string, object>
{
{ "TeamId", entity.Id },
{ "EventName", entity.Event.Name },
{ "ActualSize", actualSize },
{ "MinSize", minSize },
{ "MaxSize", entity.Event.MaxTeamSize }
}
};
}
public bool AppliesTo(ValidationContext context) =>
context == ValidationContext.Team;
}
+199
View File
@@ -0,0 +1,199 @@
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
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
return JsonSerializer.Deserialize<ValidationConfiguration>(json, options) ?? 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 options = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
var json = JsonSerializer.Serialize(this, options);
await File.WriteAllTextAsync(path, json);
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace Core.Validation;
/// <summary>
/// Context in which validation is being performed
/// </summary>
public enum ValidationContext
{
/// <summary>
/// Validating a single student's event rankings
/// </summary>
StudentRanking = 0,
/// <summary>
/// Validating a single team configuration
/// </summary>
Team = 1,
/// <summary>
/// Validating a student's assigned events
/// </summary>
StudentAssignment = 2,
/// <summary>
/// Validating student registration data
/// </summary>
StudentRegistration = 3
}
+130
View File
@@ -0,0 +1,130 @@
using Core.Entities;
using System.Reflection;
namespace Core.Validation;
/// <summary>
/// Main service for executing validation rules and generating warnings
/// </summary>
public class ValidationService
{
private readonly ValidationConfiguration _config;
private readonly List<IValidationRule<Student>> _studentRules;
private readonly List<IValidationRule<Team>> _teamRules;
private readonly List<IValidationRule<StudentEventStatistics>> _statisticsRules;
// Lazy static singletons for rule definitions (instantiated once per application lifetime)
private static readonly Lazy<List<IValidationRule<Student>>> _studentRuleDefinitions =
new(() => DiscoverRules<Student>());
private static readonly Lazy<List<IValidationRule<Team>>> _teamRuleDefinitions =
new(() => DiscoverRules<Team>());
private static readonly Lazy<List<IValidationRule<StudentEventStatistics>>> _statisticsRuleDefinitions =
new(() => DiscoverRules<StudentEventStatistics>());
/// <summary>
/// Create a new validation service with the specified configuration
/// </summary>
/// <param name="config">Validation configuration (uses Default if null)</param>
public ValidationService(ValidationConfiguration? config = null)
{
_config = config ?? ValidationConfiguration.Default;
// Use the shared rule definitions (singleton pattern)
_studentRules = _studentRuleDefinitions.Value;
_teamRules = _teamRuleDefinitions.Value;
_statisticsRules = _statisticsRuleDefinitions.Value;
}
/// <summary>
/// Discover all validation rules for a given entity type using reflection
/// </summary>
/// <typeparam name="TEntity">The entity type to find rules for</typeparam>
/// <returns>List of discovered validation rules</returns>
private static List<IValidationRule<TEntity>> DiscoverRules<TEntity>()
{
var ruleType = typeof(IValidationRule<TEntity>);
return Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && ruleType.IsAssignableFrom(t))
.Select(t => (IValidationRule<TEntity>)Activator.CreateInstance(t)!)
.ToList();
}
/// <summary>
/// Validate a student's event rankings
/// </summary>
/// <param name="student">Student to validate</param>
/// <param name="context">Validation context</param>
/// <returns>List of validation warnings</returns>
public List<ValidationWarning> ValidateStudentRankings(Student student, ValidationContext context)
{
return _studentRules
.Where(rule => rule.AppliesTo(context))
.Select(rule => rule.Validate(student, _config))
.Where(warning => warning != null)
.Cast<ValidationWarning>()
.ToList();
}
/// <summary>
/// Validate a team configuration
/// </summary>
/// <param name="team">Team to validate</param>
/// <param name="context">Validation context (defaults to Team)</param>
/// <returns>List of validation warnings</returns>
public List<ValidationWarning> ValidateTeam(Team team, ValidationContext context = ValidationContext.Team)
{
return _teamRules
.Where(rule => rule.AppliesTo(context))
.Select(rule => rule.Validate(team, _config))
.Where(warning => warning != null)
.Cast<ValidationWarning>()
.ToList();
}
/// <summary>
/// Validate student event assignment statistics
/// </summary>
/// <param name="stats">Student statistics to validate</param>
/// <param name="context">Validation context</param>
/// <returns>List of validation warnings</returns>
public List<ValidationWarning> ValidateStudentStatistics(StudentEventStatistics stats, ValidationContext context)
{
return _statisticsRules
.Where(rule => rule.AppliesTo(context))
.Select(rule => rule.Validate(stats, _config))
.Where(warning => warning != null)
.Cast<ValidationWarning>()
.ToList();
}
/// <summary>
/// Validate all students in a collection
/// </summary>
/// <param name="students">Students to validate</param>
/// <param name="context">Validation context</param>
/// <returns>Dictionary mapping each student to their warnings</returns>
public Dictionary<Student, List<ValidationWarning>> ValidateStudents(
IEnumerable<Student> students,
ValidationContext context)
{
return students.ToDictionary(
student => student,
student => ValidateStudentRankings(student, context)
);
}
/// <summary>
/// Validate all teams in a collection
/// </summary>
/// <param name="teams">Teams to validate</param>
/// <returns>Dictionary mapping each team to their warnings</returns>
public Dictionary<Team, List<ValidationWarning>> ValidateTeams(IEnumerable<Team> teams)
{
return teams.ToDictionary(
team => team,
team => ValidateTeam(team)
);
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Core.Validation;
/// <summary>
/// Severity level for validation warnings
/// </summary>
public enum ValidationSeverity
{
/// <summary>
/// Warning that should be addressed but doesn't prevent operation
/// </summary>
Warning = 0,
/// <summary>
/// Error that indicates a critical issue
/// </summary>
Error = 1
}
+37
View File
@@ -0,0 +1,37 @@
namespace Core.Validation;
/// <summary>
/// Represents a validation warning or error for a student, team, or assignment
/// </summary>
public class ValidationWarning
{
/// <summary>
/// Human-readable warning message
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Unique code identifying the validation rule (e.g., "NO_REGIONAL_EVENT")
/// </summary>
public required string Code { get; init; }
/// <summary>
/// Severity level of the warning
/// </summary>
public required ValidationSeverity Severity { get; init; }
/// <summary>
/// Context in which this warning applies
/// </summary>
public required ValidationContext Context { get; init; }
/// <summary>
/// Icon identifier for UI display (maps to AppIcons constants in WebApp)
/// </summary>
public string? IconIdentifier { get; init; }
/// <summary>
/// Additional contextual information about the warning
/// </summary>
public Dictionary<string, object> Metadata { get; init; } = new();
}
+1
View File
@@ -147,6 +147,7 @@ public class StudentBuilder
Rank = rank Rank = rank
}; };
student.EventRankings.Add(ranking); student.EventRankings.Add(ranking);
student.RankedEvents.Add(eventDef); // Also add to RankedEvents for validation rules
} }
return student; return student;
+3 -1
View File
@@ -21,10 +21,12 @@ public class TeamBuilder
return this; return this;
} }
public TeamBuilder WithStudent(Student student) public TeamBuilder WithStudent(Student student, bool isCaptain = false)
{ {
if (!_students.Contains(student)) if (!_students.Contains(student))
_students.Add(student); _students.Add(student);
if (isCaptain)
_captain = student;
return this; return this;
} }
+5 -5
View File
@@ -11,7 +11,7 @@ public static class TestEntityHandler
public static EventDefinition[] GetEvents() public static EventDefinition[] GetEvents()
{ {
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2023-24 RMS TSA student & event - Event Definitions.csv"); var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024 Event Definitions.csv");
var eventRankingsParser = new EventDefinitionParser(fileInfo); var eventRankingsParser = new EventDefinitionParser(fileInfo);
return eventRankingsParser.Parse(); return eventRankingsParser.Parse();
} }
@@ -24,7 +24,7 @@ public static class TestEntityHandler
public static Student[] GetStudents(IList<EventDefinition> events) public static Student[] GetStudents(IList<EventDefinition> events)
{ {
//var studentEventRankingsCsv = "Student Event Rankings.csv"; //var studentEventRankingsCsv = "Student Event Rankings.csv";
var studentEventRankingsCsv = "2023-24 RMS TSA student & event - Student Event Rankings.csv"; var studentEventRankingsCsv = "2024 Student Event Rankings.csv";
var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv); var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv);
var eventRankingsParser = new StudentParser(fileInfo); var eventRankingsParser = new StudentParser(fileInfo);
@@ -35,7 +35,7 @@ public static class TestEntityHandler
public static Team[] GetTeams(IList<EventDefinition> events, IList<Student> students) public static Team[] GetTeams(IList<EventDefinition> events, IList<Student> students)
{ {
//var studentEventRankingsCsv = "Student Event Rankings.csv"; //var studentEventRankingsCsv = "Student Event Rankings.csv";
var studentEventRankingsCsv = "2023-24 RMS TSA student & event - Teams.csv"; var studentEventRankingsCsv = "2024 Teams.csv";
var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv); var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv);
var eventRankingsParser = new TeamParser(fileInfo); var eventRankingsParser = new TeamParser(fileInfo);
@@ -50,14 +50,14 @@ public static class TestEntityHandler
public static AssignmentRequirement[] GetEventRequirements(IList<EventDefinition> events, IList<Student> students) public static AssignmentRequirement[] GetEventRequirements(IList<EventDefinition> events, IList<Student> students)
{ {
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024-25 RMS TSA student & event - assumptions.csv"); var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2025 Assumptions.csv");
var assumptionParser = new AssignmentRequirementParser(fileInfo); var assumptionParser = new AssignmentRequirementParser(fileInfo);
return assumptionParser.Parse(events, students); return assumptionParser.Parse(events, students);
} }
public static StudentEventRanking[] GetStudentEventRankings(Student[] students, EventDefinition[] events) public static StudentEventRanking[] GetStudentEventRankings(Student[] students, EventDefinition[] events)
{ {
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024-25 RMS TSA student & event - Student Event Rankings.csv"); var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2025 Student Event Rankings.csv");
var rankingParser = new StudentEventRankingParser(fileInfo); var rankingParser = new StudentEventRankingParser(fileInfo);
return rankingParser.Parse(students, events); return rankingParser.Parse(students, events);
@@ -1,37 +0,0 @@
Student Name,Grade,TSA year,Officer,State ID,Regional ID,1,2,3,4,5,6,7,TOTAL # OF EVENTS
"Abiodun-Adeniyi, Ife",8,2nd,,831177,,Dragster,Junior Solar Sprint,Flight,Mechanical Engineering,Off the Grid,,,5
"Alden, Ainsley",5,1st,Sergeant-At-Arms,924213,12227003,Construction Challenge,Off the Grid,Mechanical Engineering,Dragster,Problem Solving,,,5
"Blanco, Tavi",7,2nd,,800323,12227005,Off the Grid,Inventions & Innovations,Flight,Structural Engineering,Problem Solving,Tech Bowl,,6
"Bolin, Rylan",8,3rd,Secretary,831174,12227006,Structural Engineering,Website Design,Tech Bowl,Flight,Construction Challenge,Junior Solar Sprint,,6
"Borboa, Lillian",8,3rd,President,702470,12227007,Promotional Marketing,Children's Stories,STEM animation,Leadership Strategies,Off the Grid,Career Prep,,6
"Brady, Tucker",7,2nd,,820332,,Video Game Design,Website Design,Construction Challenge,STEM animation,Flight,,,5
"Callaghan, Cole",7,1st,,924305,,Video Game Design,Coding,Problem Solving,STEM animation,,,,4
"Carmon, Adaiah",7,2nd,,800328,,STEM Animation,Digital Photography,Video Game Design,Promotional Marketing,CAD Foundations,,,5
"Carmon, Isaiah",5,1st,,924210,12227008,Video Game Design,Community Service Video,Coding,Technical Design,Promotional Marketing,CAD foundations,,6
"Chesson, Bella",8,1st,,922730,12227009,Off the Grid,Leadership Strategies,Digital Photography,Children's Stories,,,,4
"Dean, Lucas",5,1st,,924302,,Video Game Design,Digital Photography,Junior Solar Sprint,Microcontroller Design,Dragster,Coding,,6
"Eldridge, AJ ",6,1st,,924310,,Electrical Applications,Tech Bowl,Coding,Structural Engineering,Technical Design,Cybersecurity,,6
"Fischer, Avery",6,2nd,,800321,12227011,Structural Engineering,Construction Challenge,Off the Grid,Dragster,Children's Stories,Career Prep,,6
"Greear, Ben",8,3rd,Treasurer,712415,12227012,Flight,Structural Engineering,Tech Bowl,Problem Solving,Construction Challenge,,,5
"Hermann, Eliam",7,2nd,,831172,12227013,Inventions & Innovations,Prepared Speech ,Leadership Strategies,Video Game Design,System Control Technology,Biotechnology,,6
"Jones, Miauna",8,2nd,,800316,12227014,Essays On Technology,Digital Photography,Children's Stories,Off the Grid,,,,4
"Kolpack, Grant",7,2nd,,800315,12227015,Children's Stories,STEM animation,Website Design,Leadership Strategies,,,,4
"Laney, Wyatt",8,3rd,Vice President,702477,12227016,Structural Engineering,Website Design,Tech Bowl ,Vlogging,Leadership Strategies ,Cybersecurity ,,6
"Marx, Ryen",8,2nd,,800320,,Website Design,Construction Challenge,Junior Solar Sprint,Off the Grid,Children's Stories,Mechanical Engineering,,6
"McDonald, Raylee",8,2nd,,800317,12227017,Digital Photography,Off the Grid,Medical Technology,Children's Stories,Mechanical Engineering,Construction Challenge,,6
"McKee, Ellie",6,1st,,924314,12227018,Off the Grid,Structural Engineering,Construction Challenge,Digital Photography,Biotechnology,Medical Technology,,6
"Moulton, Kyle",6,2nd,,800322,12227019,Inventions & Innovations,Mechanical Engineering,Vlogging,Website Design,,,,4
"Naeve, Lydia",8,1st,,924208,12227020,Off the Grid,Promotional Marketing,Medical Technology,Forensic Technology,Mechanical Engineering,,,5
"Naeve, Suse",6,1st,,924317,12227021,Digital Photography,Off the Grid,Vlogging,Children's Stories,Forensic Technology,Technical Design,,6
"Schlesser, Abby",5,1st,,924211,12227022,Children's Stories,Digital Photography,Mass Production,Problem Solving,Inventions & Innovations,Promotional Marketing,,6
"Schlesser, David",8,3rd,Reporter,702464,12227023,Dragster,Prepared Speech,Career Prep,Systems Control Technology,Junior Solar Sprint,Mechanical Engineering,Inventions & Innovations,7
"Toth, Keegan",8,2nd,,831171,12227024,Inventions & Innovations,Coding,Video Game Design,Prepared Speech,Off the Grid,,,5
"Underwood, Ian",8,2nd,,800325,12227025,Leadership Strategies,Inventions & Innovations,Flight,,,,,3
"White, Thomas",7,2nd,,800319,12227026,Leadership Strategies,Inventions & Innovations,Challenging Technology Issues,Tech Bowl,Prepared Speech,,,5
,,,,,,,,,,,,,
,,,,,,,,,,,,,
,,,,,,,,,,,,,
,,,,,,,,,,,,,
,,,,,,,,,,,,,
,,,,,,,,,,,,,
,,,,,,, ,,,,,,
1 Student Name Grade TSA year Officer State ID Regional ID 1 2 3 4 5 6 7 TOTAL # OF EVENTS
2 Abiodun-Adeniyi, Ife 8 2nd 831177 Dragster Junior Solar Sprint Flight Mechanical Engineering Off the Grid 5
3 Alden, Ainsley 5 1st Sergeant-At-Arms 924213 12227003 Construction Challenge Off the Grid Mechanical Engineering Dragster Problem Solving 5
4 Blanco, Tavi 7 2nd 800323 12227005 Off the Grid Inventions & Innovations Flight Structural Engineering Problem Solving Tech Bowl 6
5 Bolin, Rylan 8 3rd Secretary 831174 12227006 Structural Engineering Website Design Tech Bowl Flight Construction Challenge Junior Solar Sprint 6
6 Borboa, Lillian 8 3rd President 702470 12227007 Promotional Marketing Children's Stories STEM animation Leadership Strategies Off the Grid Career Prep 6
7 Brady, Tucker 7 2nd 820332 Video Game Design Website Design Construction Challenge STEM animation Flight 5
8 Callaghan, Cole 7 1st 924305 Video Game Design Coding Problem Solving STEM animation 4
9 Carmon, Adaiah 7 2nd 800328 STEM Animation Digital Photography Video Game Design Promotional Marketing CAD Foundations 5
10 Carmon, Isaiah 5 1st 924210 12227008 Video Game Design Community Service Video Coding Technical Design Promotional Marketing CAD foundations 6
11 Chesson, Bella 8 1st 922730 12227009 Off the Grid Leadership Strategies Digital Photography Children's Stories 4
12 Dean, Lucas 5 1st 924302 Video Game Design Digital Photography Junior Solar Sprint Microcontroller Design Dragster Coding 6
13 Eldridge, AJ 6 1st 924310 Electrical Applications Tech Bowl Coding Structural Engineering Technical Design Cybersecurity 6
14 Fischer, Avery 6 2nd 800321 12227011 Structural Engineering Construction Challenge Off the Grid Dragster Children's Stories Career Prep 6
15 Greear, Ben 8 3rd Treasurer 712415 12227012 Flight Structural Engineering Tech Bowl Problem Solving Construction Challenge 5
16 Hermann, Eliam 7 2nd 831172 12227013 Inventions & Innovations Prepared Speech Leadership Strategies Video Game Design System Control Technology Biotechnology 6
17 Jones, Miauna 8 2nd 800316 12227014 Essays On Technology Digital Photography Children's Stories Off the Grid 4
18 Kolpack, Grant 7 2nd 800315 12227015 Children's Stories STEM animation Website Design Leadership Strategies 4
19 Laney, Wyatt 8 3rd Vice President 702477 12227016 Structural Engineering Website Design Tech Bowl Vlogging Leadership Strategies Cybersecurity 6
20 Marx, Ryen 8 2nd 800320 Website Design Construction Challenge Junior Solar Sprint Off the Grid Children's Stories Mechanical Engineering 6
21 McDonald, Raylee 8 2nd 800317 12227017 Digital Photography Off the Grid Medical Technology Children's Stories Mechanical Engineering Construction Challenge 6
22 McKee, Ellie 6 1st 924314 12227018 Off the Grid Structural Engineering Construction Challenge Digital Photography Biotechnology Medical Technology 6
23 Moulton, Kyle 6 2nd 800322 12227019 Inventions & Innovations Mechanical Engineering Vlogging Website Design 4
24 Naeve, Lydia 8 1st 924208 12227020 Off the Grid Promotional Marketing Medical Technology Forensic Technology Mechanical Engineering 5
25 Naeve, Suse 6 1st 924317 12227021 Digital Photography Off the Grid Vlogging Children's Stories Forensic Technology Technical Design 6
26 Schlesser, Abby 5 1st 924211 12227022 Children's Stories Digital Photography Mass Production Problem Solving Inventions & Innovations Promotional Marketing 6
27 Schlesser, David 8 3rd Reporter 702464 12227023 Dragster Prepared Speech Career Prep Systems Control Technology Junior Solar Sprint Mechanical Engineering Inventions & Innovations 7
28 Toth, Keegan 8 2nd 831171 12227024 Inventions & Innovations Coding Video Game Design Prepared Speech Off the Grid 5
29 Underwood, Ian 8 2nd 800325 12227025 Leadership Strategies Inventions & Innovations Flight 3
30 White, Thomas 7 2nd 800319 12227026 Leadership Strategies Inventions & Innovations Challenging Technology Issues Tech Bowl Prepared Speech 5
31
32
33
34
35
36
37
@@ -1,83 +0,0 @@
Team Name,Event Name,Regional Time Slot,Student 1,Student 2,Student 3,Student 4,Student 5
Career Prep,Career Prep,,,,,,
"Team Size: 1, Max Teams: 2 (i)","Team Size: 1, Max Teams: 2 (i)",,,,,,
Challenging Technology Issues,Challenging Technology Issues,2:30 - 2:45,David Schlesser,Thomas White,,,
"Team Size: 2, Max Teams: 2 (a)","Team Size: 2, Max Teams: 2 (a)",,,,,,
Children's Stories,Children's Stories,10:00 - 10:15,Lillian Borboa,Miauna Jones,Grant Kolpack,Abby Schlesser,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Coding Team 1,Coding Team 1,,Keegan Toth,AJ Eldridge,,,
"Team Size: 2, Max Teams: 2 (a)","Team Size: 2, Max Teams: 2 (a)",,,,,,
Coding Team 2,Coding Team 2,,Cole Callaghan,Isaiah Carmon,,,
"Team Size: 2, Max Teams: 2 (a)","Team Size: 2, Max Teams: 2 (a)",,,,,,
Community Service Video,Community Service Video,11:45 - 12:00,Isaiah Carmon,,,,
"Team Size: 1-6, Max Teams: 2","Team Size: 1-6, Max Teams: 2",,,,,,
CAD foundations,CAD foundations,,,Adaiah Carmon,Grant Kolpack,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
Construction Challenge Team 1,Construction Challenge Team 1,,Ryen Marx,Ben Greear,Rylan Bolin,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Construction Challenge Team 2,Construction Challenge Team 2,,Ellie McKee,Avery Fischer,Ainsley Alden,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Digital Photography,Digital Photography,,,Raylee McDonald,Suse Naeve,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
Dragster,Dragster,,,David Schlesser,Ife Abiodun-Adeniyi,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
Electrical Applications,Electrical Applications,,Rylan Bolin,AJ Eldridge,,,
"Team Size: 2, Max Teams: 2 (a)","Team Size: 2, Max Teams: 2 (a)",,,,,,
Essays on Technology,Essays on Technology,,,Miauna Jones,,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
Flight,Flight,,,Ben Greear,Ife Abiodun-Adeniyi,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
Inventions & Innovations Team 1,Inventions & Innovations Team 1,9:45 - 10:00,Kyle Moulton,Keegan Toth,Tavi Blanco,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Inventions & Innovations Team 2,Inventions & Innovations Team 2,9:45 - 10:00,Eliam Hermann,Ian Underwood,Thomas White,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Junior Solar Sprint,Junior Solar Sprint,,David Schlesser,Ife Abiodun-Adeniyi,Ryen Marx,Lucas Dean,
"Team Size: 2-4, Max Teams: 2 (a)","Team Size: 2-4, Max Teams: 2 (a)",,,,,,
Leadership Strategies Team 1,Leadership Strategies Team 1,10:20 - 10:30,Eliam Hermann,Lillian Borboa,Bella Chesson,,
"Team Size: 3, Max Teams: 2","Team Size: 3, Max Teams: 2",,,,,,
Leadership Strategies Team 2,Leadership Strategies Team 2,10:10 - 10:20,Thomas White,Ian Underwood,Tavi Blanco,,
"Team Size: 3, Max Teams: 2","Team Size: 3, Max Teams: 2",,,,,,
Mass Production,Mass Production,,Miauna Jones,Ellie McKee,Abby Schlesser,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Mechanical Engineering Team 1,Mechanical Engineering Team 1,,Raylee McDonald,David Schlesser,Kyle Moulton,,
"Team Size: 2-3, Max Teams: 2 (a)","Team Size: 2-3, Max Teams: 2 (a)",,,,,,
Mechanical Engineering Team 2,Mechanical Engineering Team 2,,Ryen Marx,Ainsley Alden,,,
"Team Size: 2-3, Max Teams: 2 (a)","Team Size: 2-3, Max Teams: 2 (a)",,,,,,
Medical Technology,Medical Technology,12:15 - 12:30,Lydia Naeve,Raylee McDonald,Bella Chesson,,
"Team Size: 3, Max Teams: 2 (a)","Team Size: 3, Max Teams: 2 (a)",,,,,,
Microcontroller Design,Microcontroller Design,,Lucas Dean,Isaiah Carmon,,,
"Team Size: 1-6, Max Teams: 2 (a)","Team Size: 1-6, Max Teams: 2 (a)",,,,,,
Off the Grid Team 1,Off the Grid Team 1,12:00 - 12:15,Avery Fischer,Miauna Jones,Bella Chesson,Suse Naeve,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Off the Grid Team 2,Off the Grid Team 2,10:45 - 11:00,Ainsley Alden,Raylee McDonald,Lydia Naeve,Tavi Blanco,Ellie McKee
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Prepared Speech,Prepared Speech,,,Keegan Toth,Eliam Hermann,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
Problem Solving,Problem Solving,,Ben Greear,Abby Schlesser,,,
"Team Size: 2, Max Teams: 1 (a)","Team Size: 2, Max Teams: 1 (a)",,,,,,
Promotional Marketing,Promotional Marketing,,,Lillian Borboa,Lydia Naeve,,
"Team Size: 1, Max Teams: 2 (a) (i)","Team Size: 1, Max Teams: 2 (a) (i)",,,,,,
STEM Animation,STEM Animation,,Grant Kolpack,Lillian Borboa,Adaiah Carmon,Cole Callaghan,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Structural Engineering Team 1,Structural Engineering Team 1,,Ben Greear,Avery Fischer,,,
"Team Size: 2, Max Teams: 2 (a)","Team Size: 2, Max Teams: 2 (a)",,,,,,
Structural Engineering Team 2,Structural Engineering Team 2,,Rylan Bolin,Wyatt Laney,,,
"Team Size: 2, Max Teams: 2 (a)","Team Size: 2, Max Teams: 2 (a)",,,,,,
System Control Technology,System Control Technology,,Eliam Hermann,David Schlesser,Tucker Brady,,
"Team Size: 3, Max Teams: 2 (a)","Team Size: 3, Max Teams: 2 (a)",,,,,,
Tech Bowl Team 1,Tech Bowl Team 1,11:00,Ben Greear,Rylan Bolin,Wyatt Laney,,
"Team Size: 3, Max Teams: 2 (a)","Team Size: 3, Max Teams: 2 (a)",,,,,,
Tech Bowl Team 2,Tech Bowl Team 2,11:40,Thomas White,Ian Underwood,Grant Kolpack,,
"Team Size: 3, Max Teams: 2 (a)","Team Size: 3, Max Teams: 2 (a)",,,,,,
Video Game Design Team 1,Video Game Design Team 1,,Keegan Toth,Adaiah Carmon,Lucas Dean,,
"Team Size: 2-6, Max Teams: 2","Team Size: 2-6, Max Teams: 2",,,,,,
Video Game Design Team 2,Video Game Design Team 2,,Tucker Brady,Eliam Hermann,Cole Callaghan,Isaiah Carmon,
"Team Size: 2-6, Max Teams: 2","Team Size: 2-6, Max Teams: 2",,,,,,
Vlogging,Vlogging,10:15 - 10:30,Wyatt Laney,Kyle Moulton,Suse Naeve,,
"Team Size: 2-6, Max Teams: 2 (a)","Team Size: 2-6, Max Teams: 2 (a)",,,,,,
Website Design Team 1,Website Design Team 1,,Rylan Bolin,Wyatt Laney,Tucker Brady,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
Website Design Team 2,Website Design Team 2,,Grant Kolpack,Ryen Marx,AJ Eldridge,,
"Team Size: 3-6, Max Teams: 2","Team Size: 3-6, Max Teams: 2",,,,,,
(a) denotes an event that has activity other than interview or presentation at state,,,,,,,
(i) denotes an individual event,,,,,,,
1 Team Name Event Name Regional Time Slot Student 1 Student 2 Student 3 Student 4 Student 5
2 Career Prep Career Prep
3 Team Size: 1, Max Teams: 2 (i) Team Size: 1, Max Teams: 2 (i)
4 Challenging Technology Issues Challenging Technology Issues 2:30 - 2:45 David Schlesser Thomas White
5 Team Size: 2, Max Teams: 2 (a) Team Size: 2, Max Teams: 2 (a)
6 Children's Stories Children's Stories 10:00 - 10:15 Lillian Borboa Miauna Jones Grant Kolpack Abby Schlesser
7 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
8 Coding Team 1 Coding Team 1 Keegan Toth AJ Eldridge
9 Team Size: 2, Max Teams: 2 (a) Team Size: 2, Max Teams: 2 (a)
10 Coding Team 2 Coding Team 2 Cole Callaghan Isaiah Carmon
11 Team Size: 2, Max Teams: 2 (a) Team Size: 2, Max Teams: 2 (a)
12 Community Service Video Community Service Video 11:45 - 12:00 Isaiah Carmon
13 Team Size: 1-6, Max Teams: 2 Team Size: 1-6, Max Teams: 2
14 CAD foundations CAD foundations Adaiah Carmon Grant Kolpack
15 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
16 Construction Challenge Team 1 Construction Challenge Team 1 Ryen Marx Ben Greear Rylan Bolin
17 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
18 Construction Challenge Team 2 Construction Challenge Team 2 Ellie McKee Avery Fischer Ainsley Alden
19 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
20 Digital Photography Digital Photography Raylee McDonald Suse Naeve
21 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
22 Dragster Dragster David Schlesser Ife Abiodun-Adeniyi
23 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
24 Electrical Applications Electrical Applications Rylan Bolin AJ Eldridge
25 Team Size: 2, Max Teams: 2 (a) Team Size: 2, Max Teams: 2 (a)
26 Essays on Technology Essays on Technology Miauna Jones
27 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
28 Flight Flight Ben Greear Ife Abiodun-Adeniyi
29 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
30 Inventions & Innovations Team 1 Inventions & Innovations Team 1 9:45 - 10:00 Kyle Moulton Keegan Toth Tavi Blanco
31 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
32 Inventions & Innovations Team 2 Inventions & Innovations Team 2 9:45 - 10:00 Eliam Hermann Ian Underwood Thomas White
33 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
34 Junior Solar Sprint Junior Solar Sprint David Schlesser Ife Abiodun-Adeniyi Ryen Marx Lucas Dean
35 Team Size: 2-4, Max Teams: 2 (a) Team Size: 2-4, Max Teams: 2 (a)
36 Leadership Strategies Team 1 Leadership Strategies Team 1 10:20 - 10:30 Eliam Hermann Lillian Borboa Bella Chesson
37 Team Size: 3, Max Teams: 2 Team Size: 3, Max Teams: 2
38 Leadership Strategies Team 2 Leadership Strategies Team 2 10:10 - 10:20 Thomas White Ian Underwood Tavi Blanco
39 Team Size: 3, Max Teams: 2 Team Size: 3, Max Teams: 2
40 Mass Production Mass Production Miauna Jones Ellie McKee Abby Schlesser
41 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
42 Mechanical Engineering Team 1 Mechanical Engineering Team 1 Raylee McDonald David Schlesser Kyle Moulton
43 Team Size: 2-3, Max Teams: 2 (a) Team Size: 2-3, Max Teams: 2 (a)
44 Mechanical Engineering Team 2 Mechanical Engineering Team 2 Ryen Marx Ainsley Alden
45 Team Size: 2-3, Max Teams: 2 (a) Team Size: 2-3, Max Teams: 2 (a)
46 Medical Technology Medical Technology 12:15 - 12:30 Lydia Naeve Raylee McDonald Bella Chesson
47 Team Size: 3, Max Teams: 2 (a) Team Size: 3, Max Teams: 2 (a)
48 Microcontroller Design Microcontroller Design Lucas Dean Isaiah Carmon
49 Team Size: 1-6, Max Teams: 2 (a) Team Size: 1-6, Max Teams: 2 (a)
50 Off the Grid Team 1 Off the Grid Team 1 12:00 - 12:15 Avery Fischer Miauna Jones Bella Chesson Suse Naeve
51 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
52 Off the Grid Team 2 Off the Grid Team 2 10:45 - 11:00 Ainsley Alden Raylee McDonald Lydia Naeve Tavi Blanco Ellie McKee
53 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
54 Prepared Speech Prepared Speech Keegan Toth Eliam Hermann
55 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
56 Problem Solving Problem Solving Ben Greear Abby Schlesser
57 Team Size: 2, Max Teams: 1 (a) Team Size: 2, Max Teams: 1 (a)
58 Promotional Marketing Promotional Marketing Lillian Borboa Lydia Naeve
59 Team Size: 1, Max Teams: 2 (a) (i) Team Size: 1, Max Teams: 2 (a) (i)
60 STEM Animation STEM Animation Grant Kolpack Lillian Borboa Adaiah Carmon Cole Callaghan
61 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
62 Structural Engineering Team 1 Structural Engineering Team 1 Ben Greear Avery Fischer
63 Team Size: 2, Max Teams: 2 (a) Team Size: 2, Max Teams: 2 (a)
64 Structural Engineering Team 2 Structural Engineering Team 2 Rylan Bolin Wyatt Laney
65 Team Size: 2, Max Teams: 2 (a) Team Size: 2, Max Teams: 2 (a)
66 System Control Technology System Control Technology Eliam Hermann David Schlesser Tucker Brady
67 Team Size: 3, Max Teams: 2 (a) Team Size: 3, Max Teams: 2 (a)
68 Tech Bowl Team 1 Tech Bowl Team 1 11:00 Ben Greear Rylan Bolin Wyatt Laney
69 Team Size: 3, Max Teams: 2 (a) Team Size: 3, Max Teams: 2 (a)
70 Tech Bowl Team 2 Tech Bowl Team 2 11:40 Thomas White Ian Underwood Grant Kolpack
71 Team Size: 3, Max Teams: 2 (a) Team Size: 3, Max Teams: 2 (a)
72 Video Game Design Team 1 Video Game Design Team 1 Keegan Toth Adaiah Carmon Lucas Dean
73 Team Size: 2-6, Max Teams: 2 Team Size: 2-6, Max Teams: 2
74 Video Game Design Team 2 Video Game Design Team 2 Tucker Brady Eliam Hermann Cole Callaghan Isaiah Carmon
75 Team Size: 2-6, Max Teams: 2 Team Size: 2-6, Max Teams: 2
76 Vlogging Vlogging 10:15 - 10:30 Wyatt Laney Kyle Moulton Suse Naeve
77 Team Size: 2-6, Max Teams: 2 (a) Team Size: 2-6, Max Teams: 2 (a)
78 Website Design Team 1 Website Design Team 1 Rylan Bolin Wyatt Laney Tucker Brady
79 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
80 Website Design Team 2 Website Design Team 2 Grant Kolpack Ryen Marx AJ Eldridge
81 Team Size: 3-6, Max Teams: 2 Team Size: 3-6, Max Teams: 2
82 (a) denotes an event that has activity other than interview or presentation at state
83 (i) denotes an individual event
Internal Server Error - Gitea: Git with a cup of tea
500 Internal Server Error

Gitea Version: 1.28.0+dev-115-gb06002f449