Track critical curation summaries and edit resets
This commit is contained in:
@@ -34,6 +34,9 @@ public sealed record CriticalTableReference(
|
|||||||
string Family,
|
string Family,
|
||||||
string SourceDocument,
|
string SourceDocument,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
|
int CuratedResultCount,
|
||||||
|
int TotalResultCount,
|
||||||
|
int CurationPercentage,
|
||||||
IReadOnlyList<CriticalColumnReference> Columns,
|
IReadOnlyList<CriticalColumnReference> Columns,
|
||||||
IReadOnlyList<CriticalGroupReference> Groups,
|
IReadOnlyList<CriticalGroupReference> Groups,
|
||||||
IReadOnlyList<CriticalRollBandReference> RollBands);
|
IReadOnlyList<CriticalRollBandReference> RollBands);
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ public sealed class LookupService(
|
|||||||
.OrderBy(item => item.DisplayName)
|
.OrderBy(item => item.DisplayName)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var criticalResultCounts = await dbContext.CriticalResults
|
||||||
|
.AsNoTracking()
|
||||||
|
.GroupBy(item => item.CriticalTableId)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
CriticalTableId = group.Key,
|
||||||
|
TotalCount = group.Count(),
|
||||||
|
CuratedCount = group.Count(item => item.IsCurated)
|
||||||
|
})
|
||||||
|
.ToDictionaryAsync(
|
||||||
|
item => item.CriticalTableId,
|
||||||
|
item => (item.CuratedCount, item.TotalCount),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
return new LookupReferenceData(
|
return new LookupReferenceData(
|
||||||
attackTables,
|
attackTables,
|
||||||
armorTypes,
|
armorTypes,
|
||||||
@@ -52,6 +66,9 @@ public sealed class LookupService(
|
|||||||
item.Family,
|
item.Family,
|
||||||
item.SourceDocument,
|
item.SourceDocument,
|
||||||
item.Notes,
|
item.Notes,
|
||||||
|
GetCuratedCount(item.Id),
|
||||||
|
GetTotalCount(item.Id),
|
||||||
|
GetCurationPercentage(item.Id),
|
||||||
item.Columns
|
item.Columns
|
||||||
.OrderBy(column => column.SortOrder)
|
.OrderBy(column => column.SortOrder)
|
||||||
.Select(column => new CriticalColumnReference(column.ColumnKey, column.Label, column.Role, column.SortOrder))
|
.Select(column => new CriticalColumnReference(column.ColumnKey, column.Label, column.Role, column.SortOrder))
|
||||||
@@ -65,6 +82,22 @@ public sealed class LookupService(
|
|||||||
.Select(rollBand => new CriticalRollBandReference(rollBand.Label, rollBand.MinRoll, rollBand.MaxRoll, rollBand.SortOrder))
|
.Select(rollBand => new CriticalRollBandReference(rollBand.Label, rollBand.MinRoll, rollBand.MaxRoll, rollBand.SortOrder))
|
||||||
.ToList()))
|
.ToList()))
|
||||||
.ToList());
|
.ToList());
|
||||||
|
|
||||||
|
int GetCuratedCount(int tableId) =>
|
||||||
|
criticalResultCounts.TryGetValue(tableId, out var counts) ? counts.CuratedCount : 0;
|
||||||
|
|
||||||
|
int GetTotalCount(int tableId) =>
|
||||||
|
criticalResultCounts.TryGetValue(tableId, out var counts) ? counts.TotalCount : 0;
|
||||||
|
|
||||||
|
int GetCurationPercentage(int tableId)
|
||||||
|
{
|
||||||
|
if (!criticalResultCounts.TryGetValue(tableId, out var counts) || counts.TotalCount == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)Math.Round((double)counts.CuratedCount * 100 / counts.TotalCount, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AttackLookupResponse?> LookupAttackAsync(AttackLookupRequest request, CancellationToken cancellationToken = default)
|
public async Task<AttackLookupResponse?> LookupAttackAsync(AttackLookupRequest request, CancellationToken cancellationToken = default)
|
||||||
@@ -383,12 +416,15 @@ public sealed class LookupService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentState = CreateCurrentEditorState(result);
|
||||||
|
var hasEdits = HasCriticalCellEdits(currentState, request);
|
||||||
|
|
||||||
result.RawCellText = request.RawCellText.Trim();
|
result.RawCellText = request.RawCellText.Trim();
|
||||||
result.DescriptionText = request.DescriptionText.Trim();
|
result.DescriptionText = request.DescriptionText.Trim();
|
||||||
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
|
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
|
||||||
result.ParseStatus = request.ParseStatus.Trim();
|
result.ParseStatus = request.ParseStatus.Trim();
|
||||||
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
|
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
|
||||||
result.IsCurated = request.IsCurated;
|
result.IsCurated = hasEdits ? false : request.IsCurated;
|
||||||
|
|
||||||
ReplaceBaseEffects(dbContext, result, request.Effects);
|
ReplaceBaseEffects(dbContext, result, request.Effects);
|
||||||
ReplaceBranches(dbContext, result, request.Branches);
|
ReplaceBranches(dbContext, result, request.Branches);
|
||||||
@@ -396,7 +432,11 @@ public sealed class LookupService(
|
|||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, request.QuickParseInput, cancellationToken);
|
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, request.QuickParseInput, cancellationToken);
|
||||||
return CreateCellEditorResponse(result, request, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
var persistedState = request with
|
||||||
|
{
|
||||||
|
IsCurated = result.IsCurated
|
||||||
|
};
|
||||||
|
return CreateCellEditorResponse(result, persistedState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
|
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
|
||||||
@@ -1023,6 +1063,96 @@ public sealed class LookupService(
|
|||||||
private static string? NormalizeOptionalText(string? value) =>
|
private static string? NormalizeOptionalText(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
|
private static bool HasCriticalCellEdits(CriticalCellUpdateRequest currentState, CriticalCellUpdateRequest request)
|
||||||
|
{
|
||||||
|
if (!string.Equals(currentState.RawCellText.Trim(), request.RawCellText.Trim(), StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(currentState.QuickParseInput.Trim(), request.QuickParseInput.Trim(), StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(currentState.DescriptionText.Trim(), request.DescriptionText.Trim(), StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(NormalizeOptionalText(currentState.RawAffixText), NormalizeOptionalText(request.RawAffixText), StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(currentState.ParseStatus.Trim(), request.ParseStatus.Trim(), StringComparison.Ordinal) ||
|
||||||
|
currentState.IsDescriptionOverridden != request.IsDescriptionOverridden ||
|
||||||
|
currentState.IsRawAffixTextOverridden != request.IsRawAffixTextOverridden ||
|
||||||
|
currentState.AreEffectsOverridden != request.AreEffectsOverridden ||
|
||||||
|
currentState.AreBranchesOverridden != request.AreBranchesOverridden)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !EffectListsEqual(currentState.Effects, request.Effects) ||
|
||||||
|
!BranchListsEqual(currentState.Branches, request.Branches);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EffectListsEqual(
|
||||||
|
IReadOnlyList<CriticalEffectEditorItem> left,
|
||||||
|
IReadOnlyList<CriticalEffectEditorItem> right)
|
||||||
|
{
|
||||||
|
if (left.Count != right.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < left.Count; index++)
|
||||||
|
{
|
||||||
|
if (!EffectsEqual(left[index], right[index]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool BranchListsEqual(
|
||||||
|
IReadOnlyList<CriticalBranchEditorItem> left,
|
||||||
|
IReadOnlyList<CriticalBranchEditorItem> right)
|
||||||
|
{
|
||||||
|
if (left.Count != right.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < left.Count; index++)
|
||||||
|
{
|
||||||
|
if (!BranchesEqual(left[index], right[index]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EffectsEqual(CriticalEffectEditorItem left, CriticalEffectEditorItem right) =>
|
||||||
|
string.Equals(left.EffectCode.Trim(), right.EffectCode.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.Target), NormalizeOptionalText(right.Target), StringComparison.Ordinal) &&
|
||||||
|
left.ValueInteger == right.ValueInteger &&
|
||||||
|
left.ValueDecimal == right.ValueDecimal &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.ValueExpression), NormalizeOptionalText(right.ValueExpression), StringComparison.Ordinal) &&
|
||||||
|
left.DurationRounds == right.DurationRounds &&
|
||||||
|
left.PerRound == right.PerRound &&
|
||||||
|
left.Modifier == right.Modifier &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.BodyPart), NormalizeOptionalText(right.BodyPart), StringComparison.Ordinal) &&
|
||||||
|
left.IsPermanent == right.IsPermanent &&
|
||||||
|
string.Equals(left.SourceType.Trim(), right.SourceType.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.SourceText), NormalizeOptionalText(right.SourceText), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.OriginKey), NormalizeOptionalText(right.OriginKey), StringComparison.Ordinal) &&
|
||||||
|
left.IsOverridden == right.IsOverridden;
|
||||||
|
|
||||||
|
private static bool BranchesEqual(CriticalBranchEditorItem left, CriticalBranchEditorItem right) =>
|
||||||
|
string.Equals(left.BranchKind.Trim(), right.BranchKind.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.ConditionKey), NormalizeOptionalText(right.ConditionKey), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(left.ConditionText.Trim(), right.ConditionText.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(left.ConditionJson.Trim(), right.ConditionJson.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(left.RawText.Trim(), right.RawText.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(left.DescriptionText.Trim(), right.DescriptionText.Trim(), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.RawAffixText), NormalizeOptionalText(right.RawAffixText), StringComparison.Ordinal) &&
|
||||||
|
string.Equals(left.ParsedJson.Trim(), right.ParsedJson.Trim(), StringComparison.Ordinal) &&
|
||||||
|
left.SortOrder == right.SortOrder &&
|
||||||
|
string.Equals(NormalizeOptionalText(left.OriginKey), NormalizeOptionalText(right.OriginKey), StringComparison.Ordinal) &&
|
||||||
|
left.IsOverridden == right.IsOverridden &&
|
||||||
|
left.AreEffectsOverridden == right.AreEffectsOverridden &&
|
||||||
|
EffectListsEqual(left.Effects, right.Effects);
|
||||||
|
|
||||||
private static string NormalizeSlug(string value) =>
|
private static string NormalizeSlug(string value) =>
|
||||||
value.Trim().Replace(' ', '_').ToLowerInvariant();
|
value.Trim().Replace(' ', '_').ToLowerInvariant();
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,9 @@ public sealed class LookupRollingTests
|
|||||||
"standard",
|
"standard",
|
||||||
$"{label}.pdf",
|
$"{label}.pdf",
|
||||||
null,
|
null,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
rollBands);
|
rollBands);
|
||||||
|
|||||||
@@ -56,6 +56,56 @@ public sealed class LookupServiceCurationIntegrationTests
|
|||||||
Assert.Equal(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl);
|
Assert.Equal(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Lookup_service_clears_curated_state_when_any_content_is_edited()
|
||||||
|
{
|
||||||
|
var databasePath = CreateEmptyDatabasePath();
|
||||||
|
var repositoryRoot = CreateTemporaryRepositoryRoot();
|
||||||
|
var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")));
|
||||||
|
|
||||||
|
await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2);
|
||||||
|
WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png");
|
||||||
|
|
||||||
|
var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator);
|
||||||
|
var resultId = await GetResultIdAsync(databasePath);
|
||||||
|
|
||||||
|
var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
|
||||||
|
Assert.NotNull(initialResponse);
|
||||||
|
|
||||||
|
var markCuratedRequest = new CriticalCellUpdateRequest(
|
||||||
|
initialResponse!.RawCellText,
|
||||||
|
initialResponse.QuickParseInput,
|
||||||
|
initialResponse.DescriptionText,
|
||||||
|
initialResponse.RawAffixText,
|
||||||
|
initialResponse.ParseStatus,
|
||||||
|
initialResponse.ParsedJson,
|
||||||
|
true,
|
||||||
|
initialResponse.IsDescriptionOverridden,
|
||||||
|
initialResponse.IsRawAffixTextOverridden,
|
||||||
|
initialResponse.AreEffectsOverridden,
|
||||||
|
initialResponse.AreBranchesOverridden,
|
||||||
|
initialResponse.Effects,
|
||||||
|
initialResponse.Branches);
|
||||||
|
|
||||||
|
var curatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, markCuratedRequest);
|
||||||
|
Assert.NotNull(curatedResponse);
|
||||||
|
Assert.True(curatedResponse!.IsCurated);
|
||||||
|
|
||||||
|
var editedRequest = markCuratedRequest with
|
||||||
|
{
|
||||||
|
DescriptionText = "Edited description after curation."
|
||||||
|
};
|
||||||
|
|
||||||
|
var editedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, editedRequest);
|
||||||
|
Assert.NotNull(editedResponse);
|
||||||
|
Assert.False(editedResponse!.IsCurated);
|
||||||
|
|
||||||
|
var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
|
||||||
|
Assert.NotNull(reopenedResponse);
|
||||||
|
Assert.False(reopenedResponse!.IsCurated);
|
||||||
|
Assert.Equal("Edited description after curation.", reopenedResponse.DescriptionText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Lookup_service_resolves_source_image_paths_only_when_artifacts_exist()
|
public async Task Lookup_service_resolves_source_image_paths_only_when_artifacts_exist()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user