Update release notes and deploy workspace
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Backend.Contracts;
|
||||
using Backend.Common;
|
||||
using Backend.Data;
|
||||
using Backend.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -20,37 +21,39 @@ public static class AdminDashboardEndpoints
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetDashboard(AwardsDbContext db, HttpContext context)
|
||||
private static async Task<IResult> GetDashboard(int? seasonId, AwardsDbContext db, HttpContext context)
|
||||
{
|
||||
var canViewAuditIp = CanViewAuditIp(context);
|
||||
var currentSeason = await db.Seasons.AsNoTracking().FirstOrDefaultAsync(item => item.IsCurrent);
|
||||
if (currentSeason is null)
|
||||
var selectedSeason = seasonId.HasValue
|
||||
? await db.Seasons.AsNoTracking().FirstOrDefaultAsync(item => item.Id == seasonId.Value)
|
||||
: await db.Seasons.AsNoTracking().FirstOrDefaultAsync(item => item.IsCurrent);
|
||||
if (selectedSeason is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var nominationCount = await db.Nominations.CountAsync(item => item.SeasonId == currentSeason.Id);
|
||||
var voteCount = await db.VoteEntries.CountAsync(item => item.Ballot.SeasonId == currentSeason.Id);
|
||||
var categoryCount = await db.Categories.CountAsync(item => item.SeasonId == currentSeason.Id);
|
||||
var reviewCount = await db.Nominations.CountAsync(item => item.SeasonId == currentSeason.Id && item.Status == "pending");
|
||||
var riskFlagCount = await db.RiskFlags.CountAsync(item => item.Status == "open");
|
||||
var selectedSeasonId = selectedSeason.Id;
|
||||
var phaseKey = SeasonMappings.NormalizePhaseKey(selectedSeason.CurrentPhase);
|
||||
var nominationCount = await db.Nominations.CountAsync(item => item.SeasonId == selectedSeasonId);
|
||||
var voteCount = await db.VoteEntries.CountAsync(item => item.Ballot.SeasonId == selectedSeasonId);
|
||||
var categoryCount = await db.Categories.CountAsync(item => item.SeasonId == selectedSeasonId);
|
||||
var reviewCount = await db.Nominations.CountAsync(item => item.SeasonId == selectedSeasonId && item.Status == "pending");
|
||||
var riskFlagCount = await db.RiskFlags.CountAsync(item =>
|
||||
item.Status == "open" &&
|
||||
(item.SeasonId == selectedSeasonId || item.SeasonId == null));
|
||||
var globalRiskFlagCount = await db.RiskFlags.CountAsync(item =>
|
||||
item.Status == "open" &&
|
||||
item.SeasonId == null);
|
||||
|
||||
var topCategoryNames = await db.VoteEntries
|
||||
.AsNoTracking()
|
||||
.Where(item => item.Ballot.SeasonId == currentSeason.Id)
|
||||
.Select(item => item.Category.Name)
|
||||
.ToListAsync();
|
||||
|
||||
var topCategories = topCategoryNames
|
||||
.GroupBy(name => name)
|
||||
.Select(grouping => new AdminTopCategoryDto(grouping.Key, grouping.Count()))
|
||||
.OrderByDescending(item => item.Votes)
|
||||
.Take(5)
|
||||
.ToArray();
|
||||
var topCategories = phaseKey == "nomination"
|
||||
? await BuildTopNominationCategoriesAsync(db, selectedSeasonId)
|
||||
: await BuildTopVotingCategoriesAsync(db, selectedSeasonId);
|
||||
|
||||
var riskFlags = await db.RiskFlags
|
||||
.AsNoTracking()
|
||||
.Where(item => item.Status == "open")
|
||||
.Where(item =>
|
||||
item.Status == "open" &&
|
||||
(item.SeasonId == selectedSeasonId || item.SeasonId == null))
|
||||
.OrderByDescending(item => item.CreatedAt)
|
||||
.Take(8)
|
||||
.ToArrayAsync();
|
||||
@@ -74,18 +77,27 @@ public static class AdminDashboardEndpoints
|
||||
.ToArrayAsync();
|
||||
|
||||
var activityItems = auditEntries
|
||||
.Take(3)
|
||||
.Take(6)
|
||||
.Select(item => new AdminActivityDto(item.Summary, $"{Math.Max(1, (int)Math.Round((DateTimeOffset.UtcNow - item.CreatedAt).TotalMinutes))} Min."))
|
||||
.ToArray();
|
||||
|
||||
return Results.Ok(new AdminDashboardResponse(
|
||||
selectedSeason.Id,
|
||||
selectedSeason.Year,
|
||||
selectedSeason.Name,
|
||||
selectedSeason.IsCurrent,
|
||||
new[]
|
||||
{
|
||||
new AdminMetricDto("Nominierungen", nominationCount, "Gespeicherte Einreichungen im aktuellen Public-Jahr"),
|
||||
new AdminMetricDto("Stimmen", voteCount, "Abgegebene Stimmen im aktuellen Public-Jahr"),
|
||||
new AdminMetricDto("Kategorien", categoryCount, "Aktive Kategorien im aktuellen Public-Jahr"),
|
||||
new AdminMetricDto("Reviews offen", reviewCount, "Offene Nominierungen mit Review-Bedarf"),
|
||||
new AdminMetricDto("Risikohinweise", riskFlagCount, "Offene Risk Flags ueber alle Quellen"),
|
||||
new AdminMetricDto("Nominierungen", nominationCount, $"Gespeicherte Einreichungen im Award-Jahr {selectedSeason.Year}"),
|
||||
new AdminMetricDto("Stimmen", voteCount, $"Abgegebene Stimmen im Award-Jahr {selectedSeason.Year}"),
|
||||
new AdminMetricDto("Kategorien", categoryCount, $"Aktive Kategorien im Award-Jahr {selectedSeason.Year}"),
|
||||
new AdminMetricDto("Reviews offen", reviewCount, "Offene Nominierungen mit Review-Bedarf in diesem Jahr"),
|
||||
new AdminMetricDto(
|
||||
"Risikohinweise",
|
||||
riskFlagCount,
|
||||
globalRiskFlagCount > 0
|
||||
? $"Offene Hinweise fuer {selectedSeason.Year}, inklusive {globalRiskFlagCount} globaler Hinweise"
|
||||
: $"Offene Hinweise fuer {selectedSeason.Year}"),
|
||||
},
|
||||
activityItems,
|
||||
topCategories,
|
||||
@@ -93,6 +105,45 @@ public static class AdminDashboardEndpoints
|
||||
auditEntries));
|
||||
}
|
||||
|
||||
private static async Task<AdminTopCategoryDto[]> BuildTopVotingCategoriesAsync(AwardsDbContext db, int seasonId)
|
||||
{
|
||||
var categoryNames = await db.VoteEntries
|
||||
.AsNoTracking()
|
||||
.Where(item => item.Ballot.SeasonId == seasonId)
|
||||
.Select(item => item.Category.Name)
|
||||
.ToListAsync();
|
||||
|
||||
return categoryNames
|
||||
.GroupBy(name => name)
|
||||
.Select(grouping => new AdminTopCategoryDto(grouping.Key, grouping.Count(), "Stimmen"))
|
||||
.OrderByDescending(item => item.Value)
|
||||
.Take(5)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static async Task<AdminTopCategoryDto[]> BuildTopNominationCategoriesAsync(AwardsDbContext db, int seasonId)
|
||||
{
|
||||
var nominationCategories = await db.Nominations
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
.Select(item => new
|
||||
{
|
||||
CategoryName = item.Category != null ? item.Category.Name : null,
|
||||
item.CategoryGroupName,
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return nominationCategories
|
||||
.Select(item => string.IsNullOrWhiteSpace(item.CategoryName)
|
||||
? string.IsNullOrWhiteSpace(item.CategoryGroupName) ? "Ohne Kategorie" : item.CategoryGroupName
|
||||
: item.CategoryName)
|
||||
.GroupBy(name => name)
|
||||
.Select(grouping => new AdminTopCategoryDto(grouping.Key, grouping.Count(), "Nominierungen"))
|
||||
.OrderByDescending(item => item.Value)
|
||||
.Take(5)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetAuditEntries(
|
||||
int? limit,
|
||||
string? query,
|
||||
|
||||
@@ -27,7 +27,6 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
return Results.BadRequest(new { message = $"A season for {request.Year} already exists." });
|
||||
}
|
||||
|
||||
var showStreamUrl = NormalizeSeasonStreamUrl(request.ShowStreamUrl);
|
||||
var settings = await db.SiteSettings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(item => item.Id == 1, context.RequestAborted);
|
||||
@@ -37,7 +36,7 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
{
|
||||
Year = request.Year,
|
||||
Name = request.Name.Trim(),
|
||||
ShowStreamUrl = showStreamUrl,
|
||||
IsDemo = false,
|
||||
CurrentPhase = request.CurrentPhase.Trim(),
|
||||
IsCurrent = request.IsCurrent,
|
||||
IsCommunityOnly = request.IsCommunityOnly,
|
||||
@@ -125,7 +124,6 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
{
|
||||
request.IsCurrent,
|
||||
request.IsCommunityOnly,
|
||||
showStreamUrl,
|
||||
request.CurrentPhase,
|
||||
request.ShowDate,
|
||||
request.ShowStartsAt,
|
||||
|
||||
@@ -208,6 +208,23 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
item.Candidate.Platform))
|
||||
.ToArrayAsync();
|
||||
|
||||
var workflowRules = WorkflowRuleSettings.Read(season, settings);
|
||||
var votingEntryRows = await db.VoteEntries
|
||||
.AsNoTracking()
|
||||
.Where(item => item.Category.SeasonId == seasonId)
|
||||
.Select(item => new AdminVotingEntryRow(
|
||||
item.BallotId,
|
||||
item.CategoryId,
|
||||
item.CandidateId))
|
||||
.ToArrayAsync();
|
||||
var votingWorkspace = BuildVotingWorkspace(
|
||||
categories,
|
||||
candidates,
|
||||
pendingNominations,
|
||||
resultItems,
|
||||
votingEntryRows,
|
||||
workflowRules);
|
||||
|
||||
var clipSubmissions = await db.ClipSubmissions
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
@@ -232,7 +249,7 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
season.Id,
|
||||
season.Year,
|
||||
season.Name,
|
||||
NormalizeSeasonStreamUrl(season.ShowStreamUrl),
|
||||
season.IsDemo,
|
||||
season.CurrentPhase,
|
||||
season.IsCurrent,
|
||||
season.IsCommunityOnly,
|
||||
@@ -244,6 +261,8 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
season.ReviewEndsAt,
|
||||
season.ShowDate,
|
||||
season.ShowStartsAt,
|
||||
season.WinnersPublishedAt,
|
||||
season.WinnersPublishedByTwitchId,
|
||||
subcategoryTemplates,
|
||||
categories,
|
||||
candidates,
|
||||
@@ -253,9 +272,15 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
settings?.TrackingReviewNotes ?? string.Empty,
|
||||
trackingRules.Source.ShowManualReviewNotesInReview,
|
||||
resultItems,
|
||||
votingWorkspace,
|
||||
clipSubmissions));
|
||||
}
|
||||
|
||||
private sealed record AdminVotingEntryRow(
|
||||
int BallotId,
|
||||
int CategoryId,
|
||||
int CandidateId);
|
||||
|
||||
private sealed record AdminNominationRow(
|
||||
int Id,
|
||||
int? CategoryId,
|
||||
@@ -330,6 +355,165 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
item.ReviewedAt);
|
||||
}
|
||||
|
||||
private static AdminVotingWorkspaceDto BuildVotingWorkspace(
|
||||
AdminCategoryItemDto[] categories,
|
||||
AdminCandidateItemDto[] candidates,
|
||||
AdminNominationReviewItemDto[] pendingNominations,
|
||||
AdminAwardResultItemDto[] results,
|
||||
AdminVotingEntryRow[] voteEntries,
|
||||
WorkflowRuleSetting[] workflowRules)
|
||||
{
|
||||
var resultMap = results.ToDictionary(item => item.CategoryId);
|
||||
var totalBallots = voteEntries.Select(item => item.BallotId).Distinct().Count();
|
||||
var voteCountByCategory = voteEntries
|
||||
.GroupBy(item => item.CategoryId)
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
var ballotCountByCategory = voteEntries
|
||||
.GroupBy(item => item.CategoryId)
|
||||
.ToDictionary(group => group.Key, group => group.Select(item => item.BallotId).Distinct().Count());
|
||||
var voteCountByCandidate = voteEntries
|
||||
.GroupBy(item => (item.CategoryId, item.CandidateId))
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
|
||||
var winnerPlacementsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxWinnerPlacements);
|
||||
var recommendedNominatorsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.RecommendedNominatorsPerSubcategory);
|
||||
var winnerRequiresClipRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.WinnerRequiresClip);
|
||||
|
||||
var workspaceItems = categories
|
||||
.Select(category =>
|
||||
{
|
||||
var categoryCandidates = candidates
|
||||
.Where(candidate => candidate.CategoryId == category.Id)
|
||||
.ToArray();
|
||||
var categoryVoteCount = voteCountByCategory.TryGetValue(category.Id, out var voteCount) ? voteCount : 0;
|
||||
var categoryBallotCount = ballotCountByCategory.TryGetValue(category.Id, out var ballotCount) ? ballotCount : 0;
|
||||
var nominationCount = categoryCandidates.Sum(candidate => Math.Max(candidate.NominationTally, 0));
|
||||
var openReviewCount = pendingNominations.Count(item =>
|
||||
item.CategoryId == category.Id
|
||||
|| item.SuggestedCategoryId == category.Id);
|
||||
var existingResult = resultMap.GetValueOrDefault(category.Id);
|
||||
var maxVotes = categoryCandidates.Length == 0
|
||||
? 0
|
||||
: categoryCandidates.Max(candidate => voteCountByCandidate.GetValueOrDefault((category.Id, candidate.Id), 0));
|
||||
var topVoteTieCount = maxVotes <= 0
|
||||
? 0
|
||||
: categoryCandidates.Count(candidate => voteCountByCandidate.GetValueOrDefault((category.Id, candidate.Id), 0) == maxVotes);
|
||||
|
||||
var leaderboard = categoryCandidates
|
||||
.Select(candidate =>
|
||||
{
|
||||
var candidateVotes = voteCountByCandidate.GetValueOrDefault((category.Id, candidate.Id), 0);
|
||||
var hasWinnerConflict = CandidateHasWinnerConflict(candidate, category.Id, results, winnerPlacementsRule);
|
||||
return new AdminVotingCandidateRankDto(
|
||||
candidate.Id,
|
||||
candidate.DisplayName,
|
||||
candidate.ChannelSlug,
|
||||
candidate.Platform,
|
||||
candidateVotes,
|
||||
categoryVoteCount > 0 ? (int)Math.Round(candidateVotes * 100d / categoryVoteCount) : 0,
|
||||
Math.Max(candidate.NominationTally, 0),
|
||||
!string.IsNullOrWhiteSpace(candidate.ClipCompilationUrl),
|
||||
string.IsNullOrWhiteSpace(candidate.ClipEmbedStatus) ? "unchecked" : candidate.ClipEmbedStatus,
|
||||
hasWinnerConflict,
|
||||
existingResult?.CandidateId == candidate.Id,
|
||||
!string.Equals(candidate.AcceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase),
|
||||
maxVotes > 0 && candidateVotes == maxVotes && topVoteTieCount > 1);
|
||||
})
|
||||
.OrderByDescending(item => item.Votes)
|
||||
.ThenByDescending(item => item.NominationTally)
|
||||
.ThenBy(item => item.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var readyCandidates = leaderboard.Count(item => item.IsAccepted);
|
||||
var leadingCandidate = leaderboard.FirstOrDefault();
|
||||
var hasMissingClip = winnerRequiresClipRule.Enabled
|
||||
&& leadingCandidate is not null
|
||||
&& !leadingCandidate.HasClip;
|
||||
var hasRuleConflict = leadingCandidate?.HasWinnerConflict ?? false;
|
||||
var hasOpenReviews = openReviewCount > 0;
|
||||
var hasSoftNominatorWarning = recommendedNominatorsRule.Enabled
|
||||
&& nominationCount < Math.Max(1, recommendedNominatorsRule.Limit);
|
||||
var winnerReady = existingResult is not null
|
||||
|| leadingCandidate is not null
|
||||
&& leadingCandidate.IsAccepted
|
||||
&& !hasMissingClip
|
||||
&& !hasRuleConflict
|
||||
&& !hasOpenReviews;
|
||||
|
||||
return new AdminVotingCategoryWorkspaceItemDto(
|
||||
category.Id,
|
||||
category.GroupName,
|
||||
category.Name,
|
||||
category.SortOrder,
|
||||
category.ViewerRangeMin,
|
||||
category.ViewerRangeMax,
|
||||
categoryVoteCount,
|
||||
categoryBallotCount,
|
||||
categoryCandidates.Length,
|
||||
readyCandidates,
|
||||
nominationCount,
|
||||
openReviewCount,
|
||||
existingResult is not null,
|
||||
winnerReady,
|
||||
topVoteTieCount > 1,
|
||||
hasMissingClip,
|
||||
hasRuleConflict,
|
||||
hasOpenReviews,
|
||||
hasSoftNominatorWarning,
|
||||
leaderboard);
|
||||
})
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ThenBy(item => item.GroupName, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(item => item.CategoryName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var summary = new AdminVotingWorkspaceSummaryDto(
|
||||
voteEntries.Length,
|
||||
totalBallots,
|
||||
workspaceItems.Length,
|
||||
workspaceItems.Count(item => item.VoteCount > 0),
|
||||
workspaceItems.Count(item => item.WinnerReady),
|
||||
workspaceItems.Count(item =>
|
||||
item.HasMissingClip
|
||||
|| item.HasRuleConflict
|
||||
|| item.HasOpenReviews
|
||||
|| item.HasTopVoteTie),
|
||||
workspaceItems.Count(item => item.HasWinner));
|
||||
|
||||
return new AdminVotingWorkspaceDto(summary, workspaceItems);
|
||||
}
|
||||
|
||||
private static bool CandidateHasWinnerConflict(
|
||||
AdminCandidateItemDto candidate,
|
||||
int categoryId,
|
||||
AdminAwardResultItemDto[] results,
|
||||
WorkflowRuleSetting winnerPlacementsRule)
|
||||
{
|
||||
if (!winnerPlacementsRule.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var identityKey = candidate.StreamerIdentityId.HasValue
|
||||
? $"identity:{candidate.StreamerIdentityId.Value}"
|
||||
: WorkflowRuleSettings.CandidateIdentityKey(candidate.DisplayName, candidate.ChannelSlug);
|
||||
|
||||
var existingWinnerCount = results.Count(result =>
|
||||
{
|
||||
if (result.CategoryId == categoryId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resultIdentityKey = result.StreamerIdentityId.HasValue
|
||||
? $"identity:{result.StreamerIdentityId.Value}"
|
||||
: WorkflowRuleSettings.CandidateIdentityKey(result.CandidateDisplayName, result.CandidateChannelSlug);
|
||||
return string.Equals(resultIdentityKey, identityKey, StringComparison.Ordinal);
|
||||
});
|
||||
|
||||
return existingWinnerCount >= winnerPlacementsRule.Limit;
|
||||
}
|
||||
|
||||
private static AdminNominationReviewGroupDto[] BuildNominationReviewGroups(
|
||||
IEnumerable<AdminNominationRow> rows,
|
||||
IEnumerable<dynamic> categoryRows,
|
||||
|
||||
@@ -17,7 +17,10 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
item.Name,
|
||||
item.CurrentPhase,
|
||||
item.IsCurrent,
|
||||
item.Categories.Count))
|
||||
item.IsDemo,
|
||||
item.Categories.Count,
|
||||
item.WinnersPublishedAt,
|
||||
item.WinnersPublishedByTwitchId))
|
||||
.ToArrayAsync();
|
||||
|
||||
return Results.Ok(seasons);
|
||||
|
||||
@@ -72,6 +72,14 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, Backend.Security.AdminPermissionCatalog.Winners))
|
||||
.WithName("DeleteAdminResult")
|
||||
.WithOpenApi();
|
||||
group.MapPost("/seasons/{seasonId:int}/winners/publish", PublishWinners)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, Backend.Security.AdminPermissionCatalog.Winners))
|
||||
.WithName("PublishAdminSeasonWinners")
|
||||
.WithOpenApi();
|
||||
group.MapPost("/seasons/{seasonId:int}/winners/unpublish", UnpublishWinners)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, Backend.Security.AdminPermissionCatalog.Winners))
|
||||
.WithName("UnpublishAdminSeasonWinners")
|
||||
.WithOpenApi();
|
||||
group.MapGet("/seasons/{seasonId:int}/workflow-rules", GetWorkflowRules)
|
||||
.AddEndpointFilter((context, next) => AdminEndpointConventions.RequirePermission(context, next, Backend.Security.AdminPermissionCatalog.Settings))
|
||||
.WithName("GetAdminWorkflowRules")
|
||||
|
||||
@@ -44,6 +44,13 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
string ChannelSlug,
|
||||
string? ClipCompilationUrl);
|
||||
|
||||
private sealed record WinnerPublicationSnapshot(
|
||||
int CategoryId,
|
||||
int? StreamerIdentityId,
|
||||
string DisplayName,
|
||||
string ChannelSlug,
|
||||
string? ClipCompilationUrl);
|
||||
|
||||
private static IResult? ValidateSeasonRequest(CreateSeasonRequest request)
|
||||
{
|
||||
if (request.Year < 2020 || request.Year > 2100)
|
||||
@@ -108,11 +115,6 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeSeasonStreamUrl(string? showStreamUrl)
|
||||
{
|
||||
return SeasonMappings.NormalizeSeasonStreamUrl(showStreamUrl);
|
||||
}
|
||||
|
||||
private static bool IsKnownSeasonPhase(string? currentPhase)
|
||||
{
|
||||
var value = currentPhase?.Trim().ToLowerInvariant() ?? string.Empty;
|
||||
@@ -156,7 +158,17 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
var issueList = issues.ToArray();
|
||||
return Results.BadRequest(new
|
||||
{
|
||||
message = $"Public-/Archiv-Readiness blockiert: {string.Join(" ", issueList)}",
|
||||
message = $"Landingpage-Freigabe blockiert: {string.Join(" ", issueList)}",
|
||||
issues = issueList,
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult CreateWinnerPublicationError(IEnumerable<string> issues)
|
||||
{
|
||||
var issueList = issues.ToArray();
|
||||
return Results.BadRequest(new
|
||||
{
|
||||
message = $"Gewinner-Freigabe blockiert: {string.Join(" ", issueList)}",
|
||||
issues = issueList,
|
||||
});
|
||||
}
|
||||
@@ -421,6 +433,87 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
return issues.ToArray();
|
||||
}
|
||||
|
||||
private static async Task<string[]> BuildWinnerPublicationIssuesAsync(
|
||||
AwardsDbContext db,
|
||||
int seasonId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var categoryIds = await db.Categories
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
.Select(item => item.Id)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
var issues = new List<string>();
|
||||
if (categoryIds.Length == 0)
|
||||
{
|
||||
issues.Add("Mindestens eine Kategorie ist erforderlich.");
|
||||
return issues.ToArray();
|
||||
}
|
||||
|
||||
var resultSnapshots = await db.Results
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
.Select(item => new WinnerPublicationSnapshot(
|
||||
item.CategoryId,
|
||||
item.Candidate.StreamerIdentityId,
|
||||
item.Candidate.DisplayName,
|
||||
item.Candidate.ChannelSlug,
|
||||
item.Candidate.ClipCompilationUrl))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
var categoriesWithResults = resultSnapshots
|
||||
.Select(item => item.CategoryId)
|
||||
.Distinct()
|
||||
.Count();
|
||||
var missingResults = Math.Max(0, categoryIds.Length - categoriesWithResults);
|
||||
if (missingResults > 0)
|
||||
{
|
||||
issues.Add($"{missingResults} Kategorien haben noch keinen Gewinner.");
|
||||
}
|
||||
|
||||
var openReviewCount = await db.Nominations
|
||||
.AsNoTracking()
|
||||
.CountAsync(item => item.SeasonId == seasonId && item.Status == "pending", cancellationToken);
|
||||
if (openReviewCount > 0)
|
||||
{
|
||||
issues.Add($"{openReviewCount} Nominierungs-Review(s) sind noch offen.");
|
||||
}
|
||||
|
||||
var workflowRules = await LoadWorkflowRulesAsync(db, seasonId, cancellationToken);
|
||||
var winnerRequiresClipRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.WinnerRequiresClip);
|
||||
if (WorkflowRuleSettings.ShouldBlock(winnerRequiresClipRule))
|
||||
{
|
||||
var missingWinnerClipCount = resultSnapshots.Count(item => string.IsNullOrWhiteSpace(item.ClipCompilationUrl));
|
||||
if (missingWinnerClipCount > 0)
|
||||
{
|
||||
issues.Add($"{missingWinnerClipCount} Gewinner haben noch keinen gepflegten Clip-Link.");
|
||||
}
|
||||
}
|
||||
|
||||
var winnerPlacementsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxWinnerPlacements);
|
||||
if (WorkflowRuleSettings.ShouldBlock(winnerPlacementsRule))
|
||||
{
|
||||
var winnerOverflow = resultSnapshots
|
||||
.GroupBy(item => ResolveCandidateIdentityKey(item.StreamerIdentityId, item.DisplayName, item.ChannelSlug))
|
||||
.Select(group => new
|
||||
{
|
||||
Count = group.Count(),
|
||||
DisplayName = group.Select(item => item.DisplayName).FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)) ?? "Unbekannt",
|
||||
})
|
||||
.Where(item => item.Count > winnerPlacementsRule.Limit)
|
||||
.OrderByDescending(item => item.Count)
|
||||
.FirstOrDefault();
|
||||
if (winnerOverflow is not null)
|
||||
{
|
||||
issues.Add(
|
||||
$"Workflow-Regel blockiert: {winnerOverflow.DisplayName} hat bereits {winnerOverflow.Count} Gewinnerplatz(e). Limit: {winnerPlacementsRule.Limit}.");
|
||||
}
|
||||
}
|
||||
|
||||
return issues.ToArray();
|
||||
}
|
||||
|
||||
private static string ResolveCandidateIdentityKey(int? streamerIdentityId, string displayName, string channelSlug)
|
||||
{
|
||||
if (streamerIdentityId.HasValue)
|
||||
|
||||
@@ -73,6 +73,7 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId);
|
||||
var existingResult = await db.Results.FirstOrDefaultAsync(item =>
|
||||
item.SeasonId == seasonId
|
||||
&& item.CategoryId == request.CategoryId);
|
||||
@@ -94,6 +95,13 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
existingResult.CategoryName = category.Name;
|
||||
}
|
||||
|
||||
var wasPublished = season?.WinnersPublishedAt is not null;
|
||||
if (wasPublished && season is not null)
|
||||
{
|
||||
season.WinnersPublishedAt = null;
|
||||
season.WinnersPublishedByTwitchId = null;
|
||||
}
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"result.set",
|
||||
@@ -106,6 +114,7 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
categoryId = request.CategoryId,
|
||||
candidateId = request.CandidateId,
|
||||
candidateName = candidate.DisplayName,
|
||||
unpublishedWinners = wasPublished,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
@@ -130,12 +139,20 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
var result = await db.Results
|
||||
.Include(item => item.Category)
|
||||
.Include(item => item.Candidate)
|
||||
.Include(item => item.Season)
|
||||
.FirstOrDefaultAsync(item => item.Id == resultId);
|
||||
if (result is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var wasPublished = result.Season.WinnersPublishedAt is not null;
|
||||
if (wasPublished)
|
||||
{
|
||||
result.Season.WinnersPublishedAt = null;
|
||||
result.Season.WinnersPublishedByTwitchId = null;
|
||||
}
|
||||
|
||||
db.Results.Remove(result);
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
@@ -149,10 +166,98 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
result.CategoryId,
|
||||
result.CandidateId,
|
||||
candidateName = result.Candidate.DisplayName,
|
||||
unpublishedWinners = wasPublished,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(new { deleted = true, resultId });
|
||||
}
|
||||
|
||||
private static async Task<IResult> PublishWinners(
|
||||
HttpContext context,
|
||||
int seasonId,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId, context.RequestAborted);
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var issues = await BuildWinnerPublicationIssuesAsync(db, seasonId, context.RequestAborted);
|
||||
if (issues.Length > 0)
|
||||
{
|
||||
return CreateWinnerPublicationError(issues);
|
||||
}
|
||||
|
||||
season.WinnersPublishedAt = DateTimeOffset.UtcNow;
|
||||
season.WinnersPublishedByTwitchId = session.TwitchUserId;
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"winners.publish",
|
||||
"season",
|
||||
season.Id.ToString(),
|
||||
$"Gewinner für {season.Year} wurden veröffentlicht.",
|
||||
new
|
||||
{
|
||||
seasonId,
|
||||
season.Year,
|
||||
season.WinnersPublishedAt,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(new
|
||||
{
|
||||
saved = true,
|
||||
seasonId,
|
||||
winnersPublishedAt = season.WinnersPublishedAt,
|
||||
winnersPublishedByTwitchId = season.WinnersPublishedByTwitchId,
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> UnpublishWinners(
|
||||
HttpContext context,
|
||||
int seasonId,
|
||||
AwardsDbContext db,
|
||||
IAdminAuditService adminAuditService)
|
||||
{
|
||||
var session = AdminEndpointConventions.CurrentSession(context);
|
||||
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId, context.RequestAborted);
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var previousPublishedAt = season.WinnersPublishedAt;
|
||||
season.WinnersPublishedAt = null;
|
||||
season.WinnersPublishedByTwitchId = null;
|
||||
|
||||
adminAuditService.AddEntry(
|
||||
session.TwitchUserId,
|
||||
"winners.unpublish",
|
||||
"season",
|
||||
season.Id.ToString(),
|
||||
$"Gewinner für {season.Year} wurden von der Landingpage zurückgenommen.",
|
||||
new
|
||||
{
|
||||
seasonId,
|
||||
season.Year,
|
||||
previousPublishedAt,
|
||||
},
|
||||
RequestMetadataReader.Read(context));
|
||||
|
||||
await db.SaveChangesAsync(context.RequestAborted);
|
||||
return Results.Ok(new
|
||||
{
|
||||
saved = true,
|
||||
seasonId,
|
||||
winnersPublishedAt = season.WinnersPublishedAt,
|
||||
winnersPublishedByTwitchId = season.WinnersPublishedByTwitchId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
return Results.BadRequest(new { message = $"A season for {request.Year} already exists." });
|
||||
}
|
||||
|
||||
var showStreamUrl = NormalizeSeasonStreamUrl(request.ShowStreamUrl);
|
||||
|
||||
var wasCurrent = season.IsCurrent;
|
||||
var previousPhase = season.CurrentPhase;
|
||||
var previousPhaseKey = SeasonMappings.NormalizePhaseKey(previousPhase);
|
||||
@@ -58,7 +56,6 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
|
||||
season.Year = request.Year;
|
||||
season.Name = request.Name.Trim();
|
||||
season.ShowStreamUrl = showStreamUrl;
|
||||
season.CurrentPhase = request.CurrentPhase.Trim();
|
||||
season.IsCommunityOnly = request.IsCommunityOnly;
|
||||
season.NominationStartsAt = request.NominationStartsAt;
|
||||
@@ -98,7 +95,6 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
{
|
||||
request.Year,
|
||||
request.Name,
|
||||
showStreamUrl,
|
||||
previousPhase,
|
||||
request.CurrentPhase,
|
||||
wasCurrent,
|
||||
|
||||
@@ -86,6 +86,22 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.SponsorsContent,
|
||||
settings.ShowactsUrl,
|
||||
settings.ShowactsContent,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerText),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerLiveButtonLabel),
|
||||
settings.StreamBannerLiveButtonUrl,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerLockedButtonLabel),
|
||||
settings.StreamBannerUseCompletedContent,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedText),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.StreamBannerCompletedButtonLabel),
|
||||
settings.StreamBannerCompletedButtonUrl,
|
||||
SeasonMappings.NormalizePlainTextContent(settings.AwardsSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.AwardsSectionDescription),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(settings.SubcategoriesSectionDescription),
|
||||
SeasonMappings.ReadSocialLinks(settings),
|
||||
SeasonMappings.ReadFaqItems(settings),
|
||||
settings.ShowactFormSchemaJson ?? "[]"));
|
||||
@@ -134,6 +150,22 @@ public static class AdminSiteSettingsEndpoints
|
||||
settings.SponsorsContent = request.SponsorsContent.Trim();
|
||||
settings.ShowactsUrl = normalizedUrls.ShowactsUrl;
|
||||
settings.ShowactsContent = request.ShowactsContent.Trim();
|
||||
settings.StreamBannerEyebrow = SeasonMappings.NormalizePlainTextContent(request.StreamBannerEyebrow);
|
||||
settings.StreamBannerTitle = SeasonMappings.NormalizePlainTextContent(request.StreamBannerTitle);
|
||||
settings.StreamBannerText = SeasonMappings.NormalizePlainTextContent(request.StreamBannerText);
|
||||
settings.StreamBannerLiveButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerLiveButtonLabel);
|
||||
settings.StreamBannerLiveButtonUrl = normalizedUrls.StreamBannerLiveButtonUrl;
|
||||
settings.StreamBannerLockedButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerLockedButtonLabel);
|
||||
settings.StreamBannerUseCompletedContent = request.StreamBannerUseCompletedContent;
|
||||
settings.StreamBannerCompletedEyebrow = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedEyebrow);
|
||||
settings.StreamBannerCompletedTitle = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedTitle);
|
||||
settings.StreamBannerCompletedText = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedText);
|
||||
settings.StreamBannerCompletedButtonLabel = SeasonMappings.NormalizePlainTextContent(request.StreamBannerCompletedButtonLabel);
|
||||
settings.StreamBannerCompletedButtonUrl = normalizedUrls.StreamBannerCompletedButtonUrl;
|
||||
settings.AwardsSectionTitle = SeasonMappings.NormalizePlainTextContent(request.AwardsSectionTitle);
|
||||
settings.AwardsSectionDescription = SeasonMappings.NormalizePlainTextContent(request.AwardsSectionDescription);
|
||||
settings.SubcategoriesSectionTitle = SeasonMappings.NormalizePlainTextContent(request.SubcategoriesSectionTitle);
|
||||
settings.SubcategoriesSectionDescription = SeasonMappings.NormalizePlainTextContent(request.SubcategoriesSectionDescription);
|
||||
settings.SocialLinksJson = JsonSerializer.Serialize(socialLinks);
|
||||
settings.FaqJson = JsonSerializer.Serialize(request.Faq ?? []);
|
||||
settings.ShowactFormSchemaJson = request.ShowactFormSchemaJson ?? "[]";
|
||||
@@ -171,7 +203,9 @@ public static class AdminSiteSettingsEndpoints
|
||||
|| !TryNormalizePublicUrl(request.ImprintUrl, "Impressum-Link", out var imprintUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.ContactUrl, "Kontakt-Link", out var contactUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.SponsorsUrl, "Sponsoren-Link", out var sponsorsUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.ShowactsUrl, "Showact-Link", out var showactsUrl, out errorMessage))
|
||||
|| !TryNormalizePublicUrl(request.ShowactsUrl, "Showact-Link", out var showactsUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.StreamBannerLiveButtonUrl, "Finale-Banner Button-Link", out var streamBannerLiveButtonUrl, out errorMessage)
|
||||
|| !TryNormalizePublicUrl(request.StreamBannerCompletedButtonUrl, "Finale-Banner Abschluss-Link", out var streamBannerCompletedButtonUrl, out errorMessage))
|
||||
{
|
||||
return Results.BadRequest(new { message = errorMessage });
|
||||
}
|
||||
@@ -185,6 +219,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
ContactUrl = contactUrl,
|
||||
SponsorsUrl = sponsorsUrl,
|
||||
ShowactsUrl = showactsUrl,
|
||||
StreamBannerLiveButtonUrl = streamBannerLiveButtonUrl,
|
||||
StreamBannerCompletedButtonUrl = streamBannerCompletedButtonUrl,
|
||||
};
|
||||
|
||||
var normalizedSocialLinks = new List<PublicSocialLinkDto>();
|
||||
@@ -231,6 +267,8 @@ public static class AdminSiteSettingsEndpoints
|
||||
public string ContactUrl { get; set; } = string.Empty;
|
||||
public string SponsorsUrl { get; set; } = string.Empty;
|
||||
public string ShowactsUrl { get; set; } = string.Empty;
|
||||
public string StreamBannerLiveButtonUrl { get; set; } = string.Empty;
|
||||
public string StreamBannerCompletedButtonUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetOptionalFeatureSettings(AwardsDbContext db)
|
||||
|
||||
@@ -19,19 +19,20 @@ public static class AdminTeamEndpoints
|
||||
|
||||
private static readonly AdminTeamPermissionDto[] PermissionCatalog =
|
||||
[
|
||||
new(AdminPermissionCatalog.Dashboard, "Dashboard", "Live-Lage, Aufgaben und Checks sehen.", "/admin/dashboard", true),
|
||||
new(AdminPermissionCatalog.Years, "Jahre", "Award-Jahre anlegen und pflegen.", "/admin/years", false),
|
||||
new(AdminPermissionCatalog.Nominations, "Nominierungen", "Nominierungen prüfen und entscheiden.", "/admin/nominations", false),
|
||||
new(AdminPermissionCatalog.Categories, "Kategorien", "Kategorien und Limits verwalten.", "/admin/categories", false),
|
||||
new(AdminPermissionCatalog.Candidates, "Kandidaten", "Kandidatenbasis bearbeiten.", "/admin/candidates", false),
|
||||
new(AdminPermissionCatalog.Clips, "Clips", "Clip-Einreichungen prüfen.", "/admin/clips", false),
|
||||
new(AdminPermissionCatalog.Risk, "Risiko", "Flags, Regeln und Moderationsrisiken sehen.", "/admin/risk", true),
|
||||
new(AdminPermissionCatalog.Audit, "Audit-Log", "Admin-Aktionen nachvollziehen.", "/admin/users-logs", true),
|
||||
new(AdminPermissionCatalog.Analytics, "Analytics", "Metriken und Rankings lesen.", "/admin/analytics", true),
|
||||
new(AdminPermissionCatalog.Winners, "Gewinner", "Finale Ergebnisse pflegen und freigeben.", "/admin/winners", false),
|
||||
new(AdminPermissionCatalog.Content, "Landingpage", "FAQ, Links, Datenschutz und öffentliche Inhalte pflegen.", "/admin/content", false),
|
||||
new(AdminPermissionCatalog.Settings, "Einstellungen", "Systemchecks, Demo-Zugang und Wartung sehen.", "/admin/settings", true),
|
||||
new(AdminPermissionCatalog.Team, "Team", "Mitglieder, Rollen und Berechtigungen verwalten.", "/admin/team", false),
|
||||
new(AdminPermissionCatalog.Dashboard, "Dashboard", "Live-Lage, Aufgaben und Checks sehen.", "Betrieb", "/admin/dashboard", true),
|
||||
new(AdminPermissionCatalog.Nominations, "Nominierungen", "Nominierungen prüfen und entscheiden.", "Betrieb", "/admin/nominations", false),
|
||||
new(AdminPermissionCatalog.Years, "Jahre", "Award-Jahre anlegen und pflegen.", "Awards", "/admin/years", false),
|
||||
new(AdminPermissionCatalog.Categories, "Kategorien", "Hauptkategorien, Unterkategorien und Limits verwalten.", "Awards", "/admin/categories", false),
|
||||
new(AdminPermissionCatalog.Candidates, "Kandidaten", "Kandidatenbasis, Clips und Annahmestatus pflegen.", "Awards", "/admin/candidates", false),
|
||||
new(AdminPermissionCatalog.Clips, "Clips", "Optionale Clip-Einreichungen prüfen.", "Awards", "/admin/clips", false),
|
||||
new(AdminPermissionCatalog.Content, "Landingpage", "FAQ, Links, Footer, Showacts und öffentliche Inhalte pflegen.", "Landingpage", "/admin/content", false),
|
||||
new(AdminPermissionCatalog.Risk, "Risiko", "Flags, Regeln und Moderationsrisiken sehen.", "Kontrolle", "/admin/risk", true),
|
||||
new(AdminPermissionCatalog.Audit, "Audit-Log", "Admin-Aktionen nachvollziehen.", "Kontrolle", "/admin/users-logs", true),
|
||||
new(AdminPermissionCatalog.Analytics, "Analytics", "Jahresmetriken und Überblick lesen.", "Auswertung", "/admin/analytics", true),
|
||||
new(AdminPermissionCatalog.Voting, "Voting", "Stimmenlage und Gewinner-Vorbereitung sehen.", "Auswertung", "/admin/voting", true),
|
||||
new(AdminPermissionCatalog.Winners, "Gewinner", "Finale Ergebnisse pflegen und freigeben.", "Auswertung", "/admin/winners", false),
|
||||
new(AdminPermissionCatalog.Settings, "Einstellungen", "Systemchecks, Demo-Zugang, Wartung und Workflow-Steuerung sehen.", "Einstellungen", "/admin/settings", true),
|
||||
new(AdminPermissionCatalog.Team, "Team", "Mitglieder, Rollen und Berechtigungen verwalten.", "Einstellungen", "/admin/team", false),
|
||||
];
|
||||
|
||||
private static readonly AdminTeamRoleDto[] DefaultRoles =
|
||||
|
||||
@@ -29,21 +29,26 @@ public static partial class PublicEndpoints
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var showactApplicationsOpenNow = ShowactApplicationSchedule.IsOpenNow(siteSettings, today);
|
||||
|
||||
var canExposeCurrentSeasonWinners = CanExposeCurrentSeasonWinners(season.CurrentPhase);
|
||||
var latestPublishedWinnerYear = await db.Results
|
||||
.AsNoTracking()
|
||||
.Where(result => result.Season.WinnersPublishedAt != null)
|
||||
.Select(result => (int?)result.Season.Year)
|
||||
.MaxAsync();
|
||||
|
||||
var winnerPreviewRows = await db.Results
|
||||
.AsNoTracking()
|
||||
.Include(result => result.Season)
|
||||
.Include(result => result.Candidate)
|
||||
.Where(result =>
|
||||
result.Season.Year < season.Year
|
||||
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
|
||||
.Where(result => result.Season.WinnersPublishedAt != null
|
||||
&& latestPublishedWinnerYear != null
|
||||
&& result.Season.Year == latestPublishedWinnerYear.Value)
|
||||
.OrderByDescending(result => result.Season.Year)
|
||||
.ThenBy(result => result.CategoryName)
|
||||
.Take(8)
|
||||
.Select(result => new
|
||||
{
|
||||
Year = result.Season.Year,
|
||||
CategoryGroup = result.Category.GroupName,
|
||||
result.CategoryName,
|
||||
WinnerName = result.Candidate.DisplayName,
|
||||
WinnerSlug = result.Candidate.ChannelSlug,
|
||||
@@ -58,6 +63,7 @@ public static partial class PublicEndpoints
|
||||
var winnerPreviewItems = winnerPreviewRows
|
||||
.Select(result => new WinnerPreviewDto(
|
||||
result.Year,
|
||||
result.CategoryGroup,
|
||||
result.CategoryName,
|
||||
result.WinnerName,
|
||||
result.WinnerSlug,
|
||||
@@ -71,9 +77,9 @@ public static partial class PublicEndpoints
|
||||
|
||||
var archiveYearRows = await db.Results
|
||||
.AsNoTracking()
|
||||
.Where(result =>
|
||||
result.Season.Year < season.Year
|
||||
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
|
||||
.Where(result => result.Season.WinnersPublishedAt != null
|
||||
&& latestPublishedWinnerYear != null
|
||||
&& result.Season.Year < latestPublishedWinnerYear.Value)
|
||||
.GroupBy(result => result.Season.Year)
|
||||
.Select(group => new
|
||||
{
|
||||
@@ -100,6 +106,10 @@ public static partial class PublicEndpoints
|
||||
.ThenBy(category => category.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var first = ordered[0];
|
||||
var groupDescription = ordered
|
||||
.Select(category => category.Description?.Trim())
|
||||
.FirstOrDefault(description => !string.IsNullOrWhiteSpace(description))
|
||||
?? string.Empty;
|
||||
var maxNomineesPerUser = ordered
|
||||
.Select(category => category.MaxNomineesPerUser)
|
||||
.Where(value => value > 0)
|
||||
@@ -110,7 +120,7 @@ public static partial class PublicEndpoints
|
||||
first.Id,
|
||||
first.GroupName,
|
||||
first.GroupName,
|
||||
first.Description,
|
||||
groupDescription,
|
||||
maxNomineesPerUser);
|
||||
})
|
||||
.OrderBy(category => publicCategories
|
||||
@@ -124,7 +134,6 @@ public static partial class PublicEndpoints
|
||||
season.Name,
|
||||
season.ShowDate,
|
||||
season.ShowStartsAt,
|
||||
SeasonMappings.NormalizeSeasonStreamUrl(season.ShowStreamUrl),
|
||||
season.CurrentPhase,
|
||||
season.IsCommunityOnly,
|
||||
"Twitch",
|
||||
@@ -146,6 +155,23 @@ public static partial class PublicEndpoints
|
||||
siteSettings.ShareDiscordUrl,
|
||||
siteSettings.PrivacyEmail,
|
||||
siteSettings.PrivacyPolicyContent,
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.AwardsSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.AwardsSectionDescription),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.SubcategoriesSectionTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.SubcategoriesSectionDescription),
|
||||
new PublicStreamBannerContentDto(
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerText),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerLiveButtonLabel),
|
||||
SeasonMappings.NormalizeSeasonStreamUrl(siteSettings.StreamBannerLiveButtonUrl),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerLockedButtonLabel),
|
||||
siteSettings.StreamBannerUseCompletedContent,
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerCompletedEyebrow),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerCompletedTitle),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerCompletedText),
|
||||
SeasonMappings.NormalizePlainTextContent(siteSettings.StreamBannerCompletedButtonLabel),
|
||||
SeasonMappings.NormalizeSeasonStreamUrl(siteSettings.StreamBannerCompletedButtonUrl)),
|
||||
SeasonMappings.ReadSocialLinks(siteSettings),
|
||||
SeasonMappings.BuildFooterLinks(siteSettings)),
|
||||
new PublicFeatureFlagsDto(
|
||||
|
||||
@@ -57,8 +57,6 @@ public static partial class PublicEndpoints
|
||||
category.GroupName,
|
||||
category.Description,
|
||||
category.MaxNomineesPerUser,
|
||||
category.ViewerRangeMin,
|
||||
category.ViewerRangeMax,
|
||||
category.Candidates
|
||||
.Where(candidate => !string.Equals(candidate.AcceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(candidate =>
|
||||
|
||||
@@ -12,14 +12,24 @@ public static partial class PublicEndpoints
|
||||
var season = await db.Seasons
|
||||
.AsNoTracking()
|
||||
.Where(item => item.Year == year)
|
||||
.Select(item => new { item.Id, item.Year, item.IsCurrent, item.CurrentPhase })
|
||||
.Select(item => new { item.Id, item.Year, item.WinnersPublishedAt })
|
||||
.FirstOrDefaultAsync();
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
if (season.IsCurrent && !CanExposeCurrentSeasonWinners(season.CurrentPhase))
|
||||
if (season.WinnersPublishedAt is null)
|
||||
{
|
||||
return Results.Ok(new WinnerArchiveResponse(year, []));
|
||||
}
|
||||
|
||||
var latestPublishedWinnerYear = await db.Results
|
||||
.AsNoTracking()
|
||||
.Where(result => result.Season.WinnersPublishedAt != null)
|
||||
.Select(result => (int?)result.Season.Year)
|
||||
.MaxAsync();
|
||||
if (latestPublishedWinnerYear == season.Year)
|
||||
{
|
||||
return Results.Ok(new WinnerArchiveResponse(year, []));
|
||||
}
|
||||
@@ -31,6 +41,7 @@ public static partial class PublicEndpoints
|
||||
.OrderBy(result => result.CategoryName)
|
||||
.Select(result => new
|
||||
{
|
||||
CategoryGroup = result.Category.GroupName,
|
||||
result.CategoryName,
|
||||
WinnerName = result.Candidate.DisplayName,
|
||||
WinnerSlug = result.Candidate.ChannelSlug,
|
||||
@@ -44,6 +55,7 @@ public static partial class PublicEndpoints
|
||||
|
||||
var items = winnerRows
|
||||
.Select(result => new WinnerArchiveItemDto(
|
||||
result.CategoryGroup,
|
||||
result.CategoryName,
|
||||
result.WinnerName,
|
||||
result.WinnerSlug,
|
||||
@@ -57,10 +69,4 @@ public static partial class PublicEndpoints
|
||||
|
||||
return Results.Ok(new WinnerArchiveResponse(year, items));
|
||||
}
|
||||
|
||||
private static bool CanExposeCurrentSeasonWinners(string currentPhase)
|
||||
{
|
||||
var phaseKey = SeasonMappings.NormalizePhaseKey(currentPhase);
|
||||
return phaseKey is "completed";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user