Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season
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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Backend.Domain;
|
||||
using Backend.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
@@ -7,54 +8,137 @@ public static partial class SeedDataBootstrapper
|
||||
{
|
||||
private static async Task EnsureCategoriesAsync(AwardsDbContext db, Season season)
|
||||
{
|
||||
var seasonCategories = await db.Categories
|
||||
var categories = await db.Categories
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.ToArrayAsync();
|
||||
.ToListAsync();
|
||||
var templates = SeedCatalog.DefaultSubcategoryTemplates
|
||||
.Select((item, index) => item with { SortOrder = index + 1 })
|
||||
.ToArray();
|
||||
var usedCategories = new HashSet<Category>();
|
||||
|
||||
foreach (var category in seasonCategories)
|
||||
season.SubcategoryTemplatesJson = SeasonSubcategoryTemplateSettings.Serialize(templates);
|
||||
|
||||
var nextSortOrder = 1;
|
||||
foreach (var award in SeedCatalog.AwardCategorySeeds.OrderBy(item => item.SortOrder))
|
||||
{
|
||||
if (!SeedCatalog.LegacyCategorySlugMap.TryGetValue(category.Slug, out var targetSlug))
|
||||
foreach (var template in templates)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var category = FindReusableCategory(categories, award, template, usedCategories)
|
||||
?? new Category { SeasonId = season.Id };
|
||||
usedCategories.Add(category);
|
||||
|
||||
var target = SeedCatalog.CategorySeeds.First(item => item.Slug == targetSlug);
|
||||
category.GroupName = target.GroupName;
|
||||
category.Name = target.Name;
|
||||
category.Slug = target.Slug;
|
||||
category.Description = target.Description;
|
||||
category.SortOrder = target.SortOrder;
|
||||
category.MaxNomineesPerUser = 3;
|
||||
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();
|
||||
}
|
||||
|
||||
var existing = await db.Categories
|
||||
.Where(item => item.SeasonId == season.Id)
|
||||
.Select(item => item.Slug)
|
||||
.ToArrayAsync();
|
||||
var existingSlugs = existing.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var seed in SeedCatalog.CategorySeeds)
|
||||
private static async Task RemoveStaleCategoryDataAsync(AwardsDbContext db, Category[] staleCategories)
|
||||
{
|
||||
if (staleCategories.Length == 0)
|
||||
{
|
||||
if (existingSlugs.Contains(seed.Slug))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
db.Categories.Add(new Category
|
||||
{
|
||||
SeasonId = season.Id,
|
||||
GroupName = seed.GroupName,
|
||||
Name = seed.Name,
|
||||
Slug = seed.Slug,
|
||||
Description = seed.Description,
|
||||
SortOrder = seed.SortOrder,
|
||||
MaxNomineesPerUser = 3,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
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)
|
||||
@@ -142,4 +226,7 @@ public static partial class SeedDataBootstrapper
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCategorySlug(string awardSlug, string templateSlug) =>
|
||||
$"{SeasonSubcategoryTemplateSettings.Slugify(awardSlug)}-{SeasonSubcategoryTemplateSettings.Slugify(templateSlug)}";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user