Enhance event occurrence parsing with school level filtering
This commit introduces a new SchoolLevel enum and updates the EventOccurrenceParser to filter event occurrences based on the specified school level (Middle School or High School). The EventOccurrenceParseResult and EventOccurrenceParserResult classes have been updated to track skipped section headers and counts for both school levels. Additionally, the EventOccurrenceParserService has been modified to read the school level from configuration, and the UI has been updated to allow users to select the school level for event imports. This enhancement improves the accuracy of event parsing and provides better user feedback on skipped occurrences.
This commit is contained in:
@@ -32,10 +32,26 @@ public class EventOccurrenceParseResult
|
||||
|
||||
/// <summary>
|
||||
/// List of high school (HS) section headers that were encountered but skipped
|
||||
/// because they don't match any event definition in the system.
|
||||
/// because they don't match the chapter's school level setting.
|
||||
/// </summary>
|
||||
public List<string> SkippedHSSectionHeaders { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of middle school (MS) section headers that were encountered but skipped
|
||||
/// because they don't match the chapter's school level setting.
|
||||
/// </summary>
|
||||
public List<string> SkippedMSSectionHeaders { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Number of Middle School (MS) event occurrences that were skipped due to school level filtering.
|
||||
/// </summary>
|
||||
public int SkippedMSEventCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of High School (HS) event occurrences that were skipped due to school level filtering.
|
||||
/// </summary>
|
||||
public int SkippedHSEventCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of event occurrences successfully parsed.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// School level for filtering event occurrences.
|
||||
/// </summary>
|
||||
public enum SchoolLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Middle School level
|
||||
/// </summary>
|
||||
MiddleSchool,
|
||||
|
||||
/// <summary>
|
||||
/// High School level
|
||||
/// </summary>
|
||||
HighSchool
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Core.Entities;
|
||||
using Core.Models;
|
||||
using EventOccurrenceParsers = Core.Parsers.EventOccurrence;
|
||||
using Core.Utility;
|
||||
using SchoolLevel = Core.Models.SchoolLevel;
|
||||
|
||||
namespace Core.Parsers;
|
||||
|
||||
@@ -14,17 +15,22 @@ public class EventOccurrenceParserResult
|
||||
public IDictionary<EventDefinition, List<Entities.EventOccurrence>> Occurrences { get; set; } = new Dictionary<EventDefinition, List<Entities.EventOccurrence>>();
|
||||
public List<ParsingIssue> Issues { get; set; } = new();
|
||||
public List<string> SkippedHSSectionHeaders { get; set; } = new();
|
||||
public List<string> SkippedMSSectionHeaders { get; set; } = new();
|
||||
public int SkippedMSEventCount { get; set; }
|
||||
public int SkippedHSEventCount { get; set; }
|
||||
}
|
||||
|
||||
public class EventOccurrenceParser
|
||||
{
|
||||
private FileSystemInfo _txtFile;
|
||||
private ICollection<EventDefinition> _events;
|
||||
private SchoolLevel? _schoolLevel;
|
||||
|
||||
public EventOccurrenceParser(FileSystemInfo txtFile, ICollection<EventDefinition> events)
|
||||
public EventOccurrenceParser(FileSystemInfo txtFile, ICollection<EventDefinition> events, SchoolLevel? schoolLevel = null)
|
||||
{
|
||||
_events = events;
|
||||
_txtFile = txtFile;
|
||||
_schoolLevel = schoolLevel;
|
||||
}
|
||||
|
||||
public EventOccurrenceParserResult Parse()
|
||||
@@ -35,6 +41,7 @@ public class EventOccurrenceParser
|
||||
EventDefinition? currentEventDefinition = null;
|
||||
bool inContinuationMode = false;
|
||||
bool inHSSection = false;
|
||||
bool inMSSection = false;
|
||||
|
||||
var lines = File.ReadLines(_txtFile.FullName);
|
||||
foreach (var (line, index) in lines.Select((line, index) => (line, index + 1)))
|
||||
@@ -73,22 +80,71 @@ public class EventOccurrenceParser
|
||||
// Section headers break continuation mode
|
||||
inContinuationMode = false;
|
||||
|
||||
// Check if this is an HS event - if so, skip gracefully regardless of whether it matches
|
||||
// This prevents HS events from being incorrectly associated with MS events (e.g.,
|
||||
// "Biotechnology Design – HS" matching "Biotechnology" MS event)
|
||||
if (schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
|
||||
// Determine if we should skip this event based on chapter's school level setting
|
||||
bool shouldSkip = false;
|
||||
if (!string.IsNullOrWhiteSpace(schoolLevel))
|
||||
{
|
||||
if (_schoolLevel.HasValue)
|
||||
{
|
||||
// School level is set - filter based on it
|
||||
if (_schoolLevel.Value == SchoolLevel.MiddleSchool &&
|
||||
schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldSkip = true;
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
inHSSection = true;
|
||||
inMSSection = false;
|
||||
}
|
||||
else if (_schoolLevel.Value == SchoolLevel.HighSchool &&
|
||||
schoolLevel.Equals("MS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldSkip = true;
|
||||
result.SkippedMSSectionHeaders.Add(normalizedLine);
|
||||
inMSSection = true;
|
||||
inHSSection = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No school level set - backward compatibility: skip HS events
|
||||
if (schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldSkip = true;
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
inHSSection = true;
|
||||
inMSSection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSkip)
|
||||
{
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
currentEventDefinition = null; // Skip subsequent occurrences
|
||||
inHSSection = true; // Mark that we're in an HS section
|
||||
continue; // No issue created
|
||||
}
|
||||
|
||||
// For MS events, use fuzzy matching to find the best matching event definition
|
||||
// Reset section flags for events we're processing
|
||||
if (schoolLevel.Equals("MS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
inMSSection = true;
|
||||
inHSSection = false;
|
||||
}
|
||||
else if (schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
inHSSection = true;
|
||||
inMSSection = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
inHSSection = false;
|
||||
inMSSection = false;
|
||||
}
|
||||
|
||||
// Use fuzzy matching to find the best matching event definition
|
||||
var evt = EventOccurrenceParsers.SectionHeaderMatcher.MatchEventDefinition(eventNamePart, _events);
|
||||
if (evt == null)
|
||||
{
|
||||
// For unmatched MS headers, create issue
|
||||
// For unmatched headers, create issue
|
||||
var bestRatio = EventOccurrenceParsers.SectionHeaderMatcher.GetBestMatchRatio(eventNamePart, _events);
|
||||
issues.Add(new ParsingIssue
|
||||
{
|
||||
@@ -100,7 +156,6 @@ public class EventOccurrenceParser
|
||||
continue;
|
||||
}
|
||||
currentEventDefinition = evt;
|
||||
inHSSection = false; // Reset HS section flag for MS events
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -109,7 +164,8 @@ public class EventOccurrenceParser
|
||||
{
|
||||
// General schedule breaks continuation mode
|
||||
inContinuationMode = false;
|
||||
inHSSection = false; // Reset HS section flag
|
||||
inHSSection = false; // Reset section flags
|
||||
inMSSection = false;
|
||||
currentEventDefinition = EventDefinition.GeneralSchedule;
|
||||
continue;
|
||||
}
|
||||
@@ -120,21 +176,68 @@ public class EventOccurrenceParser
|
||||
// Section headers break continuation mode
|
||||
inContinuationMode = false;
|
||||
|
||||
// Check if this is an HS event - if so, skip gracefully regardless of whether it matches
|
||||
// This prevents HS events from being incorrectly associated with MS events
|
||||
if (normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
|
||||
// Determine if we should skip this event based on chapter's school level setting
|
||||
bool shouldSkip = false;
|
||||
if (_schoolLevel.HasValue)
|
||||
{
|
||||
// School level is set - filter based on it
|
||||
if (_schoolLevel.Value == SchoolLevel.MiddleSchool &&
|
||||
normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldSkip = true;
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
inHSSection = true;
|
||||
inMSSection = false;
|
||||
}
|
||||
else if (_schoolLevel.Value == SchoolLevel.HighSchool &&
|
||||
normalizedLine.Contains("MS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldSkip = true;
|
||||
result.SkippedMSSectionHeaders.Add(normalizedLine);
|
||||
inMSSection = true;
|
||||
inHSSection = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No school level set - backward compatibility: skip HS events
|
||||
if (normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldSkip = true;
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
inHSSection = true;
|
||||
inMSSection = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSkip)
|
||||
{
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
currentEventDefinition = null; // Skip subsequent occurrences
|
||||
inHSSection = true; // Mark that we're in an HS section
|
||||
continue; // No issue created
|
||||
}
|
||||
|
||||
// For MS events, use fuzzy matching to find the best matching event definition
|
||||
// Reset section flags for events we're processing
|
||||
if (normalizedLine.Contains("MS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
inMSSection = true;
|
||||
inHSSection = false;
|
||||
}
|
||||
else if (normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
inHSSection = true;
|
||||
inMSSection = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
inHSSection = false;
|
||||
inMSSection = false;
|
||||
}
|
||||
|
||||
// Use fuzzy matching to find the best matching event definition
|
||||
var evt = EventOccurrenceParsers.SectionHeaderMatcher.MatchEventDefinition(normalizedLine, _events);
|
||||
if (evt == null)
|
||||
{
|
||||
// For unmatched MS headers, create issue
|
||||
// For unmatched headers, create issue
|
||||
var bestRatio = EventOccurrenceParsers.SectionHeaderMatcher.GetBestMatchRatio(normalizedLine, _events);
|
||||
issues.Add(new ParsingIssue
|
||||
{
|
||||
@@ -146,7 +249,6 @@ public class EventOccurrenceParser
|
||||
continue;
|
||||
}
|
||||
currentEventDefinition = evt;
|
||||
inHSSection = false; // Reset HS section flag for MS events
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,10 +282,28 @@ public class EventOccurrenceParser
|
||||
// Occurrence lines break continuation mode
|
||||
inContinuationMode = false;
|
||||
|
||||
// Skip occurrences under HS sections (they won't match any event definition)
|
||||
if (inHSSection)
|
||||
// Skip occurrences under sections that don't match the school level setting
|
||||
if (_schoolLevel.HasValue)
|
||||
{
|
||||
continue;
|
||||
if (_schoolLevel.Value == SchoolLevel.MiddleSchool && inHSSection)
|
||||
{
|
||||
result.SkippedHSEventCount++;
|
||||
continue;
|
||||
}
|
||||
if (_schoolLevel.Value == SchoolLevel.HighSchool && inMSSection)
|
||||
{
|
||||
result.SkippedMSEventCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no school level is set, skip HS sections (backward compatibility)
|
||||
if (inHSSection)
|
||||
{
|
||||
result.SkippedHSEventCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var (occurrenceName, month, dayOfMonthStr, timeAndLocation) = occurrenceLine.Value;
|
||||
|
||||
@@ -4,6 +4,7 @@ using Core.Entities;
|
||||
using Core.Models;
|
||||
using Core.Parsers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SchoolLevel = Core.Models.SchoolLevel;
|
||||
|
||||
namespace Core.Services;
|
||||
|
||||
@@ -13,9 +14,11 @@ namespace Core.Services;
|
||||
/// </summary>
|
||||
public class EventOccurrenceParserService : IEventOccurrenceParserService
|
||||
{
|
||||
private readonly IConfiguration? _configuration;
|
||||
|
||||
public EventOccurrenceParserService(IConfiguration? configuration = null)
|
||||
{
|
||||
// Configuration parameter kept for backward compatibility but not used
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -38,8 +41,22 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
|
||||
File.WriteAllText(tempFile, text, Encoding.UTF8);
|
||||
var fileInfo = new FileInfo(tempFile);
|
||||
|
||||
// Use the existing EventOccurrenceParser
|
||||
var parser = new EventOccurrenceParser(fileInfo, events);
|
||||
// Read SchoolLevel from configuration
|
||||
SchoolLevel? schoolLevel = null;
|
||||
if (_configuration != null)
|
||||
{
|
||||
var schoolLevelStr = _configuration.GetSection("ChapterSettings:SchoolLevel").Get<string>();
|
||||
if (!string.IsNullOrWhiteSpace(schoolLevelStr))
|
||||
{
|
||||
if (Enum.TryParse<SchoolLevel>(schoolLevelStr, ignoreCase: true, out var parsed))
|
||||
{
|
||||
schoolLevel = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the existing EventOccurrenceParser with school level
|
||||
var parser = new EventOccurrenceParser(fileInfo, events, schoolLevel);
|
||||
var parserResult = parser.Parse();
|
||||
|
||||
// Copy occurrences from parser result
|
||||
@@ -92,8 +109,21 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
|
||||
// Copy parsing issues from parser result
|
||||
result.Issues.AddRange(parserResult.Issues);
|
||||
|
||||
// Copy skipped HS section headers from parser result
|
||||
// Copy skipped section headers from parser result
|
||||
result.SkippedHSSectionHeaders.AddRange(parserResult.SkippedHSSectionHeaders);
|
||||
result.SkippedMSSectionHeaders.AddRange(parserResult.SkippedMSSectionHeaders);
|
||||
result.SkippedMSEventCount = parserResult.SkippedMSEventCount;
|
||||
result.SkippedHSEventCount = parserResult.SkippedHSEventCount;
|
||||
|
||||
// Add informational messages about skipped events
|
||||
if (parserResult.SkippedMSEventCount > 0)
|
||||
{
|
||||
result.Warnings.Add($"Skipped {parserResult.SkippedMSEventCount} Middle School (MS) event occurrence(s) based on school level setting");
|
||||
}
|
||||
if (parserResult.SkippedHSEventCount > 0)
|
||||
{
|
||||
result.Warnings.Add($"Skipped {parserResult.SkippedHSEventCount} High School (HS) event occurrence(s) based on school level setting");
|
||||
}
|
||||
|
||||
// Validate locations and add warnings for problematic ones
|
||||
ValidateLocations(result);
|
||||
|
||||
@@ -125,9 +125,50 @@
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Dense="true">
|
||||
Successfully parsed @_parseResult.TotalParsed occurrence(s) from @_parseResult.Occurrences.Count event definition(s)
|
||||
@if (_parseResult.SkippedMSEventCount > 0 || _parseResult.SkippedHSEventCount > 0)
|
||||
{
|
||||
<text> (Skipped @(_parseResult.SkippedMSEventCount + _parseResult.SkippedHSEventCount) event occurrence(s) from other school level)</text>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@* Skipped Section Headers *@
|
||||
@if ((_parseResult.SkippedMSSectionHeaders.Any() || _parseResult.SkippedHSSectionHeaders.Any()) && _parseResult.IsSuccess)
|
||||
{
|
||||
<MudExpansionPanels Elevation="0" Class="mt-2">
|
||||
@if (_parseResult.SkippedMSSectionHeaders.Any())
|
||||
{
|
||||
<MudExpansionPanel Text="@($"Skipped MS Section Headers ({_parseResult.SkippedMSSectionHeaders.Count})")"
|
||||
Icon="@Icons.Material.Filled.Info"
|
||||
iconcolor="Color.Info">
|
||||
<MudList T="string">
|
||||
@foreach (var header in _parseResult.SkippedMSSectionHeaders)
|
||||
{
|
||||
<MudListItem T="string">
|
||||
<MudText>@header</MudText>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudExpansionPanel>
|
||||
}
|
||||
@if (_parseResult.SkippedHSSectionHeaders.Any())
|
||||
{
|
||||
<MudExpansionPanel Text="@($"Skipped HS Section Headers ({_parseResult.SkippedHSSectionHeaders.Count})")"
|
||||
Icon="@Icons.Material.Filled.Info"
|
||||
iconcolor="Color.Info">
|
||||
<MudList T="string">
|
||||
@foreach (var header in _parseResult.SkippedHSSectionHeaders)
|
||||
{
|
||||
<MudListItem T="string">
|
||||
<MudText>@header</MudText>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudExpansionPanel>
|
||||
}
|
||||
</MudExpansionPanels>
|
||||
}
|
||||
|
||||
@* Locations Summary *@
|
||||
@if (_parseResult.IsSuccess && _parseResult.Occurrences.Any())
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using WebApp.Models
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using System.Text.Json
|
||||
@using Core.Models
|
||||
@inject IWebHostEnvironment Environment
|
||||
@inject IConfiguration Configuration
|
||||
|
||||
@@ -43,6 +44,16 @@
|
||||
MaxLength="4"
|
||||
Required="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudSelect T="SchoolLevel?" @bind-Value="_settings.SchoolLevel"
|
||||
Label="School Level"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="Filter event occurrences by school level (leave empty to import both MS and HS)">
|
||||
<MudSelectItem T="SchoolLevel?" Value="null">Both (MS and HS)</MudSelectItem>
|
||||
<MudSelectItem T="SchoolLevel?" Value="@SchoolLevel.MiddleSchool">Middle School (MS)</MudSelectItem>
|
||||
<MudSelectItem T="SchoolLevel?" Value="@SchoolLevel.HighSchool">High School (HS)</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Core.Models;
|
||||
|
||||
namespace WebApp.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -34,4 +36,9 @@ public class ChapterSettings
|
||||
/// Competition year (e.g., "2026")
|
||||
/// </summary>
|
||||
public string CompetitionYear { get; set; } = "2026";
|
||||
|
||||
/// <summary>
|
||||
/// School level for the chapter (null = import both MS and HS events)
|
||||
/// </summary>
|
||||
public SchoolLevel? SchoolLevel { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user