Initial VTuber Awards implementation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VTSA_POSTGRES=Host=localhost;Port=5432;Database=vtuber_star_awards_dev;Username=postgres;Password=postgres
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseAppHost>false</UseAppHost>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@Backend_HostAddress = http://localhost:0
|
||||
|
||||
GET {{Backend_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,78 @@
|
||||
namespace Backend.Contracts;
|
||||
|
||||
public sealed record AdminMetricDto(string Label, int Value, string Note);
|
||||
|
||||
public sealed record AdminActivityDto(string Label, string Age);
|
||||
|
||||
public sealed record AdminTopCategoryDto(string Category, int Votes);
|
||||
|
||||
public sealed record AdminDashboardResponse(
|
||||
IEnumerable<AdminMetricDto> Metrics,
|
||||
IEnumerable<AdminActivityDto> Activities,
|
||||
IEnumerable<AdminTopCategoryDto> TopCategories);
|
||||
|
||||
public sealed record AdminSeasonListItemDto(
|
||||
int Id,
|
||||
int Year,
|
||||
string Name,
|
||||
string CurrentPhase,
|
||||
bool IsCurrent,
|
||||
int CategoryCount);
|
||||
|
||||
public sealed record AdminCategoryItemDto(
|
||||
int Id,
|
||||
string GroupName,
|
||||
string Name,
|
||||
string Slug,
|
||||
string Description,
|
||||
int SortOrder,
|
||||
int MaxNomineesPerUser,
|
||||
int CandidateCount);
|
||||
|
||||
public sealed record AdminCandidateItemDto(
|
||||
int Id,
|
||||
int CategoryId,
|
||||
string DisplayName,
|
||||
string ChannelSlug,
|
||||
string Platform);
|
||||
|
||||
public sealed record AdminNominationReviewItemDto(
|
||||
int Id,
|
||||
int CategoryId,
|
||||
string CategoryName,
|
||||
string SubmittedByTwitchId,
|
||||
string CandidateText,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public sealed record AdminSeasonDetailResponse(
|
||||
int Id,
|
||||
int Year,
|
||||
string Name,
|
||||
string CurrentPhase,
|
||||
bool IsCurrent,
|
||||
IEnumerable<AdminCategoryItemDto> Categories,
|
||||
IEnumerable<AdminCandidateItemDto> Candidates,
|
||||
IEnumerable<AdminNominationReviewItemDto> PendingNominations);
|
||||
|
||||
public sealed record UpdateSeasonRequest(
|
||||
string CurrentPhase,
|
||||
bool IsCurrent);
|
||||
|
||||
public sealed record UpsertCategoryRequest(
|
||||
string GroupName,
|
||||
string Name,
|
||||
string Slug,
|
||||
string Description,
|
||||
int SortOrder,
|
||||
int MaxNomineesPerUser);
|
||||
|
||||
public sealed record UpsertCandidateRequest(
|
||||
int CategoryId,
|
||||
string DisplayName,
|
||||
string ChannelSlug,
|
||||
string Platform);
|
||||
|
||||
public sealed record ApproveNominationRequest(
|
||||
string? DisplayName,
|
||||
string? ChannelSlug,
|
||||
string? Platform);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Backend.Contracts;
|
||||
|
||||
public sealed record LoginRequest(
|
||||
string TwitchUserId,
|
||||
string DisplayName,
|
||||
string Role);
|
||||
|
||||
public sealed record AuthSessionDto(
|
||||
string SessionToken,
|
||||
string TwitchUserId,
|
||||
string DisplayName,
|
||||
string Role);
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace Backend.Contracts;
|
||||
|
||||
public sealed record TimelineItem(
|
||||
string Key,
|
||||
string Title,
|
||||
DateOnly StartsAt,
|
||||
DateOnly EndsAt,
|
||||
string State);
|
||||
|
||||
public sealed record FeaturedCategoryDto(
|
||||
int Id,
|
||||
string GroupName,
|
||||
string Name,
|
||||
string Description,
|
||||
int MaxNomineesPerUser);
|
||||
|
||||
public sealed record WinnerPreviewDto(
|
||||
int Year,
|
||||
string Category,
|
||||
string WinnerName,
|
||||
string WinnerSlug);
|
||||
|
||||
public sealed record FaqItemDto(string Question, string Answer);
|
||||
|
||||
public sealed record OverviewResponse(
|
||||
int SeasonId,
|
||||
int Year,
|
||||
string Title,
|
||||
DateOnly ShowDate,
|
||||
string CurrentPhase,
|
||||
bool IsCommunityOnly,
|
||||
string LoginProvider,
|
||||
IEnumerable<TimelineItem> Timeline,
|
||||
IEnumerable<FeaturedCategoryDto> FeaturedCategories,
|
||||
IEnumerable<WinnerPreviewDto> WinnersPreview,
|
||||
IEnumerable<FaqItemDto> Faq);
|
||||
|
||||
public sealed record CandidateSummaryDto(
|
||||
int Id,
|
||||
string DisplayName,
|
||||
string ChannelSlug,
|
||||
string Platform);
|
||||
|
||||
public sealed record PublicCategoryDetailDto(
|
||||
int Id,
|
||||
string Name,
|
||||
string GroupName,
|
||||
string Description,
|
||||
int MaxNomineesPerUser,
|
||||
IEnumerable<CandidateSummaryDto> Candidates);
|
||||
|
||||
public sealed record SeasonCategoriesResponse(
|
||||
int SeasonId,
|
||||
int Year,
|
||||
IEnumerable<PublicCategoryDetailDto> Categories);
|
||||
|
||||
public sealed record WinnerArchiveItemDto(
|
||||
string Category,
|
||||
string WinnerName,
|
||||
string WinnerSlug);
|
||||
|
||||
public sealed record WinnerArchiveResponse(
|
||||
int Year,
|
||||
IEnumerable<WinnerArchiveItemDto> Items);
|
||||
|
||||
public sealed record CreateNominationRequest(
|
||||
int Year,
|
||||
int CategoryId,
|
||||
string TwitchUserId,
|
||||
string[] Nominees);
|
||||
|
||||
public sealed record VoteEntryRequest(
|
||||
int CategoryId,
|
||||
int CandidateId);
|
||||
|
||||
public sealed record CreateVoteRequest(
|
||||
int SeasonId,
|
||||
string TwitchUserId,
|
||||
VoteEntryRequest[] Entries);
|
||||
@@ -0,0 +1,69 @@
|
||||
using Backend.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public sealed class AwardsDbContext(DbContextOptions<AwardsDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Season> Seasons => Set<Season>();
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
public DbSet<Candidate> Candidates => Set<Candidate>();
|
||||
public DbSet<AwardResult> Results => Set<AwardResult>();
|
||||
public DbSet<Nomination> Nominations => Set<Nomination>();
|
||||
public DbSet<VoteBallot> VoteBallots => Set<VoteBallot>();
|
||||
public DbSet<VoteEntry> VoteEntries => Set<VoteEntry>();
|
||||
public DbSet<UserSession> UserSessions => Set<UserSession>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Season>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.Year).IsUnique();
|
||||
entity.Property(item => item.Name).HasMaxLength(160);
|
||||
entity.Property(item => item.CurrentPhase).HasMaxLength(60);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Category>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.SeasonId, item.Slug }).IsUnique();
|
||||
entity.Property(item => item.GroupName).HasMaxLength(80);
|
||||
entity.Property(item => item.Name).HasMaxLength(120);
|
||||
entity.Property(item => item.Description).HasMaxLength(400);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Candidate>(entity =>
|
||||
{
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(120);
|
||||
entity.Property(item => item.ChannelSlug).HasMaxLength(120);
|
||||
entity.Property(item => item.Platform).HasMaxLength(40);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Nomination>(entity =>
|
||||
{
|
||||
entity.Property(item => item.SubmittedByTwitchId).HasMaxLength(120);
|
||||
entity.Property(item => item.CandidateText).HasMaxLength(120);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<VoteBallot>(entity =>
|
||||
{
|
||||
entity.Property(item => item.SubmittedByTwitchId).HasMaxLength(120);
|
||||
entity.Property(item => item.Status).HasMaxLength(30);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AwardResult>(entity =>
|
||||
{
|
||||
entity.Property(item => item.CategoryName).HasMaxLength(120);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UserSession>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.SessionToken).IsUnique();
|
||||
entity.Property(item => item.SessionToken).HasMaxLength(120);
|
||||
entity.Property(item => item.TwitchUserId).HasMaxLength(120);
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(120);
|
||||
entity.Property(item => item.Role).HasMaxLength(40);
|
||||
});
|
||||
|
||||
SeedData.Apply(modelBuilder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public sealed class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AwardsDbContext>
|
||||
{
|
||||
public AwardsDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<AwardsDbContext>();
|
||||
var connectionString = Environment.GetEnvironmentVariable("VTSA_POSTGRES")
|
||||
?? "Host=localhost;Port=5432;Database=vtuber_star_awards;Username=postgres;Password=postgres";
|
||||
|
||||
optionsBuilder.UseNpgsql(connectionString);
|
||||
return new AwardsDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Backend.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static class SeedData
|
||||
{
|
||||
public static void Apply(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Season>().HasData(
|
||||
new Season
|
||||
{
|
||||
Id = 1,
|
||||
Year = 2026,
|
||||
Name = "VTuber Star Awards 2026",
|
||||
IsCurrent = true,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Community Voting",
|
||||
NominationStartsAt = new DateOnly(2026, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2026, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2026, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2026, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2026, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2026, 7, 10),
|
||||
ShowDate = new DateOnly(2026, 7, 20),
|
||||
},
|
||||
new Season
|
||||
{
|
||||
Id = 2,
|
||||
Year = 2025,
|
||||
Name = "VTuber Star Awards 2025",
|
||||
IsCurrent = false,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Archived",
|
||||
NominationStartsAt = new DateOnly(2025, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2025, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2025, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2025, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2025, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2025, 7, 10),
|
||||
ShowDate = new DateOnly(2025, 7, 20),
|
||||
},
|
||||
new Season
|
||||
{
|
||||
Id = 3,
|
||||
Year = 2024,
|
||||
Name = "VTuber Star Awards 2024",
|
||||
IsCurrent = false,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Archived",
|
||||
NominationStartsAt = new DateOnly(2024, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2024, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2024, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2024, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2024, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2024, 7, 10),
|
||||
ShowDate = new DateOnly(2024, 7, 20),
|
||||
},
|
||||
new Season
|
||||
{
|
||||
Id = 4,
|
||||
Year = 2023,
|
||||
Name = "VTuber Star Awards 2023",
|
||||
IsCurrent = false,
|
||||
IsCommunityOnly = true,
|
||||
CurrentPhase = "Archived",
|
||||
NominationStartsAt = new DateOnly(2023, 5, 1),
|
||||
NominationEndsAt = new DateOnly(2023, 5, 31),
|
||||
VotingStartsAt = new DateOnly(2023, 6, 1),
|
||||
VotingEndsAt = new DateOnly(2023, 6, 30),
|
||||
ReviewStartsAt = new DateOnly(2023, 7, 1),
|
||||
ReviewEndsAt = new DateOnly(2023, 7, 10),
|
||||
ShowDate = new DateOnly(2023, 7, 20),
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Category>().HasData(
|
||||
new Category { Id = 1, SeasonId = 1, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Die groesste Auszeichnung des Jahres.", SortOrder = 1, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 2, SeasonId = 1, GroupName = "Performance", Name = "Bestes Live Event", Slug = "bestes-live-event", Description = "Events, Konzerte und 3D-Shows.", SortOrder = 2, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 3, SeasonId = 1, GroupName = "Clips & Highlights", Name = "Clip des Jahres", Slug = "clip-des-jahres", Description = "Der lustigste oder emotionalste Clip des Jahres.", SortOrder = 3, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 4, SeasonId = 1, GroupName = "Main Awards", Name = "Beste Community", Slug = "beste-community", Description = "Die aktivste und freundlichste Community.", SortOrder = 4, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 5, SeasonId = 2, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Archivkategorie 2025.", SortOrder = 1, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 6, SeasonId = 2, GroupName = "Performance", Name = "Bestes Live Event", Slug = "bestes-live-event", Description = "Archivkategorie 2025.", SortOrder = 2, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 7, SeasonId = 2, GroupName = "Clips & Highlights", Name = "Clip des Jahres", Slug = "clip-des-jahres", Description = "Archivkategorie 2025.", SortOrder = 3, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 8, SeasonId = 3, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Archivkategorie 2024.", SortOrder = 1, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 9, SeasonId = 3, GroupName = "Clips & Highlights", Name = "Clip des Jahres", Slug = "clip-des-jahres", Description = "Archivkategorie 2024.", SortOrder = 2, MaxNomineesPerUser = 3 },
|
||||
new Category { Id = 10, SeasonId = 4, GroupName = "Main Awards", Name = "VTuber des Jahres", Slug = "vtuber-des-jahres", Description = "Archivkategorie 2023.", SortOrder = 1, MaxNomineesPerUser = 3 });
|
||||
|
||||
modelBuilder.Entity<Candidate>().HasData(
|
||||
new Candidate { Id = 1, SeasonId = 1, CategoryId = 1, DisplayName = "Hoshimi Miyu", ChannelSlug = "@hoshimimiyu", Platform = "Twitch" },
|
||||
new Candidate { Id = 2, SeasonId = 1, CategoryId = 1, DisplayName = "Kurainu", ChannelSlug = "@kurainu", Platform = "Twitch" },
|
||||
new Candidate { Id = 3, SeasonId = 1, CategoryId = 1, DisplayName = "Shiro Ch.", ChannelSlug = "@shiroch", Platform = "Twitch" },
|
||||
new Candidate { Id = 4, SeasonId = 1, CategoryId = 2, DisplayName = "Kurainu 3D Live", ChannelSlug = "@kurainu", Platform = "Twitch" },
|
||||
new Candidate { Id = 5, SeasonId = 1, CategoryId = 2, DisplayName = "Aoi Sakura Showcase", ChannelSlug = "@aoisakura", Platform = "YouTube" },
|
||||
new Candidate { Id = 6, SeasonId = 1, CategoryId = 3, DisplayName = "Pyonkichi Kingdom", ChannelSlug = "@pyonkichikingdom", Platform = "Twitch" },
|
||||
new Candidate { Id = 7, SeasonId = 1, CategoryId = 4, DisplayName = "Moonrelay", ChannelSlug = "@moonrelay", Platform = "Twitch" },
|
||||
new Candidate { Id = 8, SeasonId = 2, CategoryId = 5, DisplayName = "Hoshimi Miyu", ChannelSlug = "@hoshimimiyu", Platform = "Twitch" },
|
||||
new Candidate { Id = 9, SeasonId = 2, CategoryId = 6, DisplayName = "Kurainu 3D Live", ChannelSlug = "@kurainu", Platform = "Twitch" },
|
||||
new Candidate { Id = 10, SeasonId = 2, CategoryId = 7, DisplayName = "Pyonkichi Kingdom", ChannelSlug = "@pyonkichikingdom", Platform = "Twitch" },
|
||||
new Candidate { Id = 11, SeasonId = 3, CategoryId = 8, DisplayName = "Aoi Sakura", ChannelSlug = "@aoisakura", Platform = "YouTube" },
|
||||
new Candidate { Id = 12, SeasonId = 3, CategoryId = 9, DisplayName = "Starbyte", ChannelSlug = "@starbyte", Platform = "Twitch" },
|
||||
new Candidate { Id = 13, SeasonId = 4, CategoryId = 10, DisplayName = "Tenshi Vox", ChannelSlug = "@tenshivox", Platform = "Twitch" });
|
||||
|
||||
modelBuilder.Entity<AwardResult>().HasData(
|
||||
new AwardResult { Id = 1, SeasonId = 2, CandidateId = 8, CategoryName = "VTuber des Jahres" },
|
||||
new AwardResult { Id = 2, SeasonId = 2, CandidateId = 9, CategoryName = "Bestes Live Event" },
|
||||
new AwardResult { Id = 3, SeasonId = 2, CandidateId = 10, CategoryName = "Clip des Jahres" },
|
||||
new AwardResult { Id = 4, SeasonId = 3, CandidateId = 11, CategoryName = "VTuber des Jahres" },
|
||||
new AwardResult { Id = 5, SeasonId = 3, CandidateId = 12, CategoryName = "Clip des Jahres" },
|
||||
new AwardResult { Id = 6, SeasonId = 4, CandidateId = 13, CategoryName = "VTuber des Jahres" });
|
||||
|
||||
modelBuilder.Entity<Nomination>().HasData(
|
||||
new Nomination { Id = 1, SeasonId = 1, CategoryId = 1, SubmittedByTwitchId = "twitch_hoshi", CandidateText = "Hoshimi Miyu", CreatedAt = new DateTimeOffset(2026, 6, 10, 13, 0, 0, TimeSpan.Zero) },
|
||||
new Nomination { Id = 2, SeasonId = 1, CategoryId = 2, SubmittedByTwitchId = "twitch_kurainu", CandidateText = "Kurainu 3D Live", CreatedAt = new DateTimeOffset(2026, 6, 10, 14, 0, 0, TimeSpan.Zero) });
|
||||
|
||||
modelBuilder.Entity<VoteBallot>().HasData(
|
||||
new VoteBallot { Id = 1, SeasonId = 1, SubmittedByTwitchId = "twitch_vote_1", Status = "submitted", SubmittedAt = new DateTimeOffset(2026, 6, 11, 12, 0, 0, TimeSpan.Zero) },
|
||||
new VoteBallot { Id = 2, SeasonId = 1, SubmittedByTwitchId = "twitch_vote_2", Status = "submitted", SubmittedAt = new DateTimeOffset(2026, 6, 11, 12, 5, 0, TimeSpan.Zero) });
|
||||
|
||||
modelBuilder.Entity<VoteEntry>().HasData(
|
||||
new VoteEntry { Id = 1, BallotId = 1, CategoryId = 1, CandidateId = 1 },
|
||||
new VoteEntry { Id = 2, BallotId = 1, CategoryId = 2, CandidateId = 4 },
|
||||
new VoteEntry { Id = 3, BallotId = 2, CategoryId = 1, CandidateId = 2 },
|
||||
new VoteEntry { Id = 4, BallotId = 2, CategoryId = 3, CandidateId = 6 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend.Data;
|
||||
|
||||
public static class SessionBootstrapper
|
||||
{
|
||||
public static Task EnsureAsync(AwardsDbContext db) =>
|
||||
db.Database.ExecuteSqlRawAsync(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "UserSessions" (
|
||||
"Id" uuid NOT NULL PRIMARY KEY,
|
||||
"SessionToken" character varying(120) NOT NULL,
|
||||
"TwitchUserId" character varying(120) NOT NULL,
|
||||
"DisplayName" character varying(120) NOT NULL,
|
||||
"Role" character varying(40) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"LastSeenAt" timestamp with time zone NOT NULL,
|
||||
"IsActive" boolean NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_UserSessions_SessionToken"
|
||||
ON "UserSessions" ("SessionToken");
|
||||
""");
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class AwardResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeasonId { get; set; }
|
||||
public Season Season { get; set; } = null!;
|
||||
public int CandidateId { get; set; }
|
||||
public Candidate Candidate { get; set; } = null!;
|
||||
public string CategoryName { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class Candidate
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeasonId { get; set; }
|
||||
public Season Season { get; set; } = null!;
|
||||
public int CategoryId { get; set; }
|
||||
public Category Category { get; set; } = null!;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string ChannelSlug { get; set; } = string.Empty;
|
||||
public string Platform { get; set; } = "Twitch";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class Category
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeasonId { get; set; }
|
||||
public Season Season { get; set; } = null!;
|
||||
public string GroupName { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public int MaxNomineesPerUser { get; set; }
|
||||
public ICollection<Candidate> Candidates { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class Nomination
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeasonId { get; set; }
|
||||
public Season Season { get; set; } = null!;
|
||||
public int CategoryId { get; set; }
|
||||
public Category Category { get; set; } = null!;
|
||||
public string SubmittedByTwitchId { get; set; } = string.Empty;
|
||||
public int? CandidateId { get; set; }
|
||||
public Candidate? Candidate { get; set; }
|
||||
public string? CandidateText { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class Season
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsCurrent { get; set; }
|
||||
public bool IsCommunityOnly { get; set; }
|
||||
public string CurrentPhase { get; set; } = string.Empty;
|
||||
public DateOnly NominationStartsAt { get; set; }
|
||||
public DateOnly NominationEndsAt { get; set; }
|
||||
public DateOnly VotingStartsAt { get; set; }
|
||||
public DateOnly VotingEndsAt { get; set; }
|
||||
public DateOnly ReviewStartsAt { get; set; }
|
||||
public DateOnly ReviewEndsAt { get; set; }
|
||||
public DateOnly ShowDate { get; set; }
|
||||
public ICollection<Category> Categories { get; set; } = [];
|
||||
public ICollection<AwardResult> Results { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class UserSession
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string SessionToken { get; set; } = string.Empty;
|
||||
public string TwitchUserId { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string Role { get; set; } = "viewer";
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset LastSeenAt { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class VoteBallot
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeasonId { get; set; }
|
||||
public Season Season { get; set; } = null!;
|
||||
public string SubmittedByTwitchId { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = "draft";
|
||||
public DateTimeOffset SubmittedAt { get; set; }
|
||||
public ICollection<VoteEntry> Entries { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Backend.Domain;
|
||||
|
||||
public sealed class VoteEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int BallotId { get; set; }
|
||||
public VoteBallot Ballot { get; set; } = null!;
|
||||
public int CategoryId { get; set; }
|
||||
public Category Category { get; set; } = null!;
|
||||
public int CandidateId { get; set; }
|
||||
public Candidate Candidate { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,833 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Backend.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Backend.Migrations
|
||||
{
|
||||
[DbContext(typeof(AwardsDbContext))]
|
||||
[Migration("20260617060000_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.AwardResult", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CandidateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CategoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CandidateId");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("Results");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CandidateId = 8,
|
||||
CategoryName = "VTuber des Jahres",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CandidateId = 9,
|
||||
CategoryName = "Bestes Live Event",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CandidateId = 10,
|
||||
CategoryName = "Clip des Jahres",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CandidateId = 11,
|
||||
CategoryName = "VTuber des Jahres",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
CandidateId = 12,
|
||||
CategoryName = "Clip des Jahres",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
CandidateId = 13,
|
||||
CategoryName = "VTuber des Jahres",
|
||||
SeasonId = 4
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Candidate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ChannelSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("Candidates");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CategoryId = 1,
|
||||
ChannelSlug = "@hoshimimiyu",
|
||||
DisplayName = "Hoshimi Miyu",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CategoryId = 1,
|
||||
ChannelSlug = "@kurainu",
|
||||
DisplayName = "Kurainu",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CategoryId = 1,
|
||||
ChannelSlug = "@shiroch",
|
||||
DisplayName = "Shiro Ch.",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CategoryId = 2,
|
||||
ChannelSlug = "@kurainu",
|
||||
DisplayName = "Kurainu 3D Live",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
CategoryId = 2,
|
||||
ChannelSlug = "@aoisakura",
|
||||
DisplayName = "Aoi Sakura Showcase",
|
||||
Platform = "YouTube",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
CategoryId = 3,
|
||||
ChannelSlug = "@pyonkichikingdom",
|
||||
DisplayName = "Pyonkichi Kingdom",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
CategoryId = 4,
|
||||
ChannelSlug = "@moonrelay",
|
||||
DisplayName = "Moonrelay",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 8,
|
||||
CategoryId = 5,
|
||||
ChannelSlug = "@hoshimimiyu",
|
||||
DisplayName = "Hoshimi Miyu",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 9,
|
||||
CategoryId = 6,
|
||||
ChannelSlug = "@kurainu",
|
||||
DisplayName = "Kurainu 3D Live",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 10,
|
||||
CategoryId = 7,
|
||||
ChannelSlug = "@pyonkichikingdom",
|
||||
DisplayName = "Pyonkichi Kingdom",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 11,
|
||||
CategoryId = 8,
|
||||
ChannelSlug = "@aoisakura",
|
||||
DisplayName = "Aoi Sakura",
|
||||
Platform = "YouTube",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 12,
|
||||
CategoryId = 9,
|
||||
ChannelSlug = "@starbyte",
|
||||
DisplayName = "Starbyte",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 13,
|
||||
CategoryId = 10,
|
||||
ChannelSlug = "@tenshivox",
|
||||
DisplayName = "Tenshi Vox",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 4
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Category", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("character varying(400)");
|
||||
|
||||
b.Property<string>("GroupName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<int>("MaxNomineesPerUser")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeasonId", "Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Description = "Die groesste Auszeichnung des Jahres.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 1,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Description = "Events, Konzerte und 3D-Shows.",
|
||||
GroupName = "Performance",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Bestes Live Event",
|
||||
SeasonId = 1,
|
||||
Slug = "bestes-live-event",
|
||||
SortOrder = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Description = "Der lustigste oder emotionalste Clip des Jahres.",
|
||||
GroupName = "Clips & Highlights",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Clip des Jahres",
|
||||
SeasonId = 1,
|
||||
Slug = "clip-des-jahres",
|
||||
SortOrder = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Description = "Die aktivste und freundlichste Community.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Beste Community",
|
||||
SeasonId = 1,
|
||||
Slug = "beste-community",
|
||||
SortOrder = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Description = "Archivkategorie 2025.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 2,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
Description = "Archivkategorie 2025.",
|
||||
GroupName = "Performance",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Bestes Live Event",
|
||||
SeasonId = 2,
|
||||
Slug = "bestes-live-event",
|
||||
SortOrder = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
Description = "Archivkategorie 2025.",
|
||||
GroupName = "Clips & Highlights",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Clip des Jahres",
|
||||
SeasonId = 2,
|
||||
Slug = "clip-des-jahres",
|
||||
SortOrder = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 8,
|
||||
Description = "Archivkategorie 2024.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 3,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 9,
|
||||
Description = "Archivkategorie 2024.",
|
||||
GroupName = "Clips & Highlights",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Clip des Jahres",
|
||||
SeasonId = 3,
|
||||
Slug = "clip-des-jahres",
|
||||
SortOrder = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 10,
|
||||
Description = "Archivkategorie 2023.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 4,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Nomination", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("CandidateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CandidateText")
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SubmittedByTwitchId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CandidateId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("Nominations");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CandidateText = "Hoshimi Miyu",
|
||||
CategoryId = 1,
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SeasonId = 1,
|
||||
SubmittedByTwitchId = "twitch_hoshi"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CandidateText = "Kurainu 3D Live",
|
||||
CategoryId = 2,
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SeasonId = 1,
|
||||
SubmittedByTwitchId = "twitch_kurainu"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Season", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CurrentPhase")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<bool>("IsCommunityOnly")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsCurrent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)");
|
||||
|
||||
b.Property<DateOnly>("NominationEndsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("NominationStartsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("ReviewEndsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("ReviewStartsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("ShowDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("VotingEndsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("VotingStartsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<int>("Year")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Year")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Seasons");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CurrentPhase = "Community Voting",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = true,
|
||||
Name = "VTuber Star Awards 2026",
|
||||
NominationEndsAt = new DateOnly(2026, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2026, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2026, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2026, 7, 1),
|
||||
ShowDate = new DateOnly(2026, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2026, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2026, 6, 1),
|
||||
Year = 2026
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CurrentPhase = "Archived",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = false,
|
||||
Name = "VTuber Star Awards 2025",
|
||||
NominationEndsAt = new DateOnly(2025, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2025, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2025, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2025, 7, 1),
|
||||
ShowDate = new DateOnly(2025, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2025, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2025, 6, 1),
|
||||
Year = 2025
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CurrentPhase = "Archived",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = false,
|
||||
Name = "VTuber Star Awards 2024",
|
||||
NominationEndsAt = new DateOnly(2024, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2024, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2024, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2024, 7, 1),
|
||||
ShowDate = new DateOnly(2024, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2024, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2024, 6, 1),
|
||||
Year = 2024
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CurrentPhase = "Archived",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = false,
|
||||
Name = "VTuber Star Awards 2023",
|
||||
NominationEndsAt = new DateOnly(2023, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2023, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2023, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2023, 7, 1),
|
||||
ShowDate = new DateOnly(2023, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2023, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2023, 6, 1),
|
||||
Year = 2023
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<DateTimeOffset>("SubmittedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SubmittedByTwitchId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("VoteBallots");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
SeasonId = 1,
|
||||
Status = "submitted",
|
||||
SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SubmittedByTwitchId = "twitch_vote_1"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
SeasonId = 1,
|
||||
Status = "submitted",
|
||||
SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SubmittedByTwitchId = "twitch_vote_2"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BallotId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CandidateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BallotId");
|
||||
|
||||
b.HasIndex("CandidateId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("VoteEntries");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
BallotId = 1,
|
||||
CandidateId = 1,
|
||||
CategoryId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
BallotId = 1,
|
||||
CandidateId = 4,
|
||||
CategoryId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
BallotId = 2,
|
||||
CandidateId = 2,
|
||||
CategoryId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
BallotId = 2,
|
||||
CandidateId = 6,
|
||||
CategoryId = 3
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.AwardResult", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Candidate", "Candidate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CandidateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany("Results")
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Candidate");
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Candidate", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Category", "Category")
|
||||
.WithMany("Candidates")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Category", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Nomination", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Candidate", "Candidate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CandidateId");
|
||||
|
||||
b.HasOne("Backend.Domain.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Candidate");
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteEntry", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.VoteBallot", "Ballot")
|
||||
.WithMany("Entries")
|
||||
.HasForeignKey("BallotId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Candidate", "Candidate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CandidateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Ballot");
|
||||
|
||||
b.Navigation("Candidate");
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Category", b =>
|
||||
{
|
||||
b.Navigation("Candidates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Season", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
|
||||
b.Navigation("Results");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
|
||||
{
|
||||
b.Navigation("Entries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Backend.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Seasons",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Year = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||
IsCurrent = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsCommunityOnly = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CurrentPhase = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
|
||||
NominationStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
NominationEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
VotingStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
VotingEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
ReviewStartsAt = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
ReviewEndsAt = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
ShowDate = table.Column<DateOnly>(type: "date", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Seasons", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SeasonId = table.Column<int>(type: "integer", nullable: false),
|
||||
GroupName = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
Slug = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
MaxNomineesPerUser = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Categories_Seasons_SeasonId",
|
||||
column: x => x.SeasonId,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VoteBallots",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SeasonId = table.Column<int>(type: "integer", nullable: false),
|
||||
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
SubmittedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_VoteBallots", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_VoteBallots_Seasons_SeasonId",
|
||||
column: x => x.SeasonId,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Candidates",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SeasonId = table.Column<int>(type: "integer", nullable: false),
|
||||
CategoryId = table.Column<int>(type: "integer", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
ChannelSlug = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
Platform = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Candidates", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Candidates_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Candidates_Seasons_SeasonId",
|
||||
column: x => x.SeasonId,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Nominations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SeasonId = table.Column<int>(type: "integer", nullable: false),
|
||||
CategoryId = table.Column<int>(type: "integer", nullable: false),
|
||||
SubmittedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
CandidateId = table.Column<int>(type: "integer", nullable: true),
|
||||
CandidateText = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Nominations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Nominations_Candidates_CandidateId",
|
||||
column: x => x.CandidateId,
|
||||
principalTable: "Candidates",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_Nominations_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Nominations_Seasons_SeasonId",
|
||||
column: x => x.SeasonId,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Results",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SeasonId = table.Column<int>(type: "integer", nullable: false),
|
||||
CandidateId = table.Column<int>(type: "integer", nullable: false),
|
||||
CategoryName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Results", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Results_Candidates_CandidateId",
|
||||
column: x => x.CandidateId,
|
||||
principalTable: "Candidates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Results_Seasons_SeasonId",
|
||||
column: x => x.SeasonId,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VoteEntries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
BallotId = table.Column<int>(type: "integer", nullable: false),
|
||||
CategoryId = table.Column<int>(type: "integer", nullable: false),
|
||||
CandidateId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_VoteEntries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_VoteEntries_Candidates_CandidateId",
|
||||
column: x => x.CandidateId,
|
||||
principalTable: "Candidates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_VoteEntries_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_VoteEntries_VoteBallots_BallotId",
|
||||
column: x => x.BallotId,
|
||||
principalTable: "VoteBallots",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Seasons",
|
||||
columns: new[] { "Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "Community Voting", true, true, "VTuber Star Awards 2026", new DateOnly(2026, 5, 31), new DateOnly(2026, 5, 1), new DateOnly(2026, 7, 10), new DateOnly(2026, 7, 1), new DateOnly(2026, 7, 20), new DateOnly(2026, 6, 30), new DateOnly(2026, 6, 1), 2026 },
|
||||
{ 2, "Archived", true, false, "VTuber Star Awards 2025", new DateOnly(2025, 5, 31), new DateOnly(2025, 5, 1), new DateOnly(2025, 7, 10), new DateOnly(2025, 7, 1), new DateOnly(2025, 7, 20), new DateOnly(2025, 6, 30), new DateOnly(2025, 6, 1), 2025 },
|
||||
{ 3, "Archived", true, false, "VTuber Star Awards 2024", new DateOnly(2024, 5, 31), new DateOnly(2024, 5, 1), new DateOnly(2024, 7, 10), new DateOnly(2024, 7, 1), new DateOnly(2024, 7, 20), new DateOnly(2024, 6, 30), new DateOnly(2024, 6, 1), 2024 },
|
||||
{ 4, "Archived", true, false, "VTuber Star Awards 2023", new DateOnly(2023, 5, 31), new DateOnly(2023, 5, 1), new DateOnly(2023, 7, 10), new DateOnly(2023, 7, 1), new DateOnly(2023, 7, 20), new DateOnly(2023, 6, 30), new DateOnly(2023, 6, 1), 2023 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Categories",
|
||||
columns: new[] { "Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "Die groesste Auszeichnung des Jahres.", "Main Awards", 3, "VTuber des Jahres", 1, "vtuber-des-jahres", 1 },
|
||||
{ 2, "Events, Konzerte und 3D-Shows.", "Performance", 3, "Bestes Live Event", 1, "bestes-live-event", 2 },
|
||||
{ 3, "Der lustigste oder emotionalste Clip des Jahres.", "Clips & Highlights", 3, "Clip des Jahres", 1, "clip-des-jahres", 3 },
|
||||
{ 4, "Die aktivste und freundlichste Community.", "Main Awards", 3, "Beste Community", 1, "beste-community", 4 },
|
||||
{ 5, "Archivkategorie 2025.", "Main Awards", 3, "VTuber des Jahres", 2, "vtuber-des-jahres", 1 },
|
||||
{ 6, "Archivkategorie 2025.", "Performance", 3, "Bestes Live Event", 2, "bestes-live-event", 2 },
|
||||
{ 7, "Archivkategorie 2025.", "Clips & Highlights", 3, "Clip des Jahres", 2, "clip-des-jahres", 3 },
|
||||
{ 8, "Archivkategorie 2024.", "Main Awards", 3, "VTuber des Jahres", 3, "vtuber-des-jahres", 1 },
|
||||
{ 9, "Archivkategorie 2024.", "Clips & Highlights", 3, "Clip des Jahres", 3, "clip-des-jahres", 2 },
|
||||
{ 10, "Archivkategorie 2023.", "Main Awards", 3, "VTuber des Jahres", 4, "vtuber-des-jahres", 1 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "VoteBallots",
|
||||
columns: new[] { "Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, 1, "submitted", new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "twitch_vote_1" },
|
||||
{ 2, 1, "submitted", new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), "twitch_vote_2" }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Candidates",
|
||||
columns: new[] { "Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, 1, "@hoshimimiyu", "Hoshimi Miyu", "Twitch", 1 },
|
||||
{ 2, 1, "@kurainu", "Kurainu", "Twitch", 1 },
|
||||
{ 3, 1, "@shiroch", "Shiro Ch.", "Twitch", 1 },
|
||||
{ 4, 2, "@kurainu", "Kurainu 3D Live", "Twitch", 1 },
|
||||
{ 5, 2, "@aoisakura", "Aoi Sakura Showcase", "YouTube", 1 },
|
||||
{ 6, 3, "@pyonkichikingdom", "Pyonkichi Kingdom", "Twitch", 1 },
|
||||
{ 7, 4, "@moonrelay", "Moonrelay", "Twitch", 1 },
|
||||
{ 8, 5, "@hoshimimiyu", "Hoshimi Miyu", "Twitch", 2 },
|
||||
{ 9, 6, "@kurainu", "Kurainu 3D Live", "Twitch", 2 },
|
||||
{ 10, 7, "@pyonkichikingdom", "Pyonkichi Kingdom", "Twitch", 2 },
|
||||
{ 11, 8, "@aoisakura", "Aoi Sakura", "YouTube", 3 },
|
||||
{ 12, 9, "@starbyte", "Starbyte", "Twitch", 3 },
|
||||
{ 13, 10, "@tenshivox", "Tenshi Vox", "Twitch", 4 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Nominations",
|
||||
columns: new[] { "Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, null, "Hoshimi Miyu", 1, new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), 1, "twitch_hoshi" },
|
||||
{ 2, null, "Kurainu 3D Live", 2, new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), 1, "twitch_kurainu" }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Results",
|
||||
columns: new[] { "Id", "CandidateId", "CategoryName", "SeasonId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, 8, "VTuber des Jahres", 2 },
|
||||
{ 2, 9, "Bestes Live Event", 2 },
|
||||
{ 3, 10, "Clip des Jahres", 2 },
|
||||
{ 4, 11, "VTuber des Jahres", 3 },
|
||||
{ 5, 12, "Clip des Jahres", 3 },
|
||||
{ 6, 13, "VTuber des Jahres", 4 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "VoteEntries",
|
||||
columns: new[] { "Id", "BallotId", "CandidateId", "CategoryId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, 1, 1, 1 },
|
||||
{ 2, 1, 4, 2 },
|
||||
{ 3, 2, 2, 1 },
|
||||
{ 4, 2, 6, 3 }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Candidates_CategoryId",
|
||||
table: "Candidates",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Candidates_SeasonId",
|
||||
table: "Candidates",
|
||||
column: "SeasonId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_SeasonId_Slug",
|
||||
table: "Categories",
|
||||
columns: new[] { "SeasonId", "Slug" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nominations_CandidateId",
|
||||
table: "Nominations",
|
||||
column: "CandidateId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nominations_CategoryId",
|
||||
table: "Nominations",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nominations_SeasonId",
|
||||
table: "Nominations",
|
||||
column: "SeasonId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Results_CandidateId",
|
||||
table: "Results",
|
||||
column: "CandidateId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Results_SeasonId",
|
||||
table: "Results",
|
||||
column: "SeasonId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Seasons_Year",
|
||||
table: "Seasons",
|
||||
column: "Year",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VoteBallots_SeasonId",
|
||||
table: "VoteBallots",
|
||||
column: "SeasonId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VoteEntries_BallotId",
|
||||
table: "VoteEntries",
|
||||
column: "BallotId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VoteEntries_CandidateId",
|
||||
table: "VoteEntries",
|
||||
column: "CandidateId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VoteEntries_CategoryId",
|
||||
table: "VoteEntries",
|
||||
column: "CategoryId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Nominations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Results");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "VoteEntries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Candidates");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "VoteBallots");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Seasons");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Backend.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Backend.Migrations
|
||||
{
|
||||
[DbContext(typeof(AwardsDbContext))]
|
||||
partial class AwardsDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.AwardResult", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CandidateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CategoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CandidateId");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("Results");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CandidateId = 8,
|
||||
CategoryName = "VTuber des Jahres",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CandidateId = 9,
|
||||
CategoryName = "Bestes Live Event",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CandidateId = 10,
|
||||
CategoryName = "Clip des Jahres",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CandidateId = 11,
|
||||
CategoryName = "VTuber des Jahres",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
CandidateId = 12,
|
||||
CategoryName = "Clip des Jahres",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
CandidateId = 13,
|
||||
CategoryName = "VTuber des Jahres",
|
||||
SeasonId = 4
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Candidate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ChannelSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("Candidates");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CategoryId = 1,
|
||||
ChannelSlug = "@hoshimimiyu",
|
||||
DisplayName = "Hoshimi Miyu",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CategoryId = 1,
|
||||
ChannelSlug = "@kurainu",
|
||||
DisplayName = "Kurainu",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CategoryId = 1,
|
||||
ChannelSlug = "@shiroch",
|
||||
DisplayName = "Shiro Ch.",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CategoryId = 2,
|
||||
ChannelSlug = "@kurainu",
|
||||
DisplayName = "Kurainu 3D Live",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
CategoryId = 2,
|
||||
ChannelSlug = "@aoisakura",
|
||||
DisplayName = "Aoi Sakura Showcase",
|
||||
Platform = "YouTube",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
CategoryId = 3,
|
||||
ChannelSlug = "@pyonkichikingdom",
|
||||
DisplayName = "Pyonkichi Kingdom",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
CategoryId = 4,
|
||||
ChannelSlug = "@moonrelay",
|
||||
DisplayName = "Moonrelay",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 8,
|
||||
CategoryId = 5,
|
||||
ChannelSlug = "@hoshimimiyu",
|
||||
DisplayName = "Hoshimi Miyu",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 9,
|
||||
CategoryId = 6,
|
||||
ChannelSlug = "@kurainu",
|
||||
DisplayName = "Kurainu 3D Live",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 10,
|
||||
CategoryId = 7,
|
||||
ChannelSlug = "@pyonkichikingdom",
|
||||
DisplayName = "Pyonkichi Kingdom",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 11,
|
||||
CategoryId = 8,
|
||||
ChannelSlug = "@aoisakura",
|
||||
DisplayName = "Aoi Sakura",
|
||||
Platform = "YouTube",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 12,
|
||||
CategoryId = 9,
|
||||
ChannelSlug = "@starbyte",
|
||||
DisplayName = "Starbyte",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 13,
|
||||
CategoryId = 10,
|
||||
ChannelSlug = "@tenshivox",
|
||||
DisplayName = "Tenshi Vox",
|
||||
Platform = "Twitch",
|
||||
SeasonId = 4
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Category", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("character varying(400)");
|
||||
|
||||
b.Property<string>("GroupName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<int>("MaxNomineesPerUser")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeasonId", "Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Description = "Die groesste Auszeichnung des Jahres.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 1,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Description = "Events, Konzerte und 3D-Shows.",
|
||||
GroupName = "Performance",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Bestes Live Event",
|
||||
SeasonId = 1,
|
||||
Slug = "bestes-live-event",
|
||||
SortOrder = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Description = "Der lustigste oder emotionalste Clip des Jahres.",
|
||||
GroupName = "Clips & Highlights",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Clip des Jahres",
|
||||
SeasonId = 1,
|
||||
Slug = "clip-des-jahres",
|
||||
SortOrder = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Description = "Die aktivste und freundlichste Community.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Beste Community",
|
||||
SeasonId = 1,
|
||||
Slug = "beste-community",
|
||||
SortOrder = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Description = "Archivkategorie 2025.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 2,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
Description = "Archivkategorie 2025.",
|
||||
GroupName = "Performance",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Bestes Live Event",
|
||||
SeasonId = 2,
|
||||
Slug = "bestes-live-event",
|
||||
SortOrder = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
Description = "Archivkategorie 2025.",
|
||||
GroupName = "Clips & Highlights",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Clip des Jahres",
|
||||
SeasonId = 2,
|
||||
Slug = "clip-des-jahres",
|
||||
SortOrder = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 8,
|
||||
Description = "Archivkategorie 2024.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 3,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 9,
|
||||
Description = "Archivkategorie 2024.",
|
||||
GroupName = "Clips & Highlights",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "Clip des Jahres",
|
||||
SeasonId = 3,
|
||||
Slug = "clip-des-jahres",
|
||||
SortOrder = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 10,
|
||||
Description = "Archivkategorie 2023.",
|
||||
GroupName = "Main Awards",
|
||||
MaxNomineesPerUser = 3,
|
||||
Name = "VTuber des Jahres",
|
||||
SeasonId = 4,
|
||||
Slug = "vtuber-des-jahres",
|
||||
SortOrder = 1
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Nomination", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("CandidateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CandidateText")
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SubmittedByTwitchId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CandidateId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("Nominations");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CandidateText = "Hoshimi Miyu",
|
||||
CategoryId = 1,
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 13, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SeasonId = 1,
|
||||
SubmittedByTwitchId = "twitch_hoshi"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CandidateText = "Kurainu 3D Live",
|
||||
CategoryId = 2,
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 6, 10, 14, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SeasonId = 1,
|
||||
SubmittedByTwitchId = "twitch_kurainu"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Season", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CurrentPhase")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<bool>("IsCommunityOnly")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsCurrent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(160)
|
||||
.HasColumnType("character varying(160)");
|
||||
|
||||
b.Property<DateOnly>("NominationEndsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("NominationStartsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("ReviewEndsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("ReviewStartsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("ShowDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("VotingEndsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateOnly>("VotingStartsAt")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<int>("Year")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Year")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Seasons");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CurrentPhase = "Community Voting",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = true,
|
||||
Name = "VTuber Star Awards 2026",
|
||||
NominationEndsAt = new DateOnly(2026, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2026, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2026, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2026, 7, 1),
|
||||
ShowDate = new DateOnly(2026, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2026, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2026, 6, 1),
|
||||
Year = 2026
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CurrentPhase = "Archived",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = false,
|
||||
Name = "VTuber Star Awards 2025",
|
||||
NominationEndsAt = new DateOnly(2025, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2025, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2025, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2025, 7, 1),
|
||||
ShowDate = new DateOnly(2025, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2025, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2025, 6, 1),
|
||||
Year = 2025
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CurrentPhase = "Archived",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = false,
|
||||
Name = "VTuber Star Awards 2024",
|
||||
NominationEndsAt = new DateOnly(2024, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2024, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2024, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2024, 7, 1),
|
||||
ShowDate = new DateOnly(2024, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2024, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2024, 6, 1),
|
||||
Year = 2024
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CurrentPhase = "Archived",
|
||||
IsCommunityOnly = true,
|
||||
IsCurrent = false,
|
||||
Name = "VTuber Star Awards 2023",
|
||||
NominationEndsAt = new DateOnly(2023, 5, 31),
|
||||
NominationStartsAt = new DateOnly(2023, 5, 1),
|
||||
ReviewEndsAt = new DateOnly(2023, 7, 10),
|
||||
ReviewStartsAt = new DateOnly(2023, 7, 1),
|
||||
ShowDate = new DateOnly(2023, 7, 20),
|
||||
VotingEndsAt = new DateOnly(2023, 6, 30),
|
||||
VotingStartsAt = new DateOnly(2023, 6, 1),
|
||||
Year = 2023
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("SeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<DateTimeOffset>("SubmittedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SubmittedByTwitchId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeasonId");
|
||||
|
||||
b.ToTable("VoteBallots");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
SeasonId = 1,
|
||||
Status = "submitted",
|
||||
SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SubmittedByTwitchId = "twitch_vote_1"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
SeasonId = 1,
|
||||
Status = "submitted",
|
||||
SubmittedAt = new DateTimeOffset(new DateTime(2026, 6, 11, 12, 5, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
SubmittedByTwitchId = "twitch_vote_2"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BallotId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CandidateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BallotId");
|
||||
|
||||
b.HasIndex("CandidateId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("VoteEntries");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
BallotId = 1,
|
||||
CandidateId = 1,
|
||||
CategoryId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
BallotId = 1,
|
||||
CandidateId = 4,
|
||||
CategoryId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
BallotId = 2,
|
||||
CandidateId = 2,
|
||||
CategoryId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
BallotId = 2,
|
||||
CandidateId = 6,
|
||||
CategoryId = 3
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.AwardResult", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Candidate", "Candidate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CandidateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany("Results")
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Candidate");
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Candidate", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Category", "Category")
|
||||
.WithMany("Candidates")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Category", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Nomination", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Candidate", "Candidate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CandidateId");
|
||||
|
||||
b.HasOne("Backend.Domain.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Candidate");
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.Season", "Season")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteEntry", b =>
|
||||
{
|
||||
b.HasOne("Backend.Domain.VoteBallot", "Ballot")
|
||||
.WithMany("Entries")
|
||||
.HasForeignKey("BallotId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Candidate", "Candidate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CandidateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Backend.Domain.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Ballot");
|
||||
|
||||
b.Navigation("Candidate");
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Category", b =>
|
||||
{
|
||||
b.Navigation("Candidates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.Season", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
|
||||
b.Navigation("Results");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Backend.Domain.VoteBallot", b =>
|
||||
{
|
||||
b.Navigation("Entries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
CREATE TABLE "__EFMigrationsHistory" (
|
||||
"MigrationId" character varying(150) NOT NULL,
|
||||
"ProductVersion" character varying(32) NOT NULL,
|
||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||
);
|
||||
|
||||
CREATE TABLE "Seasons" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"Year" integer NOT NULL,
|
||||
"Name" character varying(160) NOT NULL,
|
||||
"IsCurrent" boolean NOT NULL,
|
||||
"IsCommunityOnly" boolean NOT NULL,
|
||||
"CurrentPhase" character varying(60) NOT NULL,
|
||||
"NominationStartsAt" date NOT NULL,
|
||||
"NominationEndsAt" date NOT NULL,
|
||||
"VotingStartsAt" date NOT NULL,
|
||||
"VotingEndsAt" date NOT NULL,
|
||||
"ReviewStartsAt" date NOT NULL,
|
||||
"ReviewEndsAt" date NOT NULL,
|
||||
"ShowDate" date NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Seasons_Year" ON "Seasons" ("Year");
|
||||
|
||||
CREATE TABLE "Categories" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE,
|
||||
"GroupName" character varying(80) NOT NULL,
|
||||
"Name" character varying(120) NOT NULL,
|
||||
"Slug" text NOT NULL,
|
||||
"Description" character varying(400) NOT NULL,
|
||||
"SortOrder" integer NOT NULL,
|
||||
"MaxNomineesPerUser" integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Categories_SeasonId_Slug" ON "Categories" ("SeasonId", "Slug");
|
||||
|
||||
CREATE TABLE "VoteBallots" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE,
|
||||
"SubmittedByTwitchId" character varying(120) NOT NULL,
|
||||
"Status" character varying(30) NOT NULL,
|
||||
"SubmittedAt" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_VoteBallots_SeasonId" ON "VoteBallots" ("SeasonId");
|
||||
|
||||
CREATE TABLE "Candidates" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE,
|
||||
"CategoryId" integer NOT NULL REFERENCES "Categories" ("Id") ON DELETE CASCADE,
|
||||
"DisplayName" character varying(120) NOT NULL,
|
||||
"ChannelSlug" character varying(120) NOT NULL,
|
||||
"Platform" character varying(40) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_Candidates_CategoryId" ON "Candidates" ("CategoryId");
|
||||
CREATE INDEX "IX_Candidates_SeasonId" ON "Candidates" ("SeasonId");
|
||||
|
||||
CREATE TABLE "Nominations" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE,
|
||||
"CategoryId" integer NOT NULL REFERENCES "Categories" ("Id") ON DELETE CASCADE,
|
||||
"SubmittedByTwitchId" character varying(120) NOT NULL,
|
||||
"CandidateId" integer NULL REFERENCES "Candidates" ("Id"),
|
||||
"CandidateText" character varying(120) NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_Nominations_CandidateId" ON "Nominations" ("CandidateId");
|
||||
CREATE INDEX "IX_Nominations_CategoryId" ON "Nominations" ("CategoryId");
|
||||
CREATE INDEX "IX_Nominations_SeasonId" ON "Nominations" ("SeasonId");
|
||||
|
||||
CREATE TABLE "Results" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"SeasonId" integer NOT NULL REFERENCES "Seasons" ("Id") ON DELETE CASCADE,
|
||||
"CandidateId" integer NOT NULL REFERENCES "Candidates" ("Id") ON DELETE CASCADE,
|
||||
"CategoryName" character varying(120) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_Results_CandidateId" ON "Results" ("CandidateId");
|
||||
CREATE INDEX "IX_Results_SeasonId" ON "Results" ("SeasonId");
|
||||
|
||||
CREATE TABLE "VoteEntries" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"BallotId" integer NOT NULL REFERENCES "VoteBallots" ("Id") ON DELETE CASCADE,
|
||||
"CategoryId" integer NOT NULL REFERENCES "Categories" ("Id") ON DELETE CASCADE,
|
||||
"CandidateId" integer NOT NULL REFERENCES "Candidates" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_VoteEntries_BallotId" ON "VoteEntries" ("BallotId");
|
||||
CREATE INDEX "IX_VoteEntries_CandidateId" ON "VoteEntries" ("CandidateId");
|
||||
CREATE INDEX "IX_VoteEntries_CategoryId" ON "VoteEntries" ("CategoryId");
|
||||
|
||||
INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year") VALUES
|
||||
(1, 'Community Voting', true, true, 'VTuber Star Awards 2026', '2026-05-31', '2026-05-01', '2026-07-10', '2026-07-01', '2026-07-20', '2026-06-30', '2026-06-01', 2026),
|
||||
(2, 'Archived', true, false, 'VTuber Star Awards 2025', '2025-05-31', '2025-05-01', '2025-07-10', '2025-07-01', '2025-07-20', '2025-06-30', '2025-06-01', 2025),
|
||||
(3, 'Archived', true, false, 'VTuber Star Awards 2024', '2024-05-31', '2024-05-01', '2024-07-10', '2024-07-01', '2024-07-20', '2024-06-30', '2024-06-01', 2024),
|
||||
(4, 'Archived', true, false, 'VTuber Star Awards 2023', '2023-05-31', '2023-05-01', '2023-07-10', '2023-07-01', '2023-07-20', '2023-06-30', '2023-06-01', 2023);
|
||||
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder") VALUES
|
||||
(1, 'Die groesste Auszeichnung des Jahres.', 'Main Awards', 3, 'VTuber des Jahres', 1, 'vtuber-des-jahres', 1),
|
||||
(2, 'Events, Konzerte und 3D-Shows.', 'Performance', 3, 'Bestes Live Event', 1, 'bestes-live-event', 2),
|
||||
(3, 'Der lustigste oder emotionalste Clip des Jahres.', 'Clips & Highlights', 3, 'Clip des Jahres', 1, 'clip-des-jahres', 3),
|
||||
(4, 'Die aktivste und freundlichste Community.', 'Main Awards', 3, 'Beste Community', 1, 'beste-community', 4),
|
||||
(5, 'Archivkategorie 2025.', 'Main Awards', 3, 'VTuber des Jahres', 2, 'vtuber-des-jahres', 1),
|
||||
(6, 'Archivkategorie 2025.', 'Performance', 3, 'Bestes Live Event', 2, 'bestes-live-event', 2),
|
||||
(7, 'Archivkategorie 2025.', 'Clips & Highlights', 3, 'Clip des Jahres', 2, 'clip-des-jahres', 3),
|
||||
(8, 'Archivkategorie 2024.', 'Main Awards', 3, 'VTuber des Jahres', 3, 'vtuber-des-jahres', 1),
|
||||
(9, 'Archivkategorie 2024.', 'Clips & Highlights', 3, 'Clip des Jahres', 3, 'clip-des-jahres', 2),
|
||||
(10, 'Archivkategorie 2023.', 'Main Awards', 3, 'VTuber des Jahres', 4, 'vtuber-des-jahres', 1);
|
||||
|
||||
INSERT INTO "VoteBallots" ("Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId") VALUES
|
||||
(1, 1, 'submitted', '2026-06-11T12:00:00+00:00', 'twitch_vote_1'),
|
||||
(2, 1, 'submitted', '2026-06-11T12:05:00+00:00', 'twitch_vote_2');
|
||||
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId") VALUES
|
||||
(1, 1, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 1),
|
||||
(2, 1, '@kurainu', 'Kurainu', 'Twitch', 1),
|
||||
(3, 1, '@shiroch', 'Shiro Ch.', 'Twitch', 1),
|
||||
(4, 2, '@kurainu', 'Kurainu 3D Live', 'Twitch', 1),
|
||||
(5, 2, '@aoisakura', 'Aoi Sakura Showcase', 'YouTube', 1),
|
||||
(6, 3, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 1),
|
||||
(7, 4, '@moonrelay', 'Moonrelay', 'Twitch', 1),
|
||||
(8, 5, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 2),
|
||||
(9, 6, '@kurainu', 'Kurainu 3D Live', 'Twitch', 2),
|
||||
(10, 7, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 2),
|
||||
(11, 8, '@aoisakura', 'Aoi Sakura', 'YouTube', 3),
|
||||
(12, 9, '@starbyte', 'Starbyte', 'Twitch', 3),
|
||||
(13, 10, '@tenshivox', 'Tenshi Vox', 'Twitch', 4);
|
||||
|
||||
INSERT INTO "Nominations" ("Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId") VALUES
|
||||
(1, NULL, 'Hoshimi Miyu', 1, '2026-06-10T13:00:00+00:00', 1, 'twitch_hoshi'),
|
||||
(2, NULL, 'Kurainu 3D Live', 2, '2026-06-10T14:00:00+00:00', 1, 'twitch_kurainu');
|
||||
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId") VALUES
|
||||
(1, 8, 'VTuber des Jahres', 2),
|
||||
(2, 9, 'Bestes Live Event', 2),
|
||||
(3, 10, 'Clip des Jahres', 2),
|
||||
(4, 11, 'VTuber des Jahres', 3),
|
||||
(5, 12, 'Clip des Jahres', 3),
|
||||
(6, 13, 'VTuber des Jahres', 4);
|
||||
|
||||
INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId") VALUES
|
||||
(1, 1, 1, 1),
|
||||
(2, 1, 4, 2),
|
||||
(3, 2, 2, 1),
|
||||
(4, 2, 6, 3);
|
||||
|
||||
SELECT setval(pg_get_serial_sequence('"Seasons"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Seasons";
|
||||
SELECT setval(pg_get_serial_sequence('"Categories"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Categories";
|
||||
SELECT setval(pg_get_serial_sequence('"VoteBallots"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "VoteBallots";
|
||||
SELECT setval(pg_get_serial_sequence('"Candidates"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Candidates";
|
||||
SELECT setval(pg_get_serial_sequence('"Nominations"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Nominations";
|
||||
SELECT setval(pg_get_serial_sequence('"Results"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Results";
|
||||
SELECT setval(pg_get_serial_sequence('"VoteEntries"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "VoteEntries";
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260617060000_InitialCreate', '8.0.11');
|
||||
@@ -0,0 +1,258 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" character varying(150) NOT NULL,
|
||||
"ProductVersion" character varying(32) NOT NULL,
|
||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||
);
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
CREATE TABLE "Seasons" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"Year" integer NOT NULL,
|
||||
"Name" character varying(160) NOT NULL,
|
||||
"IsCurrent" boolean NOT NULL,
|
||||
"IsCommunityOnly" boolean NOT NULL,
|
||||
"CurrentPhase" character varying(60) NOT NULL,
|
||||
"NominationStartsAt" date NOT NULL,
|
||||
"NominationEndsAt" date NOT NULL,
|
||||
"VotingStartsAt" date NOT NULL,
|
||||
"VotingEndsAt" date NOT NULL,
|
||||
"ReviewStartsAt" date NOT NULL,
|
||||
"ReviewEndsAt" date NOT NULL,
|
||||
"ShowDate" date NOT NULL,
|
||||
CONSTRAINT "PK_Seasons" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE "Categories" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"GroupName" character varying(80) NOT NULL,
|
||||
"Name" character varying(120) NOT NULL,
|
||||
"Slug" text NOT NULL,
|
||||
"Description" character varying(400) NOT NULL,
|
||||
"SortOrder" integer NOT NULL,
|
||||
"MaxNomineesPerUser" integer NOT NULL,
|
||||
CONSTRAINT "PK_Categories" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_Categories_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "VoteBallots" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"SubmittedByTwitchId" character varying(120) NOT NULL,
|
||||
"Status" character varying(30) NOT NULL,
|
||||
"SubmittedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_VoteBallots" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_VoteBallots_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Candidates" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"CategoryId" integer NOT NULL,
|
||||
"DisplayName" character varying(120) NOT NULL,
|
||||
"ChannelSlug" character varying(120) NOT NULL,
|
||||
"Platform" character varying(40) NOT NULL,
|
||||
CONSTRAINT "PK_Candidates" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_Candidates_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Candidates_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Nominations" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"CategoryId" integer NOT NULL,
|
||||
"SubmittedByTwitchId" character varying(120) NOT NULL,
|
||||
"CandidateId" integer,
|
||||
"CandidateText" character varying(120),
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_Nominations" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_Nominations_Candidates_CandidateId" FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id"),
|
||||
CONSTRAINT "FK_Nominations_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Nominations_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Results" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"SeasonId" integer NOT NULL,
|
||||
"CandidateId" integer NOT NULL,
|
||||
"CategoryName" character varying(120) NOT NULL,
|
||||
CONSTRAINT "PK_Results" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_Results_Candidates_CandidateId" FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Results_Seasons_SeasonId" FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "VoteEntries" (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"BallotId" integer NOT NULL,
|
||||
"CategoryId" integer NOT NULL,
|
||||
"CandidateId" integer NOT NULL,
|
||||
CONSTRAINT "PK_VoteEntries" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_VoteEntries_Candidates_CandidateId" FOREIGN KEY ("CandidateId") REFERENCES "Candidates" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_VoteEntries_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_VoteEntries_VoteBallots_BallotId" FOREIGN KEY ("BallotId") REFERENCES "VoteBallots" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year")
|
||||
VALUES (1, 'Community Voting', TRUE, TRUE, 'VTuber Star Awards 2026', DATE '2026-05-31', DATE '2026-05-01', DATE '2026-07-10', DATE '2026-07-01', DATE '2026-07-20', DATE '2026-06-30', DATE '2026-06-01', 2026);
|
||||
INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year")
|
||||
VALUES (2, 'Archived', TRUE, FALSE, 'VTuber Star Awards 2025', DATE '2025-05-31', DATE '2025-05-01', DATE '2025-07-10', DATE '2025-07-01', DATE '2025-07-20', DATE '2025-06-30', DATE '2025-06-01', 2025);
|
||||
INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year")
|
||||
VALUES (3, 'Archived', TRUE, FALSE, 'VTuber Star Awards 2024', DATE '2024-05-31', DATE '2024-05-01', DATE '2024-07-10', DATE '2024-07-01', DATE '2024-07-20', DATE '2024-06-30', DATE '2024-06-01', 2024);
|
||||
INSERT INTO "Seasons" ("Id", "CurrentPhase", "IsCommunityOnly", "IsCurrent", "Name", "NominationEndsAt", "NominationStartsAt", "ReviewEndsAt", "ReviewStartsAt", "ShowDate", "VotingEndsAt", "VotingStartsAt", "Year")
|
||||
VALUES (4, 'Archived', TRUE, FALSE, 'VTuber Star Awards 2023', DATE '2023-05-31', DATE '2023-05-01', DATE '2023-07-10', DATE '2023-07-01', DATE '2023-07-20', DATE '2023-06-30', DATE '2023-06-01', 2023);
|
||||
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (1, 'Die groesste Auszeichnung des Jahres.', 'Main Awards', 3, 'VTuber des Jahres', 1, 'vtuber-des-jahres', 1);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (2, 'Events, Konzerte und 3D-Shows.', 'Performance', 3, 'Bestes Live Event', 1, 'bestes-live-event', 2);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (3, 'Der lustigste oder emotionalste Clip des Jahres.', 'Clips & Highlights', 3, 'Clip des Jahres', 1, 'clip-des-jahres', 3);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (4, 'Die aktivste und freundlichste Community.', 'Main Awards', 3, 'Beste Community', 1, 'beste-community', 4);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (5, 'Archivkategorie 2025.', 'Main Awards', 3, 'VTuber des Jahres', 2, 'vtuber-des-jahres', 1);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (6, 'Archivkategorie 2025.', 'Performance', 3, 'Bestes Live Event', 2, 'bestes-live-event', 2);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (7, 'Archivkategorie 2025.', 'Clips & Highlights', 3, 'Clip des Jahres', 2, 'clip-des-jahres', 3);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (8, 'Archivkategorie 2024.', 'Main Awards', 3, 'VTuber des Jahres', 3, 'vtuber-des-jahres', 1);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (9, 'Archivkategorie 2024.', 'Clips & Highlights', 3, 'Clip des Jahres', 3, 'clip-des-jahres', 2);
|
||||
INSERT INTO "Categories" ("Id", "Description", "GroupName", "MaxNomineesPerUser", "Name", "SeasonId", "Slug", "SortOrder")
|
||||
VALUES (10, 'Archivkategorie 2023.', 'Main Awards', 3, 'VTuber des Jahres', 4, 'vtuber-des-jahres', 1);
|
||||
|
||||
INSERT INTO "VoteBallots" ("Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId")
|
||||
VALUES (1, 1, 'submitted', TIMESTAMPTZ '2026-06-11T12:00:00+00:00', 'twitch_vote_1');
|
||||
INSERT INTO "VoteBallots" ("Id", "SeasonId", "Status", "SubmittedAt", "SubmittedByTwitchId")
|
||||
VALUES (2, 1, 'submitted', TIMESTAMPTZ '2026-06-11T12:05:00+00:00', 'twitch_vote_2');
|
||||
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (1, 1, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (2, 1, '@kurainu', 'Kurainu', 'Twitch', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (3, 1, '@shiroch', 'Shiro Ch.', 'Twitch', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (4, 2, '@kurainu', 'Kurainu 3D Live', 'Twitch', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (5, 2, '@aoisakura', 'Aoi Sakura Showcase', 'YouTube', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (6, 3, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (7, 4, '@moonrelay', 'Moonrelay', 'Twitch', 1);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (8, 5, '@hoshimimiyu', 'Hoshimi Miyu', 'Twitch', 2);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (9, 6, '@kurainu', 'Kurainu 3D Live', 'Twitch', 2);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (10, 7, '@pyonkichikingdom', 'Pyonkichi Kingdom', 'Twitch', 2);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (11, 8, '@aoisakura', 'Aoi Sakura', 'YouTube', 3);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (12, 9, '@starbyte', 'Starbyte', 'Twitch', 3);
|
||||
INSERT INTO "Candidates" ("Id", "CategoryId", "ChannelSlug", "DisplayName", "Platform", "SeasonId")
|
||||
VALUES (13, 10, '@tenshivox', 'Tenshi Vox', 'Twitch', 4);
|
||||
|
||||
INSERT INTO "Nominations" ("Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId")
|
||||
VALUES (1, NULL, 'Hoshimi Miyu', 1, TIMESTAMPTZ '2026-06-10T13:00:00+00:00', 1, 'twitch_hoshi');
|
||||
INSERT INTO "Nominations" ("Id", "CandidateId", "CandidateText", "CategoryId", "CreatedAt", "SeasonId", "SubmittedByTwitchId")
|
||||
VALUES (2, NULL, 'Kurainu 3D Live', 2, TIMESTAMPTZ '2026-06-10T14:00:00+00:00', 1, 'twitch_kurainu');
|
||||
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId")
|
||||
VALUES (1, 8, 'VTuber des Jahres', 2);
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId")
|
||||
VALUES (2, 9, 'Bestes Live Event', 2);
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId")
|
||||
VALUES (3, 10, 'Clip des Jahres', 2);
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId")
|
||||
VALUES (4, 11, 'VTuber des Jahres', 3);
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId")
|
||||
VALUES (5, 12, 'Clip des Jahres', 3);
|
||||
INSERT INTO "Results" ("Id", "CandidateId", "CategoryName", "SeasonId")
|
||||
VALUES (6, 13, 'VTuber des Jahres', 4);
|
||||
|
||||
INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId")
|
||||
VALUES (1, 1, 1, 1);
|
||||
INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId")
|
||||
VALUES (2, 1, 4, 2);
|
||||
INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId")
|
||||
VALUES (3, 2, 2, 1);
|
||||
INSERT INTO "VoteEntries" ("Id", "BallotId", "CandidateId", "CategoryId")
|
||||
VALUES (4, 2, 6, 3);
|
||||
|
||||
CREATE INDEX "IX_Candidates_CategoryId" ON "Candidates" ("CategoryId");
|
||||
|
||||
CREATE INDEX "IX_Candidates_SeasonId" ON "Candidates" ("SeasonId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Categories_SeasonId_Slug" ON "Categories" ("SeasonId", "Slug");
|
||||
|
||||
CREATE INDEX "IX_Nominations_CandidateId" ON "Nominations" ("CandidateId");
|
||||
|
||||
CREATE INDEX "IX_Nominations_CategoryId" ON "Nominations" ("CategoryId");
|
||||
|
||||
CREATE INDEX "IX_Nominations_SeasonId" ON "Nominations" ("SeasonId");
|
||||
|
||||
CREATE INDEX "IX_Results_CandidateId" ON "Results" ("CandidateId");
|
||||
|
||||
CREATE INDEX "IX_Results_SeasonId" ON "Results" ("SeasonId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Seasons_Year" ON "Seasons" ("Year");
|
||||
|
||||
CREATE INDEX "IX_VoteBallots_SeasonId" ON "VoteBallots" ("SeasonId");
|
||||
|
||||
CREATE INDEX "IX_VoteEntries_BallotId" ON "VoteEntries" ("BallotId");
|
||||
|
||||
CREATE INDEX "IX_VoteEntries_CandidateId" ON "VoteEntries" ("CandidateId");
|
||||
|
||||
CREATE INDEX "IX_VoteEntries_CategoryId" ON "VoteEntries" ("CategoryId");
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"Seasons"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "Seasons") + 1,
|
||||
nextval(pg_get_serial_sequence('"Seasons"', 'Id'))),
|
||||
false);
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"Categories"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "Categories") + 1,
|
||||
nextval(pg_get_serial_sequence('"Categories"', 'Id'))),
|
||||
false);
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"VoteBallots"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "VoteBallots") + 1,
|
||||
nextval(pg_get_serial_sequence('"VoteBallots"', 'Id'))),
|
||||
false);
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"Candidates"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "Candidates") + 1,
|
||||
nextval(pg_get_serial_sequence('"Candidates"', 'Id'))),
|
||||
false);
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"Nominations"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "Nominations") + 1,
|
||||
nextval(pg_get_serial_sequence('"Nominations"', 'Id'))),
|
||||
false);
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"Results"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "Results") + 1,
|
||||
nextval(pg_get_serial_sequence('"Results"', 'Id'))),
|
||||
false);
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('"VoteEntries"', 'Id'),
|
||||
GREATEST(
|
||||
(SELECT MAX("Id") FROM "VoteEntries") + 1,
|
||||
nextval(pg_get_serial_sequence('"VoteEntries"', 'Id'))),
|
||||
false);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260617060000_InitialCreate', '8.0.11');
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,816 @@
|
||||
using Backend.Contracts;
|
||||
using Backend.Data;
|
||||
using Backend.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var connectionString = builder.Configuration["VTSA_POSTGRES"]
|
||||
?? builder.Configuration.GetConnectionString("Postgres");
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("frontend", policy =>
|
||||
{
|
||||
policy
|
||||
.WithOrigins(
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:4173",
|
||||
"http://127.0.0.1:4173")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<AwardsDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(connectionString);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
static string? ReadBearerToken(HttpContext context)
|
||||
{
|
||||
var header = context.Request.Headers.Authorization.ToString();
|
||||
return header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||
? header["Bearer ".Length..].Trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
static async Task<UserSession?> ResolveSessionAsync(HttpContext context, AwardsDbContext db)
|
||||
{
|
||||
var token = ReadBearerToken(context);
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var session = await db.UserSessions.FirstOrDefaultAsync(item => item.SessionToken == token && item.IsActive);
|
||||
if (session is not null)
|
||||
{
|
||||
session.LastSeenAt = DateTimeOffset.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors("frontend");
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AwardsDbContext>();
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
try
|
||||
{
|
||||
db.Database.Migrate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// In local environments without PostgreSQL yet, the API should still boot
|
||||
// so frontend work and migration generation can continue independently.
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SessionBootstrapper.EnsureAsync(db);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the session table bootstrap fails, the rest of the API can still start.
|
||||
}
|
||||
}
|
||||
|
||||
app.MapPost("/api/auth/dev-login", async (LoginRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = new UserSession
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SessionToken = Guid.NewGuid().ToString("N"),
|
||||
TwitchUserId = request.TwitchUserId.Trim(),
|
||||
DisplayName = request.DisplayName.Trim(),
|
||||
Role = string.Equals(request.Role, "admin", StringComparison.OrdinalIgnoreCase) ? "admin" : "viewer",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
LastSeenAt = DateTimeOffset.UtcNow,
|
||||
IsActive = true,
|
||||
};
|
||||
|
||||
db.UserSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new AuthSessionDto(
|
||||
session.SessionToken,
|
||||
session.TwitchUserId,
|
||||
session.DisplayName,
|
||||
session.Role));
|
||||
})
|
||||
.WithName("DevLogin")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/auth/session", async (HttpContext context, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
return Results.Ok(new AuthSessionDto(
|
||||
session.SessionToken,
|
||||
session.TwitchUserId,
|
||||
session.DisplayName,
|
||||
session.Role));
|
||||
})
|
||||
.WithName("GetSession")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/auth/logout", async (HttpContext context, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session is null)
|
||||
{
|
||||
return Results.Ok(new { loggedOut = true });
|
||||
}
|
||||
|
||||
session.IsActive = false;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { loggedOut = true });
|
||||
})
|
||||
.WithName("Logout")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }))
|
||||
.WithName("GetHealth")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/health/database", async (AwardsDbContext db) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var canConnect = await db.Database.CanConnectAsync();
|
||||
var pendingMigrations = canConnect
|
||||
? await db.Database.GetPendingMigrationsAsync()
|
||||
: Array.Empty<string>();
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
provider = "postgres",
|
||||
canConnect,
|
||||
pendingMigrations,
|
||||
configuredConnection = new
|
||||
{
|
||||
source = builder.Configuration["VTSA_POSTGRES"] is not null ? "environment" : "appsettings",
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return Results.Ok(new
|
||||
{
|
||||
provider = "postgres",
|
||||
canConnect = false,
|
||||
pendingMigrations = Array.Empty<string>(),
|
||||
configuredConnection = new
|
||||
{
|
||||
source = builder.Configuration["VTSA_POSTGRES"] is not null ? "environment" : "appsettings",
|
||||
},
|
||||
error = exception.Message,
|
||||
});
|
||||
}
|
||||
})
|
||||
.WithName("GetDatabaseHealth")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/public/overview", async (AwardsDbContext db) =>
|
||||
{
|
||||
var season = await db.Seasons
|
||||
.AsNoTracking()
|
||||
.Include(item => item.Categories.OrderBy(category => category.SortOrder))
|
||||
.Include(item => item.Results)
|
||||
.ThenInclude(result => result.Candidate)
|
||||
.FirstOrDefaultAsync(item => item.IsCurrent);
|
||||
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var response = new OverviewResponse(
|
||||
season.Id,
|
||||
season.Year,
|
||||
season.Name,
|
||||
season.ShowDate,
|
||||
season.CurrentPhase,
|
||||
season.IsCommunityOnly,
|
||||
"Twitch",
|
||||
new[]
|
||||
{
|
||||
new TimelineItem("nomination", "Nominierung", season.NominationStartsAt, season.NominationEndsAt, "done"),
|
||||
new TimelineItem("voting", "Voting", season.VotingStartsAt, season.VotingEndsAt, "active"),
|
||||
new TimelineItem("review", "Auswertung", season.ReviewStartsAt, season.ReviewEndsAt, "upcoming"),
|
||||
new TimelineItem("show", "Award Show", season.ShowDate, season.ShowDate, "upcoming"),
|
||||
},
|
||||
season.Categories
|
||||
.Take(6)
|
||||
.Select(category => new FeaturedCategoryDto(
|
||||
category.Id,
|
||||
category.GroupName,
|
||||
category.Name,
|
||||
category.Description,
|
||||
category.MaxNomineesPerUser))
|
||||
.ToArray(),
|
||||
season.Results
|
||||
.OrderByDescending(result => result.SeasonId)
|
||||
.Take(4)
|
||||
.Select(result => new WinnerPreviewDto(
|
||||
season.Year,
|
||||
result.CategoryName,
|
||||
result.Candidate.DisplayName,
|
||||
result.Candidate.ChannelSlug))
|
||||
.ToArray(),
|
||||
new[]
|
||||
{
|
||||
new FaqItemDto("Wer kann nominieren und voten?", "Jede Person mit Twitch Login. Das Konto wird beim ersten Login implizit erstellt."),
|
||||
new FaqItemDto("Wie werden Gewinner bestimmt?", "Aktuell rein community-basiert. Eine Mischlogik mit Jury oder Panel kann spaeter eingefuehrt werden."),
|
||||
new FaqItemDto("Wer verwaltet Kategorien und Unterkategorien?", "Das Team pflegt diese pro Jahr im Admin-Bereich."),
|
||||
});
|
||||
|
||||
return Results.Ok(response);
|
||||
})
|
||||
.WithName("GetOverview")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/public/seasons/{year:int}/categories", async (int year, AwardsDbContext db) =>
|
||||
{
|
||||
var season = await db.Seasons
|
||||
.AsNoTracking()
|
||||
.Include(item => item.Categories.OrderBy(category => category.SortOrder))
|
||||
.ThenInclude(category => category.Candidates.OrderBy(candidate => candidate.DisplayName))
|
||||
.FirstOrDefaultAsync(item => item.Year == year);
|
||||
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new SeasonCategoriesResponse(
|
||||
season.Id,
|
||||
season.Year,
|
||||
season.Categories.Select(category => new PublicCategoryDetailDto(
|
||||
category.Id,
|
||||
category.Name,
|
||||
category.GroupName,
|
||||
category.Description,
|
||||
category.MaxNomineesPerUser,
|
||||
category.Candidates.Select(candidate => new CandidateSummaryDto(
|
||||
candidate.Id,
|
||||
candidate.DisplayName,
|
||||
candidate.ChannelSlug,
|
||||
candidate.Platform))
|
||||
.ToArray()))
|
||||
.ToArray()));
|
||||
})
|
||||
.WithName("GetSeasonCategories")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/public/seasons/{year:int}/winners", async (int year, AwardsDbContext db) =>
|
||||
{
|
||||
var items = await db.Results
|
||||
.AsNoTracking()
|
||||
.Include(result => result.Candidate)
|
||||
.Where(result => result.Season.Year == year)
|
||||
.OrderBy(result => result.CategoryName)
|
||||
.Select(result => new WinnerArchiveItemDto(
|
||||
result.CategoryName,
|
||||
result.Candidate.DisplayName,
|
||||
result.Candidate.ChannelSlug))
|
||||
.ToArrayAsync();
|
||||
|
||||
return Results.Ok(new WinnerArchiveResponse(year, items));
|
||||
})
|
||||
.WithName("GetWinnerArchive")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/public/nominations", async (HttpContext context, CreateNominationRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
if (request.Nominees.Length is 0 or > 3)
|
||||
{
|
||||
return Results.BadRequest(new { message = "A nomination request must include between 1 and 3 nominees." });
|
||||
}
|
||||
|
||||
var distinctNominees = request.Nominees
|
||||
.Select(item => item.Trim())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
if (distinctNominees.Length != request.Nominees.Length)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Duplicate nominees are not allowed inside one category." });
|
||||
}
|
||||
|
||||
var category = await db.Categories
|
||||
.Include(item => item.Season)
|
||||
.FirstOrDefaultAsync(item => item.Id == request.CategoryId && item.Season.Year == request.Year);
|
||||
|
||||
if (category is null)
|
||||
{
|
||||
return Results.BadRequest(new { message = "The selected category does not exist for this season." });
|
||||
}
|
||||
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
var submitterId = session?.TwitchUserId ?? request.TwitchUserId;
|
||||
if (string.IsNullOrWhiteSpace(submitterId))
|
||||
{
|
||||
return Results.BadRequest(new { message = "A logged in user is required to submit nominations." });
|
||||
}
|
||||
|
||||
var records = distinctNominees.Select(name => new Nomination
|
||||
{
|
||||
SeasonId = category.SeasonId,
|
||||
CategoryId = category.Id,
|
||||
SubmittedByTwitchId = submitterId,
|
||||
CandidateText = name,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
|
||||
await db.Nominations.AddRangeAsync(records);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = distinctNominees.Length, category = category.Name });
|
||||
})
|
||||
.WithName("CreateNomination")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/public/votes", async (HttpContext context, CreateVoteRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
if (request.Entries.Length == 0)
|
||||
{
|
||||
return Results.BadRequest(new { message = "At least one vote entry is required." });
|
||||
}
|
||||
|
||||
var distinctCategoryCount = request.Entries
|
||||
.Select(item => item.CategoryId)
|
||||
.Distinct()
|
||||
.Count();
|
||||
|
||||
if (distinctCategoryCount != request.Entries.Length)
|
||||
{
|
||||
return Results.BadRequest(new { message = "Only one vote entry per category is allowed." });
|
||||
}
|
||||
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
var submitterId = session?.TwitchUserId ?? request.TwitchUserId;
|
||||
if (string.IsNullOrWhiteSpace(submitterId))
|
||||
{
|
||||
return Results.BadRequest(new { message = "A logged in user is required to submit votes." });
|
||||
}
|
||||
|
||||
var ballot = new VoteBallot
|
||||
{
|
||||
SeasonId = request.SeasonId,
|
||||
SubmittedByTwitchId = submitterId,
|
||||
SubmittedAt = DateTimeOffset.UtcNow,
|
||||
Status = "submitted",
|
||||
};
|
||||
|
||||
ballot.Entries = request.Entries.Select(entry => new VoteEntry
|
||||
{
|
||||
CategoryId = entry.CategoryId,
|
||||
CandidateId = entry.CandidateId,
|
||||
}).ToList();
|
||||
|
||||
await db.VoteBallots.AddAsync(ballot);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { ballotId = ballot.Id, entries = ballot.Entries.Count });
|
||||
})
|
||||
.WithName("CreateVote")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/admin/dashboard", async (HttpContext context, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var currentSeason = await db.Seasons.AsNoTracking().FirstOrDefaultAsync(item => item.IsCurrent);
|
||||
if (currentSeason 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.CandidateText != 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(group => new AdminTopCategoryDto(group.Key, group.Count()))
|
||||
.OrderByDescending(item => item.Votes)
|
||||
.Take(5)
|
||||
.ToArray();
|
||||
|
||||
var response = new AdminDashboardResponse(
|
||||
new[]
|
||||
{
|
||||
new AdminMetricDto("Nominierungen", nominationCount, "+12.4% vs. gestern"),
|
||||
new AdminMetricDto("Votes", voteCount, "+8.7% vs. gestern"),
|
||||
new AdminMetricDto("Kategorien", categoryCount, "aktiv im aktuellen Jahr"),
|
||||
new AdminMetricDto("Reviews offen", reviewCount, "Freitext und Dubletten"),
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new AdminActivityDto("Neue Nominierung in Best New VTuber", "vor 2 Min."),
|
||||
new AdminActivityDto("Clip-Dublette erkannt in Clip des Jahres", "vor 7 Min."),
|
||||
new AdminActivityDto("Alias-Merge fuer Hoshimi Miyu reviewt", "vor 18 Min."),
|
||||
},
|
||||
topCategories);
|
||||
|
||||
return Results.Ok(response);
|
||||
})
|
||||
.WithName("GetAdminDashboard")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/admin/seasons", async (HttpContext context, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var seasons = await db.Seasons
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(item => item.Year)
|
||||
.Select(item => new AdminSeasonListItemDto(
|
||||
item.Id,
|
||||
item.Year,
|
||||
item.Name,
|
||||
item.CurrentPhase,
|
||||
item.IsCurrent,
|
||||
item.Categories.Count))
|
||||
.ToArrayAsync();
|
||||
|
||||
return Results.Ok(seasons);
|
||||
})
|
||||
.WithName("GetAdminSeasons")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapGet("/api/admin/seasons/{seasonId:int}", async (HttpContext context, int seasonId, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var season = await db.Seasons
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(item => item.Id == seasonId);
|
||||
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var candidates = await db.Candidates
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
.OrderBy(item => item.DisplayName)
|
||||
.Select(item => new AdminCandidateItemDto(
|
||||
item.Id,
|
||||
item.CategoryId,
|
||||
item.DisplayName,
|
||||
item.ChannelSlug,
|
||||
item.Platform))
|
||||
.ToArrayAsync();
|
||||
|
||||
var candidateCounts = candidates
|
||||
.GroupBy(item => item.CategoryId)
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
|
||||
var categoryRows = await db.Categories
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ThenBy(item => item.Name)
|
||||
.Select(category => new
|
||||
{
|
||||
category.Id,
|
||||
category.GroupName,
|
||||
category.Name,
|
||||
category.Slug,
|
||||
category.Description,
|
||||
category.SortOrder,
|
||||
category.MaxNomineesPerUser,
|
||||
})
|
||||
.ToArrayAsync();
|
||||
|
||||
var categories = categoryRows
|
||||
.Select(category => new AdminCategoryItemDto(
|
||||
category.Id,
|
||||
category.GroupName,
|
||||
category.Name,
|
||||
category.Slug,
|
||||
category.Description,
|
||||
category.SortOrder,
|
||||
category.MaxNomineesPerUser,
|
||||
candidateCounts.TryGetValue(category.Id, out var count) ? count : 0))
|
||||
.ToArray();
|
||||
|
||||
var pendingNominations = await db.Nominations
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId && item.CandidateText != null)
|
||||
.OrderByDescending(item => item.CreatedAt)
|
||||
.Take(20)
|
||||
.Select(item => new AdminNominationReviewItemDto(
|
||||
item.Id,
|
||||
item.CategoryId,
|
||||
item.Category.Name,
|
||||
item.SubmittedByTwitchId,
|
||||
item.CandidateText!,
|
||||
item.CreatedAt))
|
||||
.ToArrayAsync();
|
||||
|
||||
return Results.Ok(new AdminSeasonDetailResponse(
|
||||
season.Id,
|
||||
season.Year,
|
||||
season.Name,
|
||||
season.CurrentPhase,
|
||||
season.IsCurrent,
|
||||
categories,
|
||||
candidates,
|
||||
pendingNominations));
|
||||
})
|
||||
.WithName("GetAdminSeasonDetail")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPut("/api/admin/seasons/{seasonId:int}", async (HttpContext context, int seasonId, UpdateSeasonRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId);
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
season.CurrentPhase = request.CurrentPhase.Trim();
|
||||
|
||||
if (request.IsCurrent && !season.IsCurrent)
|
||||
{
|
||||
var activeSeasons = await db.Seasons.Where(item => item.IsCurrent && item.Id != seasonId).ToListAsync();
|
||||
foreach (var activeSeason in activeSeasons)
|
||||
{
|
||||
activeSeason.IsCurrent = false;
|
||||
}
|
||||
}
|
||||
|
||||
season.IsCurrent = request.IsCurrent;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, seasonId = season.Id });
|
||||
})
|
||||
.WithName("UpdateAdminSeason")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/admin/seasons/{seasonId:int}/categories", async (HttpContext context, int seasonId, UpsertCategoryRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId);
|
||||
if (season is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var category = new Category
|
||||
{
|
||||
SeasonId = seasonId,
|
||||
GroupName = request.GroupName.Trim(),
|
||||
Name = request.Name.Trim(),
|
||||
Slug = request.Slug.Trim(),
|
||||
Description = request.Description.Trim(),
|
||||
SortOrder = request.SortOrder,
|
||||
MaxNomineesPerUser = request.MaxNomineesPerUser,
|
||||
};
|
||||
|
||||
db.Categories.Add(category);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, categoryId = category.Id });
|
||||
})
|
||||
.WithName("CreateAdminCategory")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPut("/api/admin/categories/{categoryId:int}", async (HttpContext context, int categoryId, UpsertCategoryRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == categoryId);
|
||||
if (category is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
category.GroupName = request.GroupName.Trim();
|
||||
category.Name = request.Name.Trim();
|
||||
category.Slug = request.Slug.Trim();
|
||||
category.Description = request.Description.Trim();
|
||||
category.SortOrder = request.SortOrder;
|
||||
category.MaxNomineesPerUser = request.MaxNomineesPerUser;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, categoryId = category.Id });
|
||||
})
|
||||
.WithName("UpdateAdminCategory")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/admin/seasons/{seasonId:int}/candidates", async (HttpContext context, int seasonId, UpsertCandidateRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == request.CategoryId && item.SeasonId == seasonId);
|
||||
if (category is null)
|
||||
{
|
||||
return Results.BadRequest(new { message = "The selected category does not exist in this season." });
|
||||
}
|
||||
|
||||
var candidate = new Candidate
|
||||
{
|
||||
SeasonId = seasonId,
|
||||
CategoryId = request.CategoryId,
|
||||
DisplayName = request.DisplayName.Trim(),
|
||||
ChannelSlug = request.ChannelSlug.Trim(),
|
||||
Platform = request.Platform.Trim(),
|
||||
};
|
||||
|
||||
db.Candidates.Add(candidate);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, candidateId = candidate.Id });
|
||||
})
|
||||
.WithName("CreateAdminCandidate")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPut("/api/admin/candidates/{candidateId:int}", async (HttpContext context, int candidateId, UpsertCandidateRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var candidate = await db.Candidates.FirstOrDefaultAsync(item => item.Id == candidateId);
|
||||
if (candidate is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
candidate.CategoryId = request.CategoryId;
|
||||
candidate.DisplayName = request.DisplayName.Trim();
|
||||
candidate.ChannelSlug = request.ChannelSlug.Trim();
|
||||
candidate.Platform = request.Platform.Trim();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, candidateId = candidate.Id });
|
||||
})
|
||||
.WithName("UpdateAdminCandidate")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/admin/nominations/{nominationId:int}/approve", async (HttpContext context, int nominationId, ApproveNominationRequest request, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var nomination = await db.Nominations
|
||||
.Include(item => item.Category)
|
||||
.FirstOrDefaultAsync(item => item.Id == nominationId);
|
||||
|
||||
if (nomination is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var rawDisplayName = string.IsNullOrWhiteSpace(request.DisplayName)
|
||||
? nomination.CandidateText
|
||||
: request.DisplayName.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawDisplayName))
|
||||
{
|
||||
return Results.BadRequest(new { message = "A display name is required to approve the nomination." });
|
||||
}
|
||||
|
||||
var channelSlug = request.ChannelSlug?.Trim() ?? string.Empty;
|
||||
var platform = string.IsNullOrWhiteSpace(request.Platform) ? "Twitch" : request.Platform.Trim();
|
||||
|
||||
var existingCandidate = await db.Candidates.FirstOrDefaultAsync(item =>
|
||||
item.SeasonId == nomination.SeasonId
|
||||
&& item.CategoryId == nomination.CategoryId
|
||||
&& item.DisplayName.ToLower() == rawDisplayName.ToLower());
|
||||
|
||||
var candidate = existingCandidate;
|
||||
if (candidate is null)
|
||||
{
|
||||
candidate = new Candidate
|
||||
{
|
||||
SeasonId = nomination.SeasonId,
|
||||
CategoryId = nomination.CategoryId,
|
||||
DisplayName = rawDisplayName,
|
||||
ChannelSlug = channelSlug,
|
||||
Platform = platform,
|
||||
};
|
||||
|
||||
db.Candidates.Add(candidate);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(channelSlug))
|
||||
{
|
||||
candidate.ChannelSlug = channelSlug;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(platform))
|
||||
{
|
||||
candidate.Platform = platform;
|
||||
}
|
||||
}
|
||||
|
||||
nomination.CandidateId = candidate.Id;
|
||||
nomination.CandidateText = null;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, nominationId = nomination.Id, candidateId = candidate.Id, created = existingCandidate is null });
|
||||
})
|
||||
.WithName("ApproveAdminNomination")
|
||||
.WithOpenApi();
|
||||
|
||||
app.MapPost("/api/admin/nominations/{nominationId:int}/reject", async (HttpContext context, int nominationId, AwardsDbContext db) =>
|
||||
{
|
||||
var session = await ResolveSessionAsync(context, db);
|
||||
if (session?.Role != "admin")
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var nomination = await db.Nominations.FirstOrDefaultAsync(item => item.Id == nominationId);
|
||||
if (nomination is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
nomination.CandidateText = null;
|
||||
nomination.CandidateId = null;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { saved = true, nominationId = nomination.Id, rejected = true });
|
||||
})
|
||||
.WithName("RejectAdminNomination")
|
||||
.WithOpenApi();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:0",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://127.0.0.1:5084",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://127.0.0.1:7241;http://127.0.0.1:5084",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# Backend
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
The API targets PostgreSQL through EF Core 8 and `Npgsql.EntityFrameworkCore.PostgreSQL`.
|
||||
|
||||
Default local development connection string:
|
||||
|
||||
```text
|
||||
Host=localhost;Port=5433;Database=vtuber_star_awards_dev;Username=postgres;Password=postgres
|
||||
```
|
||||
|
||||
You can override it with:
|
||||
|
||||
- `Backend/appsettings.Development.json`
|
||||
- environment variable `VTSA_POSTGRES`
|
||||
|
||||
If Docker is available locally, start a dev database from the repository root with:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
Restore and build:
|
||||
|
||||
```bash
|
||||
dotnet restore
|
||||
dotnet build
|
||||
```
|
||||
|
||||
Create a migration:
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add InitialCreate
|
||||
```
|
||||
|
||||
Generate a SQL migration script:
|
||||
|
||||
```bash
|
||||
dotnet ef migrations script 0 20260617060000_InitialCreate --output Migrations/InitialCreate.sql
|
||||
```
|
||||
|
||||
Apply migrations once PostgreSQL is running:
|
||||
|
||||
```bash
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
Fallback bootstrap if `dotnet ef` is not usable in the current environment:
|
||||
|
||||
```bash
|
||||
psql "Host=localhost Port=5433 Database=vtuber_star_awards_dev Username=postgres Password=postgres" -f Migrations/InitialCreate.manual.sql
|
||||
```
|
||||
|
||||
Run the API:
|
||||
|
||||
```bash
|
||||
ASPNETCORE_ENVIRONMENT=Development dotnet run
|
||||
```
|
||||
|
||||
Check the API and database wiring:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5084/api/health
|
||||
curl http://localhost:5084/api/health/database
|
||||
```
|
||||
|
||||
Development auth/session:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5084/api/auth/dev-login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"twitchUserId":"admin_demo","displayName":"Admin Demo","role":"admin"}'
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=localhost;Port=5433;Database=vtuber_star_awards_dev;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=localhost;Port=5432;Database=vtuber_star_awards;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user