b53c7fb736
Features: - Category viewer ranges + subcategory templates (admin group modal, tree workspace) - Nomination enrichment via TwitchTracker API (NominationEnrichmentService, TwitchTrackerViewerStatsProvider) with admin tracking rules editor - Nomination group tracker: CategoryGroupName as primary identifier, CategoryId stays as nullable legacy field; StreamerIdentity table - Dynamic showact application form builder (AdminShowactFormBuilder, ShowactApplicationSchedule) - Session idle timeout setting (AdminSessionTimeoutCard) - Share URLs for X and Discord (SiteSettings, public extras) - Workflow rules now stored per season (falls back to global SiteSettings) - New admin routes: settings/access, settings/workflows, tracking-rules - New admin review workspace with subcategory tabs - AdminCategoriesView rebuilt with group/subcategory modals Migrations (all additive): - AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges, AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates, AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule, AddSeasonWorkflowRulesJson Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
233 lines
9.3 KiB
C#
233 lines
9.3 KiB
C#
using Backend.Domain;
|
|
using Backend.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Backend.Data;
|
|
|
|
public static partial class SeedDataBootstrapper
|
|
{
|
|
private static async Task EnsureCategoriesAsync(AwardsDbContext db, Season season)
|
|
{
|
|
var categories = await db.Categories
|
|
.Where(item => item.SeasonId == season.Id)
|
|
.ToListAsync();
|
|
var templates = SeedCatalog.DefaultSubcategoryTemplates
|
|
.Select((item, index) => item with { SortOrder = index + 1 })
|
|
.ToArray();
|
|
var usedCategories = new HashSet<Category>();
|
|
|
|
season.SubcategoryTemplatesJson = SeasonSubcategoryTemplateSettings.Serialize(templates);
|
|
|
|
var nextSortOrder = 1;
|
|
foreach (var award in SeedCatalog.AwardCategorySeeds.OrderBy(item => item.SortOrder))
|
|
{
|
|
foreach (var template in templates)
|
|
{
|
|
var category = FindReusableCategory(categories, award, template, usedCategories)
|
|
?? new Category { SeasonId = season.Id };
|
|
usedCategories.Add(category);
|
|
|
|
category.GroupName = award.Name;
|
|
category.Name = template.Name;
|
|
category.Slug = BuildCategorySlug(award.Slug, template.Slug);
|
|
category.Description = award.Description;
|
|
category.SortOrder = nextSortOrder++;
|
|
category.MaxNomineesPerUser = 3;
|
|
category.ViewerRangeMin = template.ViewerRangeMin;
|
|
category.ViewerRangeMax = template.ViewerRangeMax;
|
|
|
|
if (category.Id == 0 && !db.Categories.Local.Any(item => ReferenceEquals(item, category)))
|
|
{
|
|
db.Categories.Add(category);
|
|
categories.Add(category);
|
|
}
|
|
}
|
|
}
|
|
|
|
var staleCategories = categories
|
|
.Where(item => item.Id > 0 && !usedCategories.Contains(item))
|
|
.ToArray();
|
|
await RemoveStaleCategoryDataAsync(db, staleCategories);
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private static async Task RemoveStaleCategoryDataAsync(AwardsDbContext db, Category[] staleCategories)
|
|
{
|
|
if (staleCategories.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var staleCategoryIds = staleCategories.Select(item => item.Id).ToArray();
|
|
var staleCandidateIds = await db.Candidates
|
|
.Where(item => staleCategoryIds.Contains(item.CategoryId))
|
|
.Select(item => item.Id)
|
|
.ToArrayAsync();
|
|
|
|
var staleVoteEntries = await db.VoteEntries
|
|
.Where(item => staleCategoryIds.Contains(item.CategoryId) || staleCandidateIds.Contains(item.CandidateId))
|
|
.ToArrayAsync();
|
|
db.VoteEntries.RemoveRange(staleVoteEntries);
|
|
|
|
var staleResults = await db.Results
|
|
.Where(item => staleCategoryIds.Contains(item.CategoryId) || staleCandidateIds.Contains(item.CandidateId))
|
|
.ToArrayAsync();
|
|
db.Results.RemoveRange(staleResults);
|
|
|
|
var affectedClips = await db.ClipSubmissions
|
|
.Where(item =>
|
|
(item.CategoryId != null && staleCategoryIds.Contains(item.CategoryId.Value))
|
|
|| (item.CandidateId != null && staleCandidateIds.Contains(item.CandidateId.Value)))
|
|
.ToArrayAsync();
|
|
foreach (var clip in affectedClips)
|
|
{
|
|
if (clip.CategoryId != null && staleCategoryIds.Contains(clip.CategoryId.Value))
|
|
{
|
|
clip.CategoryId = null;
|
|
}
|
|
|
|
if (clip.CandidateId != null && staleCandidateIds.Contains(clip.CandidateId.Value))
|
|
{
|
|
clip.CandidateId = null;
|
|
}
|
|
}
|
|
|
|
var affectedNominations = await db.Nominations
|
|
.Where(item =>
|
|
(item.CategoryId != null && staleCategoryIds.Contains(item.CategoryId.Value))
|
|
|| (item.SuggestedCategoryId != null && staleCategoryIds.Contains(item.SuggestedCategoryId.Value))
|
|
|| (item.CandidateId != null && staleCandidateIds.Contains(item.CandidateId.Value)))
|
|
.ToArrayAsync();
|
|
foreach (var nomination in affectedNominations)
|
|
{
|
|
if (nomination.CategoryId != null && staleCategoryIds.Contains(nomination.CategoryId.Value))
|
|
{
|
|
nomination.CategoryId = null;
|
|
}
|
|
|
|
if (nomination.SuggestedCategoryId != null && staleCategoryIds.Contains(nomination.SuggestedCategoryId.Value))
|
|
{
|
|
nomination.SuggestedCategoryId = null;
|
|
}
|
|
|
|
if (nomination.CandidateId != null && staleCandidateIds.Contains(nomination.CandidateId.Value))
|
|
{
|
|
nomination.CandidateId = null;
|
|
}
|
|
}
|
|
|
|
var staleCandidates = await db.Candidates
|
|
.Where(item => staleCandidateIds.Contains(item.Id))
|
|
.ToArrayAsync();
|
|
db.Candidates.RemoveRange(staleCandidates);
|
|
db.Categories.RemoveRange(staleCategories);
|
|
}
|
|
|
|
private static Category? FindReusableCategory(
|
|
List<Category> categories,
|
|
AwardCategorySeed award,
|
|
SeasonSubcategoryTemplateSetting template,
|
|
HashSet<Category> usedCategories)
|
|
{
|
|
var targetSlug = BuildCategorySlug(award.Slug, template.Slug);
|
|
|
|
return categories.FirstOrDefault(item => !usedCategories.Contains(item)
|
|
&& string.Equals(item.Slug, targetSlug, StringComparison.OrdinalIgnoreCase))
|
|
?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
|
|
&& string.Equals(item.GroupName, award.Name, StringComparison.OrdinalIgnoreCase)
|
|
&& string.Equals(item.Name, template.Name, StringComparison.OrdinalIgnoreCase))
|
|
?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
|
|
&& string.Equals(item.GroupName, award.LegacyGroupName, StringComparison.OrdinalIgnoreCase)
|
|
&& string.Equals(item.Name, template.Name, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static async Task EnsureCandidatesAsync(AwardsDbContext db, Season season, CandidateSeed[] seeds)
|
|
{
|
|
var categories = await db.Categories
|
|
.Where(item => item.SeasonId == season.Id)
|
|
.ToDictionaryAsync(item => item.Slug, StringComparer.OrdinalIgnoreCase);
|
|
var existing = await db.Candidates
|
|
.Where(item => item.SeasonId == season.Id)
|
|
.Select(item => new { item.CategoryId, item.DisplayName, item.ChannelSlug })
|
|
.ToArrayAsync();
|
|
var existingKeys = existing
|
|
.Select(item => $"{item.CategoryId}|{item.DisplayName}|{item.ChannelSlug}".ToLowerInvariant())
|
|
.ToHashSet();
|
|
|
|
foreach (var seed in seeds)
|
|
{
|
|
if (!categories.TryGetValue(seed.CategorySlug, out var category))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var key = $"{category.Id}|{seed.DisplayName}|{seed.ChannelSlug}".ToLowerInvariant();
|
|
if (existingKeys.Contains(key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
db.Candidates.Add(new Candidate
|
|
{
|
|
SeasonId = season.Id,
|
|
CategoryId = category.Id,
|
|
DisplayName = seed.DisplayName,
|
|
ChannelSlug = seed.ChannelSlug,
|
|
Platform = seed.Platform,
|
|
});
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private static async Task EnsureWinnersAsync(AwardsDbContext db, Season season, WinnerSeed[] seeds)
|
|
{
|
|
var categories = await db.Categories
|
|
.Where(item => item.SeasonId == season.Id)
|
|
.ToDictionaryAsync(item => item.Slug, StringComparer.OrdinalIgnoreCase);
|
|
var candidates = await db.Candidates
|
|
.Where(item => item.SeasonId == season.Id)
|
|
.ToArrayAsync();
|
|
var existingResults = await db.Results
|
|
.Where(item => item.SeasonId == season.Id)
|
|
.ToArrayAsync();
|
|
foreach (var result in existingResults)
|
|
{
|
|
if (categories.Values.FirstOrDefault(item => item.Id == result.CategoryId) is { } category
|
|
&& result.CategoryName != category.Name)
|
|
{
|
|
result.CategoryName = category.Name;
|
|
}
|
|
}
|
|
|
|
var existingResultCategoryIds = existingResults.Select(item => item.CategoryId).ToHashSet();
|
|
|
|
foreach (var seed in seeds)
|
|
{
|
|
if (!categories.TryGetValue(seed.CategorySlug, out var category) || existingResultCategoryIds.Contains(category.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var candidate = candidates.FirstOrDefault(item =>
|
|
item.CategoryId == category.Id
|
|
&& string.Equals(item.DisplayName, seed.DisplayName, StringComparison.OrdinalIgnoreCase));
|
|
if (candidate is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
db.Results.Add(new AwardResult
|
|
{
|
|
SeasonId = season.Id,
|
|
CategoryId = category.Id,
|
|
CandidateId = candidate.Id,
|
|
CategoryName = category.Name,
|
|
});
|
|
}
|
|
}
|
|
|
|
private static string BuildCategorySlug(string awardSlug, string templateSlug) =>
|
|
$"{SeasonSubcategoryTemplateSettings.Slugify(awardSlug)}-{SeasonSubcategoryTemplateSettings.Slugify(templateSlug)}";
|
|
}
|