From 6e0f4c5b90a71778cbbe7d649e3f52209edd24d3 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Mon, 16 Dec 2024 13:41:32 -0500 Subject: [PATCH] Add game session availability warning and enrollment trend by game --- src/Gameboard.Api/Data/Entities/Game.cs | 1 + ...onAvailabilityWarningThreshold.Designer.cs | 2161 +++++++++++++++++ ...GameSessionAvailabilityWarningThreshold.cs | 28 + ...meboardDbContextPostgreSQLModelSnapshot.cs | 3 + ...onAvailabilityWarningThreshold.Designer.cs | 2161 +++++++++++++++++ ...GameSessionAvailabilityWarningThreshold.cs | 28 + ...ameboardDbContextSqlServerModelSnapshot.cs | 3 + src/Gameboard.Api/Features/Game/Game.cs | 7 + .../Features/Game/GameController.cs | 5 + .../Features/Game/GameService.cs | 33 +- .../GetGameSessionAvailability.cs | 56 + .../GetGameSessionAvailabilityModels.cs | 9 + .../EnrollmentReportLineChart.cs | 51 +- .../EnrollmentReportModels.cs | 17 +- .../StartTeamSessions/StartTeamSessions.cs | 1 - .../StartTeamSessionsValidator.cs | 2 +- .../Structure/JsonDateTimeConverter.cs | 8 +- 17 files changed, 4531 insertions(+), 43 deletions(-) create mode 100644 src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.Designer.cs create mode 100644 src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.cs create mode 100644 src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.Designer.cs create mode 100644 src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.cs create mode 100644 src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailability.cs create mode 100644 src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailabilityModels.cs diff --git a/src/Gameboard.Api/Data/Entities/Game.cs b/src/Gameboard.Api/Data/Entities/Game.cs index 10ac87ef..6d08f15f 100644 --- a/src/Gameboard.Api/Data/Entities/Game.cs +++ b/src/Gameboard.Api/Data/Entities/Game.cs @@ -35,6 +35,7 @@ public class Game : IEntity public bool RequireSponsoredTeam { get; set; } public int SessionMinutes { get; set; } = 60; public int SessionLimit { get; set; } = 0; + public int? SessionAvailabilityWarningThreshold { get; set; } public int GamespaceLimitPerSession { get; set; } = 1; public bool IsPublished { get; set; } public bool AllowLateStart { get; set; } diff --git a/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.Designer.cs b/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.Designer.cs new file mode 100644 index 00000000..78e21ba3 --- /dev/null +++ b/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.Designer.cs @@ -0,0 +1,2161 @@ +// +using System; +using Gameboard.Api.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 Gameboard.Api.Data.Migrations.PostgreSQL.GameboardDb +{ + [DbContext(typeof(GameboardDbContextPostgreSQL))] + [Migration("20241213195535_AddGameSessionAvailabilityWarningThreshold")] + partial class AddGameSessionAvailabilityWarningThreshold + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Gameboard.Api.Data.ApiKey", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ExpiresOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NULL"); + + b.Property("GeneratedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnerId") + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ArchivedChallenge", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Duration") + .HasColumnType("bigint"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Events") + .HasColumnType("text"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GameName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HasGamespaceDeployed") + .HasColumnType("boolean"); + + b.Property("LastScoreTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("PlayerMode") + .HasColumnType("integer"); + + b.Property("PlayerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("Result") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Submissions") + .HasColumnType("text"); + + b.Property("Tag") + .HasColumnType("text"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TeamMembers") + .HasColumnType("text"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("TeamId"); + + b.HasIndex("UserId"); + + b.ToTable("ArchivedChallenges"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.AwardedChallengeBonus", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ChallengeBonusId") + .HasColumnType("character varying(40)"); + + b.Property("ChallengeId") + .HasColumnType("character varying(40)"); + + b.Property("EnteredOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("InternalSummary") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeBonusId"); + + b.HasIndex("ChallengeId"); + + b.ToTable("AwardedChallengeBonuses"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Challenge", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GameEngineType") + .HasColumnType("integer"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GraderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HasDeployedGamespace") + .HasColumnType("boolean"); + + b.Property("LastScoreTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PendingSubmission") + .HasColumnType("text"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("PlayerMode") + .HasColumnType("integer"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("SpecId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Tag") + .HasColumnType("text"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("WhenCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("TeamId"); + + b.ToTable("Challenges"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonus", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ChallengeBonusType") + .HasColumnType("integer"); + + b.Property("ChallengeSpecId") + .HasColumnType("character varying(40)"); + + b.Property("Description") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PointValue") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeSpecId"); + + b.ToTable("ChallengeBonuses"); + + b.HasDiscriminator("ChallengeBonusType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeEvent", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ChallengeId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Text") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeId"); + + b.ToTable("ChallengeEvents"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeGate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("RequiredId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("RequiredScore") + .HasColumnType("double precision"); + + b.Property("TargetId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("ChallengeGates"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSpec", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("AverageDeploySeconds") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExternalId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GameEngineType") + .HasColumnType("integer"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("R") + .HasColumnType("real"); + + b.Property("ShowSolutionGuideInCompetitiveMode") + .HasColumnType("boolean"); + + b.Property("SolutionGuideUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Tag") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Text") + .HasColumnType("text"); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("ChallengeSpecs"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSubmission", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Answers") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChallengeId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("Score") + .ValueGeneratedOnAdd() + .HasColumnType("double precision") + .HasDefaultValue(0.0); + + b.Property("SubmittedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeId"); + + b.ToTable("ChallengeSubmissions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.DenormalizedTeamScore", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("CumulativeTimeMs") + .HasColumnType("double precision"); + + b.Property("GameId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("ScoreAdvanced") + .HasColumnType("double precision"); + + b.Property("ScoreAutoBonus") + .HasColumnType("double precision"); + + b.Property("ScoreChallenge") + .HasColumnType("double precision"); + + b.Property("ScoreManualBonus") + .HasColumnType("double precision"); + + b.Property("ScoreOverall") + .HasColumnType("double precision"); + + b.Property("SolveCountComplete") + .HasColumnType("integer"); + + b.Property("SolveCountNone") + .HasColumnType("integer"); + + b.Property("SolveCountPartial") + .HasColumnType("integer"); + + b.Property("TeamId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TeamName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("DenormalizedTeamScores"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Extension", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("HostUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Type"); + + b.ToTable("Extensions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameHost", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ClientUrl") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DestroyResourcesOnDeployFailure") + .HasColumnType("boolean"); + + b.Property("GamespaceDeployBatchSize") + .HasColumnType("integer"); + + b.Property("HostApiKey") + .HasMaxLength(70) + .HasColumnType("character varying(70)"); + + b.Property("HostUrl") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("HttpTimeoutInSeconds") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PingEndpoint") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StartupEndpoint") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TeamExtendedEndpoint") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("ExternalGameHosts"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameTeam", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("DeployStatus") + .HasColumnType("integer"); + + b.Property("ExternalGameUrl") + .HasColumnType("text"); + + b.Property("GameId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("TeamId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasAlternateKey("TeamId", "GameId"); + + b.HasIndex("GameId"); + + b.ToTable("ExternalGameTeams"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Feedback", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Answers") + .HasColumnType("text"); + + b.Property("ChallengeId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ChallengeSpecId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Submitted") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeId"); + + b.HasIndex("ChallengeSpecId"); + + b.HasIndex("GameId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("UserId"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmission", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttachedEntityType") + .HasColumnType("integer"); + + b.Property("FeedbackTemplateId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("WhenCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("WhenEdited") + .HasColumnType("timestamp with time zone"); + + b.Property("WhenFinalized") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackTemplateId"); + + b.HasIndex("UserId"); + + b.ToTable("FeedbackSubmissions"); + + b.HasDiscriminator("AttachedEntityType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("HelpText") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.ToTable("FeedbackTemplates"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Game", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("AllowLateStart") + .HasColumnType("boolean"); + + b.Property("AllowPreview") + .HasColumnType("boolean"); + + b.Property("AllowPublicScoreboardAccess") + .HasColumnType("boolean"); + + b.Property("AllowReset") + .HasColumnType("boolean"); + + b.Property("Background") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CardText1") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CardText2") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CardText3") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CertificateTemplate") + .HasColumnType("text"); + + b.Property("ChallengesFeedbackTemplateId") + .HasColumnType("text"); + + b.Property("Competition") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Division") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ExternalHostId") + .HasColumnType("character varying(40)"); + + b.Property("FeedbackConfig") + .HasColumnType("text"); + + b.Property("FeedbackTemplateId") + .HasColumnType("text"); + + b.Property("GameEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("GameMarkdown") + .HasColumnType("text"); + + b.Property("GameStart") + .HasColumnType("timestamp with time zone"); + + b.Property("GamespaceLimitPerSession") + .HasColumnType("integer"); + + b.Property("IsFeatured") + .HasColumnType("boolean"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("Logo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MaxAttempts") + .HasColumnType("integer"); + + b.Property("MaxTeamSize") + .HasColumnType("integer"); + + b.Property("MinTeamSize") + .HasColumnType("integer"); + + b.Property("Mode") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PlayerMode") + .HasColumnType("integer"); + + b.Property("RegistrationClose") + .HasColumnType("timestamp with time zone"); + + b.Property("RegistrationConstraint") + .HasColumnType("text"); + + b.Property("RegistrationMarkdown") + .HasColumnType("text"); + + b.Property("RegistrationOpen") + .HasColumnType("timestamp with time zone"); + + b.Property("RegistrationType") + .HasColumnType("integer"); + + b.Property("RequireSponsoredTeam") + .HasColumnType("boolean"); + + b.Property("RequireSynchronizedStart") + .HasColumnType("boolean"); + + b.Property("Season") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SessionAvailabilityWarningThreshold") + .HasColumnType("integer"); + + b.Property("SessionLimit") + .HasColumnType("integer"); + + b.Property("SessionMinutes") + .HasColumnType("integer"); + + b.Property("ShowOnHomePageInPracticeMode") + .HasColumnType("boolean"); + + b.Property("Sponsor") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TestCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Track") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengesFeedbackTemplateId"); + + b.HasIndex("ExternalHostId"); + + b.HasIndex("FeedbackTemplateId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualBonus", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EnteredByUserId") + .HasColumnType("character varying(40)"); + + b.Property("EnteredOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PointValue") + .HasColumnType("double precision"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnteredByUserId"); + + b.ToTable("ManualBonuses"); + + b.HasDiscriminator("Type"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Player", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Advanced") + .HasColumnType("boolean"); + + b.Property("AdvancedFromGameId") + .HasColumnType("character varying(40)"); + + b.Property("AdvancedFromPlayerId") + .HasColumnType("character varying(40)"); + + b.Property("AdvancedFromTeamId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("AdvancedWithScore") + .HasColumnType("double precision"); + + b.Property("ApprovedName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CorrectCount") + .HasColumnType("integer"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("InviteCode") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("IsLateStart") + .HasColumnType("boolean"); + + b.Property("IsReady") + .HasColumnType("boolean"); + + b.Property("Mode") + .HasColumnType("integer"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NameStatus") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("PartialCount") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("SessionBegin") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionMinutes") + .HasColumnType("double precision"); + + b.Property("SponsorId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("WhenCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AdvancedFromGameId"); + + b.HasIndex("AdvancedFromPlayerId"); + + b.HasIndex("GameId"); + + b.HasIndex("SponsorId"); + + b.HasIndex("TeamId"); + + b.HasIndex("UserId"); + + b.HasIndex("Id", "TeamId"); + + b.HasIndex("UserId", "TeamId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PracticeModeSettings", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("AttemptLimit") + .HasColumnType("integer"); + + b.Property("CertificateHtmlTemplate") + .HasColumnType("text"); + + b.Property("DefaultPracticeSessionLengthMinutes") + .HasColumnType("integer"); + + b.Property("IntroTextMarkdown") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MaxConcurrentPracticeSessions") + .HasColumnType("integer"); + + b.Property("MaxPracticeSessionLengthMinutes") + .HasColumnType("integer"); + + b.Property("SuggestedSearches") + .HasColumnType("text"); + + b.Property("UpdatedByUserId") + .HasColumnType("character varying(40)"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedByUserId") + .IsUnique(); + + b.ToTable("PracticeModeSettings"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedCertificate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Mode") + .HasColumnType("integer"); + + b.Property("OwnerUserId") + .HasColumnType("character varying(40)"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("PublishedCertificate"); + + b.HasDiscriminator("Mode"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Sponsor", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("Logo") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentSponsorId") + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("ParentSponsorId"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettings", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("SupportPageGreeting") + .HasColumnType("text"); + + b.Property("UpdatedByUserId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedByUserId") + .IsUnique(); + + b.ToTable("SupportSettings"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettingsAutoTag", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConditionType") + .HasColumnType("integer"); + + b.Property("ConditionValue") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("SupportSettingsId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("Tag") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("SupportSettingsId"); + + b.ToTable("SupportSettingsAutoTags"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotification", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("EndsOn") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDismissible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("MarkdownContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("NotificationType") + .HasColumnType("integer"); + + b.Property("StartsOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.ToTable("SystemNotifications"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotificationInteraction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DismissedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("SawCalloutOn") + .HasColumnType("timestamp with time zone"); + + b.Property("SawFullNotificationOn") + .HasColumnType("timestamp with time zone"); + + b.Property("SystemNotificationId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasAlternateKey("SystemNotificationId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SystemNotificationInteractions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Ticket", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("AssigneeId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("ChallengeId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Key") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Key")); + + b.Property("Label") + .HasColumnType("text"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("RequesterId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("StaffCreated") + .HasColumnType("boolean"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("ChallengeId"); + + b.HasIndex("CreatorId"); + + b.HasIndex("Key") + .IsUnique(); + + b.HasIndex("PlayerId"); + + b.HasIndex("RequesterId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.TicketActivity", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssigneeId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TicketId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TicketId"); + + b.HasIndex("UserId"); + + b.ToTable("TicketActivity"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.User", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("ApprovedName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("HasDefaultSponsor") + .HasColumnType("boolean"); + + b.Property("LastLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LoginCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("0"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NameStatus") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("PlayAudioOnBrowserNotification") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SponsorId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.Property("Username") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("SponsorId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonusCompleteSolveRank", b => + { + b.HasBaseType("Gameboard.Api.Data.ChallengeBonus"); + + b.Property("SolveRank") + .HasColumnType("integer"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionChallengeSpec", b => + { + b.HasBaseType("Gameboard.Api.Data.FeedbackSubmission"); + + b.Property("ChallengeSpecId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.HasIndex("ChallengeSpecId"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionGame", b => + { + b.HasBaseType("Gameboard.Api.Data.FeedbackSubmission"); + + b.Property("GameId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.HasIndex("GameId"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualChallengeBonus", b => + { + b.HasBaseType("Gameboard.Api.Data.ManualBonus"); + + b.Property("ChallengeId") + .IsRequired() + .HasColumnType("character varying(40)"); + + b.HasIndex("ChallengeId"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualTeamBonus", b => + { + b.HasBaseType("Gameboard.Api.Data.ManualBonus"); + + b.Property("TeamId") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedCompetitiveCertificate", b => + { + b.HasBaseType("Gameboard.Api.Data.PublishedCertificate"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasIndex("GameId"); + + b.HasIndex("OwnerUserId"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedPracticeCertificate", b => + { + b.HasBaseType("Gameboard.Api.Data.PublishedCertificate"); + + b.Property("ChallengeSpecId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasIndex("ChallengeSpecId"); + + b.HasIndex("OwnerUserId"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ApiKey", b => + { + b.HasOne("Gameboard.Api.Data.User", "Owner") + .WithMany("ApiKeys") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.AwardedChallengeBonus", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeBonus", "ChallengeBonus") + .WithMany("AwardedTo") + .HasForeignKey("ChallengeBonusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("AwardedBonuses") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Challenge"); + + b.Navigation("ChallengeBonus"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Challenge", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Challenges") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.Player", "Player") + .WithMany("Challenges") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonus", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("Bonuses") + .HasForeignKey("ChallengeSpecId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ChallengeSpec"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeEvent", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Events") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeGate", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Prerequisites") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSpec", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Specs") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSubmission", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Submissions") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.DenormalizedTeamScore", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("DenormalizedTeamScores") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameTeam", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("ExternalGameTeams") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Feedback", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Feedback") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("Feedback") + .HasForeignKey("ChallengeSpecId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Feedback") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.Player", "Player") + .WithMany("Feedback") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("Feedback") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Challenge"); + + b.Navigation("ChallengeSpec"); + + b.Navigation("Game"); + + b.Navigation("Player"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmission", b => + { + b.HasOne("Gameboard.Api.Data.FeedbackTemplate", "FeedbackTemplate") + .WithMany("Submissions") + .HasForeignKey("FeedbackTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("FeedbackSubmissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Gameboard.Api.Features.Feedback.QuestionSubmission", "Responses", b1 => + { + b1.Property("FeedbackSubmissionId") + .HasColumnType("text"); + + b1.Property("Id") + .HasColumnType("text"); + + b1.Property("Answer") + .HasColumnType("text"); + + b1.Property("Prompt") + .HasColumnType("text"); + + b1.Property("ShortName") + .HasColumnType("text"); + + b1.HasKey("FeedbackSubmissionId", "Id"); + + b1.ToTable("FeedbackSubmissionResponses", (string)null); + + b1.WithOwner() + .HasForeignKey("FeedbackSubmissionId"); + }); + + b.Navigation("FeedbackTemplate"); + + b.Navigation("Responses"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackTemplate", b => + { + b.HasOne("Gameboard.Api.Data.User", "CreatedByUser") + .WithMany("CreatedFeedbackTemplates") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Game", b => + { + b.HasOne("Gameboard.Api.Data.FeedbackTemplate", "ChallengesFeedbackTemplate") + .WithMany("UseAsFeedbackTemplateForGames") + .HasForeignKey("ChallengesFeedbackTemplateId"); + + b.HasOne("Gameboard.Api.Data.ExternalGameHost", "ExternalHost") + .WithMany("UsedByGames") + .HasForeignKey("ExternalHostId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.FeedbackTemplate", "FeedbackTemplate") + .WithMany("UseAsFeedbackTemplateForGameChallenges") + .HasForeignKey("FeedbackTemplateId"); + + b.Navigation("ChallengesFeedbackTemplate"); + + b.Navigation("ExternalHost"); + + b.Navigation("FeedbackTemplate"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualBonus", b => + { + b.HasOne("Gameboard.Api.Data.User", "EnteredByUser") + .WithMany("EnteredManualBonuses") + .HasForeignKey("EnteredByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("EnteredByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Player", b => + { + b.HasOne("Gameboard.Api.Data.Game", "AdvancedFromGame") + .WithMany("AdvancedPlayers") + .HasForeignKey("AdvancedFromGameId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.Player", "AdvancedFromPlayer") + .WithMany("AdvancedToPlayers") + .HasForeignKey("AdvancedFromPlayerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Players") + .HasForeignKey("GameId"); + + b.HasOne("Gameboard.Api.Data.Sponsor", "Sponsor") + .WithMany("SponsoredPlayers") + .HasForeignKey("SponsorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("Enrollments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("AdvancedFromGame"); + + b.Navigation("AdvancedFromPlayer"); + + b.Navigation("Game"); + + b.Navigation("Sponsor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PracticeModeSettings", b => + { + b.HasOne("Gameboard.Api.Data.User", "UpdatedByUser") + .WithOne("UpdatedPracticeModeSettings") + .HasForeignKey("Gameboard.Api.Data.PracticeModeSettings", "UpdatedByUserId"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Sponsor", b => + { + b.HasOne("Gameboard.Api.Data.Sponsor", "ParentSponsor") + .WithMany("ChildSponsors") + .HasForeignKey("ParentSponsorId"); + + b.Navigation("ParentSponsor"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettings", b => + { + b.HasOne("Gameboard.Api.Data.User", "UpdatedByUser") + .WithOne("UpdatedSupportSettings") + .HasForeignKey("Gameboard.Api.Data.SupportSettings", "UpdatedByUserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettingsAutoTag", b => + { + b.HasOne("Gameboard.Api.Data.SupportSettings", "SupportSettings") + .WithMany("AutoTags") + .HasForeignKey("SupportSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SupportSettings"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotification", b => + { + b.HasOne("Gameboard.Api.Data.User", "CreatedByUser") + .WithMany("CreatedSystemNotifications") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotificationInteraction", b => + { + b.HasOne("Gameboard.Api.Data.SystemNotification", "SystemNotification") + .WithMany("Interactions") + .HasForeignKey("SystemNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("SystemNotificationInteractions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SystemNotification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Ticket", b => + { + b.HasOne("Gameboard.Api.Data.User", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId"); + + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Tickets") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId"); + + b.HasOne("Gameboard.Api.Data.Player", "Player") + .WithMany("Tickets") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.User", "Requester") + .WithMany() + .HasForeignKey("RequesterId"); + + b.Navigation("Assignee"); + + b.Navigation("Challenge"); + + b.Navigation("Creator"); + + b.Navigation("Player"); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.TicketActivity", b => + { + b.HasOne("Gameboard.Api.Data.User", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId"); + + b.HasOne("Gameboard.Api.Data.Ticket", "Ticket") + .WithMany("Activity") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Assignee"); + + b.Navigation("Ticket"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.User", b => + { + b.HasOne("Gameboard.Api.Data.Sponsor", "Sponsor") + .WithMany("SponsoredUsers") + .HasForeignKey("SponsorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sponsor"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionChallengeSpec", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("FeedbackSubmissions") + .HasForeignKey("ChallengeSpecId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChallengeSpec"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionGame", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("FeedbackSubmissions") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualChallengeBonus", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("AwardedManualBonuses") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedCompetitiveCertificate", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("PublishedCompetitiveCertificates") + .HasForeignKey("GameId"); + + b.HasOne("Gameboard.Api.Data.User", "OwnerUser") + .WithMany("PublishedCompetitiveCertificates") + .HasForeignKey("OwnerUserId") + .HasConstraintName("FK_OwnerUserId_Users_Id"); + + b.Navigation("Game"); + + b.Navigation("OwnerUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedPracticeCertificate", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("PublishedPracticeCertificates") + .HasForeignKey("ChallengeSpecId"); + + b.HasOne("Gameboard.Api.Data.User", "OwnerUser") + .WithMany("PublishedPracticeCertificates") + .HasForeignKey("OwnerUserId") + .HasConstraintName("FK_OwnerUserId_Users_Id"); + + b.Navigation("ChallengeSpec"); + + b.Navigation("OwnerUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Challenge", b => + { + b.Navigation("AwardedBonuses"); + + b.Navigation("AwardedManualBonuses"); + + b.Navigation("Events"); + + b.Navigation("Feedback"); + + b.Navigation("Submissions"); + + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonus", b => + { + b.Navigation("AwardedTo"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSpec", b => + { + b.Navigation("Bonuses"); + + b.Navigation("Feedback"); + + b.Navigation("FeedbackSubmissions"); + + b.Navigation("PublishedPracticeCertificates"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameHost", b => + { + b.Navigation("UsedByGames"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackTemplate", b => + { + b.Navigation("Submissions"); + + b.Navigation("UseAsFeedbackTemplateForGameChallenges"); + + b.Navigation("UseAsFeedbackTemplateForGames"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Game", b => + { + b.Navigation("AdvancedPlayers"); + + b.Navigation("Challenges"); + + b.Navigation("DenormalizedTeamScores"); + + b.Navigation("ExternalGameTeams"); + + b.Navigation("Feedback"); + + b.Navigation("FeedbackSubmissions"); + + b.Navigation("Players"); + + b.Navigation("Prerequisites"); + + b.Navigation("PublishedCompetitiveCertificates"); + + b.Navigation("Specs"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Player", b => + { + b.Navigation("AdvancedToPlayers"); + + b.Navigation("Challenges"); + + b.Navigation("Feedback"); + + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Sponsor", b => + { + b.Navigation("ChildSponsors"); + + b.Navigation("SponsoredPlayers"); + + b.Navigation("SponsoredUsers"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettings", b => + { + b.Navigation("AutoTags"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotification", b => + { + b.Navigation("Interactions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Ticket", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.User", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("CreatedFeedbackTemplates"); + + b.Navigation("CreatedSystemNotifications"); + + b.Navigation("Enrollments"); + + b.Navigation("EnteredManualBonuses"); + + b.Navigation("Feedback"); + + b.Navigation("FeedbackSubmissions"); + + b.Navigation("PublishedCompetitiveCertificates"); + + b.Navigation("PublishedPracticeCertificates"); + + b.Navigation("SystemNotificationInteractions"); + + b.Navigation("UpdatedPracticeModeSettings"); + + b.Navigation("UpdatedSupportSettings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.cs b/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.cs new file mode 100644 index 00000000..f3585092 --- /dev/null +++ b/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/20241213195535_AddGameSessionAvailabilityWarningThreshold.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Gameboard.Api.Data.Migrations.PostgreSQL.GameboardDb +{ + /// + public partial class AddGameSessionAvailabilityWarningThreshold : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SessionAvailabilityWarningThreshold", + table: "Games", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SessionAvailabilityWarningThreshold", + table: "Games"); + } + } +} diff --git a/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/GameboardDbContextPostgreSQLModelSnapshot.cs b/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/GameboardDbContextPostgreSQLModelSnapshot.cs index 1379316a..0373f461 100644 --- a/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/GameboardDbContextPostgreSQLModelSnapshot.cs +++ b/src/Gameboard.Api/Data/Migrations/PostgreSQL/GameboardDb/GameboardDbContextPostgreSQLModelSnapshot.cs @@ -859,6 +859,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("SessionAvailabilityWarningThreshold") + .HasColumnType("integer"); + b.Property("SessionLimit") .HasColumnType("integer"); diff --git a/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.Designer.cs b/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.Designer.cs new file mode 100644 index 00000000..8d7c3cdc --- /dev/null +++ b/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.Designer.cs @@ -0,0 +1,2161 @@ +// +using System; +using Gameboard.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Gameboard.Api.Data.Migrations.SqlServer.GameboardDb +{ + [DbContext(typeof(GameboardDbContextSqlServer))] + [Migration("20241213195549_AddGameSessionAvailabilityWarningThreshold")] + partial class AddGameSessionAvailabilityWarningThreshold + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Gameboard.Api.Data.ApiKey", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ExpiresOn") + .ValueGeneratedOnAdd() + .HasColumnType("datetimeoffset") + .HasDefaultValueSql("NULL"); + + b.Property("GeneratedOn") + .ValueGeneratedOnAdd() + .HasColumnType("datetimeoffset") + .HasDefaultValueSql("NOW()"); + + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("OwnerId") + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ArchivedChallenge", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Duration") + .HasColumnType("bigint"); + + b.Property("EndTime") + .HasColumnType("datetimeoffset"); + + b.Property("Events") + .HasColumnType("nvarchar(max)"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("GameName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("HasGamespaceDeployed") + .HasColumnType("bit"); + + b.Property("LastScoreTime") + .HasColumnType("datetimeoffset"); + + b.Property("LastSyncTime") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("PlayerMode") + .HasColumnType("int"); + + b.Property("PlayerName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("Result") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("datetimeoffset"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Submissions") + .HasColumnType("nvarchar(max)"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TeamMembers") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("TeamId"); + + b.HasIndex("UserId"); + + b.ToTable("ArchivedChallenges"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.AwardedChallengeBonus", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ChallengeBonusId") + .HasColumnType("nvarchar(40)"); + + b.Property("ChallengeId") + .HasColumnType("nvarchar(40)"); + + b.Property("EnteredOn") + .ValueGeneratedOnAdd() + .HasColumnType("datetimeoffset") + .HasDefaultValueSql("NOW()"); + + b.Property("InternalSummary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeBonusId"); + + b.HasIndex("ChallengeId"); + + b.ToTable("AwardedChallengeBonuses"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Challenge", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("EndTime") + .HasColumnType("datetimeoffset"); + + b.Property("ExternalId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("GameEngineType") + .HasColumnType("int"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("GraderKey") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("HasDeployedGamespace") + .HasColumnType("bit"); + + b.Property("LastScoreTime") + .HasColumnType("datetimeoffset"); + + b.Property("LastSyncTime") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PendingSubmission") + .HasColumnType("nvarchar(max)"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("PlayerMode") + .HasColumnType("int"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("float"); + + b.Property("SpecId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("StartTime") + .HasColumnType("datetimeoffset"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("WhenCreated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("TeamId"); + + b.ToTable("Challenges"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonus", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ChallengeBonusType") + .HasColumnType("int"); + + b.Property("ChallengeSpecId") + .HasColumnType("nvarchar(40)"); + + b.Property("Description") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("PointValue") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeSpecId"); + + b.ToTable("ChallengeBonuses"); + + b.HasDiscriminator("ChallengeBonusType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeEvent", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ChallengeId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Text") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeId"); + + b.ToTable("ChallengeEvents"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeGate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("RequiredId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("RequiredScore") + .HasColumnType("float"); + + b.Property("TargetId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("ChallengeGates"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSpec", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AverageDeploySeconds") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("ExternalId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("GameEngineType") + .HasColumnType("int"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("IsHidden") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("R") + .HasColumnType("real"); + + b.Property("ShowSolutionGuideInCompetitiveMode") + .HasColumnType("bit"); + + b.Property("SolutionGuideUrl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Tag") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Text") + .HasColumnType("nvarchar(max)"); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("ChallengeSpecs"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSubmission", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Answers") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChallengeId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("Score") + .ValueGeneratedOnAdd() + .HasColumnType("float") + .HasDefaultValue(0.0); + + b.Property("SubmittedOn") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeId"); + + b.ToTable("ChallengeSubmissions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.DenormalizedTeamScore", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("CumulativeTimeMs") + .HasColumnType("float"); + + b.Property("GameId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Rank") + .HasColumnType("int"); + + b.Property("ScoreAdvanced") + .HasColumnType("float"); + + b.Property("ScoreAutoBonus") + .HasColumnType("float"); + + b.Property("ScoreChallenge") + .HasColumnType("float"); + + b.Property("ScoreManualBonus") + .HasColumnType("float"); + + b.Property("ScoreOverall") + .HasColumnType("float"); + + b.Property("SolveCountComplete") + .HasColumnType("int"); + + b.Property("SolveCountNone") + .HasColumnType("int"); + + b.Property("SolveCountPartial") + .HasColumnType("int"); + + b.Property("TeamId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TeamName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("DenormalizedTeamScores"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Extension", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("HostUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasAlternateKey("Type"); + + b.ToTable("Extensions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameHost", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ClientUrl") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DestroyResourcesOnDeployFailure") + .HasColumnType("bit"); + + b.Property("GamespaceDeployBatchSize") + .HasColumnType("int"); + + b.Property("HostApiKey") + .HasMaxLength(70) + .HasColumnType("nvarchar(70)"); + + b.Property("HostUrl") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HttpTimeoutInSeconds") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("PingEndpoint") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StartupEndpoint") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TeamExtendedEndpoint") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("ExternalGameHosts"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameTeam", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("DeployStatus") + .HasColumnType("int"); + + b.Property("ExternalGameUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("GameId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("TeamId") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasAlternateKey("TeamId", "GameId"); + + b.HasIndex("GameId"); + + b.ToTable("ExternalGameTeams"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Feedback", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Answers") + .HasColumnType("nvarchar(max)"); + + b.Property("ChallengeId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ChallengeSpecId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Submitted") + .HasColumnType("bit"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengeId"); + + b.HasIndex("ChallengeSpecId"); + + b.HasIndex("GameId"); + + b.HasIndex("PlayerId"); + + b.HasIndex("UserId"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmission", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AttachedEntityType") + .HasColumnType("int"); + + b.Property("FeedbackTemplateId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("WhenCreated") + .HasColumnType("datetimeoffset"); + + b.Property("WhenEdited") + .HasColumnType("datetimeoffset"); + + b.Property("WhenFinalized") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackTemplateId"); + + b.HasIndex("UserId"); + + b.ToTable("FeedbackSubmissions"); + + b.HasDiscriminator("AttachedEntityType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackTemplate", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("HelpText") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.ToTable("FeedbackTemplates"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Game", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AllowLateStart") + .HasColumnType("bit"); + + b.Property("AllowPreview") + .HasColumnType("bit"); + + b.Property("AllowPublicScoreboardAccess") + .HasColumnType("bit"); + + b.Property("AllowReset") + .HasColumnType("bit"); + + b.Property("Background") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CardText1") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CardText2") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CardText3") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CertificateTemplate") + .HasColumnType("nvarchar(max)"); + + b.Property("ChallengesFeedbackTemplateId") + .HasColumnType("nvarchar(450)"); + + b.Property("Competition") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Division") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ExternalHostId") + .HasColumnType("nvarchar(40)"); + + b.Property("FeedbackConfig") + .HasColumnType("nvarchar(max)"); + + b.Property("FeedbackTemplateId") + .HasColumnType("nvarchar(450)"); + + b.Property("GameEnd") + .HasColumnType("datetimeoffset"); + + b.Property("GameMarkdown") + .HasColumnType("nvarchar(max)"); + + b.Property("GameStart") + .HasColumnType("datetimeoffset"); + + b.Property("GamespaceLimitPerSession") + .HasColumnType("int"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("Logo") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("MaxAttempts") + .HasColumnType("int"); + + b.Property("MaxTeamSize") + .HasColumnType("int"); + + b.Property("MinTeamSize") + .HasColumnType("int"); + + b.Property("Mode") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("PlayerMode") + .HasColumnType("int"); + + b.Property("RegistrationClose") + .HasColumnType("datetimeoffset"); + + b.Property("RegistrationConstraint") + .HasColumnType("nvarchar(max)"); + + b.Property("RegistrationMarkdown") + .HasColumnType("nvarchar(max)"); + + b.Property("RegistrationOpen") + .HasColumnType("datetimeoffset"); + + b.Property("RegistrationType") + .HasColumnType("int"); + + b.Property("RequireSponsoredTeam") + .HasColumnType("bit"); + + b.Property("RequireSynchronizedStart") + .HasColumnType("bit"); + + b.Property("Season") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("SessionAvailabilityWarningThreshold") + .HasColumnType("int"); + + b.Property("SessionLimit") + .HasColumnType("int"); + + b.Property("SessionMinutes") + .HasColumnType("int"); + + b.Property("ShowOnHomePageInPracticeMode") + .HasColumnType("bit"); + + b.Property("Sponsor") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TestCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Track") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("ChallengesFeedbackTemplateId"); + + b.HasIndex("ExternalHostId"); + + b.HasIndex("FeedbackTemplateId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualBonus", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EnteredByUserId") + .HasColumnType("nvarchar(40)"); + + b.Property("EnteredOn") + .ValueGeneratedOnAdd() + .HasColumnType("datetimeoffset") + .HasDefaultValueSql("NOW()"); + + b.Property("PointValue") + .HasColumnType("float"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("EnteredByUserId"); + + b.ToTable("ManualBonuses"); + + b.HasDiscriminator("Type"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Player", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Advanced") + .HasColumnType("bit"); + + b.Property("AdvancedFromGameId") + .HasColumnType("nvarchar(40)"); + + b.Property("AdvancedFromPlayerId") + .HasColumnType("nvarchar(40)"); + + b.Property("AdvancedFromTeamId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AdvancedWithScore") + .HasColumnType("float"); + + b.Property("ApprovedName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CorrectCount") + .HasColumnType("int"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("InviteCode") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("IsLateStart") + .HasColumnType("bit"); + + b.Property("IsReady") + .HasColumnType("bit"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("NameStatus") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("PartialCount") + .HasColumnType("int"); + + b.Property("Rank") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("SessionBegin") + .HasColumnType("datetimeoffset"); + + b.Property("SessionEnd") + .HasColumnType("datetimeoffset"); + + b.Property("SessionMinutes") + .HasColumnType("float"); + + b.Property("SponsorId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("WhenCreated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("AdvancedFromGameId"); + + b.HasIndex("AdvancedFromPlayerId"); + + b.HasIndex("GameId"); + + b.HasIndex("SponsorId"); + + b.HasIndex("TeamId"); + + b.HasIndex("UserId"); + + b.HasIndex("Id", "TeamId"); + + b.HasIndex("UserId", "TeamId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PracticeModeSettings", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AttemptLimit") + .HasColumnType("int"); + + b.Property("CertificateHtmlTemplate") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultPracticeSessionLengthMinutes") + .HasColumnType("int"); + + b.Property("IntroTextMarkdown") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("MaxConcurrentPracticeSessions") + .HasColumnType("int"); + + b.Property("MaxPracticeSessionLengthMinutes") + .HasColumnType("int"); + + b.Property("SuggestedSearches") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedByUserId") + .HasColumnType("nvarchar(40)"); + + b.Property("UpdatedOn") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedByUserId") + .IsUnique() + .HasFilter("[UpdatedByUserId] IS NOT NULL"); + + b.ToTable("PracticeModeSettings"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedCertificate", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("OwnerUserId") + .HasColumnType("nvarchar(40)"); + + b.Property("PublishedOn") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("PublishedCertificate"); + + b.HasDiscriminator("Mode"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Sponsor", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Approved") + .HasColumnType("bit"); + + b.Property("Logo") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ParentSponsorId") + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("ParentSponsorId"); + + b.ToTable("Sponsors"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettings", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("SupportPageGreeting") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedByUserId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("UpdatedOn") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedByUserId") + .IsUnique(); + + b.ToTable("SupportSettings"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettingsAutoTag", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConditionType") + .HasColumnType("int"); + + b.Property("ConditionValue") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("SupportSettingsId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("Tag") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("SupportSettingsId"); + + b.ToTable("SupportSettingsAutoTags"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotification", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("EndsOn") + .HasColumnType("datetimeoffset"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsDismissible") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("MarkdownContent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("StartsOn") + .HasColumnType("datetimeoffset"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.ToTable("SystemNotifications"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotificationInteraction", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("DismissedOn") + .HasColumnType("datetimeoffset"); + + b.Property("SawCalloutOn") + .HasColumnType("datetimeoffset"); + + b.Property("SawFullNotificationOn") + .HasColumnType("datetimeoffset"); + + b.Property("SystemNotificationId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasAlternateKey("SystemNotificationId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SystemNotificationInteractions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Ticket", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AssigneeId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("ChallengeId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatorId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Key") + .HasColumnType("int") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + + b.Property("Label") + .HasColumnType("nvarchar(max)"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("PlayerId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("RequesterId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("StaffCreated") + .HasColumnType("bit"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TeamId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("ChallengeId"); + + b.HasIndex("CreatorId"); + + b.HasIndex("Key") + .IsUnique(); + + b.HasIndex("PlayerId"); + + b.HasIndex("RequesterId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.TicketActivity", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AssigneeId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Attachments") + .HasColumnType("nvarchar(max)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("TicketId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TicketId"); + + b.HasIndex("UserId"); + + b.ToTable("TicketActivity"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.User", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ApprovedName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset"); + + b.Property("Email") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("HasDefaultSponsor") + .HasColumnType("bit"); + + b.Property("LastLoginDate") + .HasColumnType("datetimeoffset"); + + b.Property("LoginCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("0"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("NameStatus") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("PlayAudioOnBrowserNotification") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("SponsorId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.Property("Username") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("SponsorId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonusCompleteSolveRank", b => + { + b.HasBaseType("Gameboard.Api.Data.ChallengeBonus"); + + b.Property("SolveRank") + .HasColumnType("int"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionChallengeSpec", b => + { + b.HasBaseType("Gameboard.Api.Data.FeedbackSubmission"); + + b.Property("ChallengeSpecId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.HasIndex("ChallengeSpecId"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionGame", b => + { + b.HasBaseType("Gameboard.Api.Data.FeedbackSubmission"); + + b.Property("GameId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.HasIndex("GameId"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualChallengeBonus", b => + { + b.HasBaseType("Gameboard.Api.Data.ManualBonus"); + + b.Property("ChallengeId") + .IsRequired() + .HasColumnType("nvarchar(40)"); + + b.HasIndex("ChallengeId"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualTeamBonus", b => + { + b.HasBaseType("Gameboard.Api.Data.ManualBonus"); + + b.Property("TeamId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedCompetitiveCertificate", b => + { + b.HasBaseType("Gameboard.Api.Data.PublishedCertificate"); + + b.Property("GameId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasIndex("GameId"); + + b.HasIndex("OwnerUserId"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedPracticeCertificate", b => + { + b.HasBaseType("Gameboard.Api.Data.PublishedCertificate"); + + b.Property("ChallengeSpecId") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasIndex("ChallengeSpecId"); + + b.HasIndex("OwnerUserId"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ApiKey", b => + { + b.HasOne("Gameboard.Api.Data.User", "Owner") + .WithMany("ApiKeys") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.AwardedChallengeBonus", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeBonus", "ChallengeBonus") + .WithMany("AwardedTo") + .HasForeignKey("ChallengeBonusId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("AwardedBonuses") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Challenge"); + + b.Navigation("ChallengeBonus"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Challenge", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Challenges") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.Player", "Player") + .WithMany("Challenges") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonus", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("Bonuses") + .HasForeignKey("ChallengeSpecId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ChallengeSpec"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeEvent", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Events") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeGate", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Prerequisites") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSpec", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Specs") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSubmission", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Submissions") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.DenormalizedTeamScore", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("DenormalizedTeamScores") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameTeam", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("ExternalGameTeams") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Feedback", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Feedback") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("Feedback") + .HasForeignKey("ChallengeSpecId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Feedback") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.Player", "Player") + .WithMany("Feedback") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("Feedback") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Challenge"); + + b.Navigation("ChallengeSpec"); + + b.Navigation("Game"); + + b.Navigation("Player"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmission", b => + { + b.HasOne("Gameboard.Api.Data.FeedbackTemplate", "FeedbackTemplate") + .WithMany("Submissions") + .HasForeignKey("FeedbackTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("FeedbackSubmissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Gameboard.Api.Features.Feedback.QuestionSubmission", "Responses", b1 => + { + b1.Property("FeedbackSubmissionId") + .HasColumnType("nvarchar(450)"); + + b1.Property("Id") + .HasColumnType("nvarchar(450)"); + + b1.Property("Answer") + .HasColumnType("nvarchar(max)"); + + b1.Property("Prompt") + .HasColumnType("nvarchar(max)"); + + b1.Property("ShortName") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("FeedbackSubmissionId", "Id"); + + b1.ToTable("FeedbackSubmissionResponses", (string)null); + + b1.WithOwner() + .HasForeignKey("FeedbackSubmissionId"); + }); + + b.Navigation("FeedbackTemplate"); + + b.Navigation("Responses"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackTemplate", b => + { + b.HasOne("Gameboard.Api.Data.User", "CreatedByUser") + .WithMany("CreatedFeedbackTemplates") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Game", b => + { + b.HasOne("Gameboard.Api.Data.FeedbackTemplate", "ChallengesFeedbackTemplate") + .WithMany("UseAsFeedbackTemplateForGames") + .HasForeignKey("ChallengesFeedbackTemplateId"); + + b.HasOne("Gameboard.Api.Data.ExternalGameHost", "ExternalHost") + .WithMany("UsedByGames") + .HasForeignKey("ExternalHostId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.FeedbackTemplate", "FeedbackTemplate") + .WithMany("UseAsFeedbackTemplateForGameChallenges") + .HasForeignKey("FeedbackTemplateId"); + + b.Navigation("ChallengesFeedbackTemplate"); + + b.Navigation("ExternalHost"); + + b.Navigation("FeedbackTemplate"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualBonus", b => + { + b.HasOne("Gameboard.Api.Data.User", "EnteredByUser") + .WithMany("EnteredManualBonuses") + .HasForeignKey("EnteredByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("EnteredByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Player", b => + { + b.HasOne("Gameboard.Api.Data.Game", "AdvancedFromGame") + .WithMany("AdvancedPlayers") + .HasForeignKey("AdvancedFromGameId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.Player", "AdvancedFromPlayer") + .WithMany("AdvancedToPlayers") + .HasForeignKey("AdvancedFromPlayerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("Players") + .HasForeignKey("GameId"); + + b.HasOne("Gameboard.Api.Data.Sponsor", "Sponsor") + .WithMany("SponsoredPlayers") + .HasForeignKey("SponsorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("Enrollments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("AdvancedFromGame"); + + b.Navigation("AdvancedFromPlayer"); + + b.Navigation("Game"); + + b.Navigation("Sponsor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PracticeModeSettings", b => + { + b.HasOne("Gameboard.Api.Data.User", "UpdatedByUser") + .WithOne("UpdatedPracticeModeSettings") + .HasForeignKey("Gameboard.Api.Data.PracticeModeSettings", "UpdatedByUserId"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Sponsor", b => + { + b.HasOne("Gameboard.Api.Data.Sponsor", "ParentSponsor") + .WithMany("ChildSponsors") + .HasForeignKey("ParentSponsorId"); + + b.Navigation("ParentSponsor"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettings", b => + { + b.HasOne("Gameboard.Api.Data.User", "UpdatedByUser") + .WithOne("UpdatedSupportSettings") + .HasForeignKey("Gameboard.Api.Data.SupportSettings", "UpdatedByUserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettingsAutoTag", b => + { + b.HasOne("Gameboard.Api.Data.SupportSettings", "SupportSettings") + .WithMany("AutoTags") + .HasForeignKey("SupportSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SupportSettings"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotification", b => + { + b.HasOne("Gameboard.Api.Data.User", "CreatedByUser") + .WithMany("CreatedSystemNotifications") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotificationInteraction", b => + { + b.HasOne("Gameboard.Api.Data.SystemNotification", "SystemNotification") + .WithMany("Interactions") + .HasForeignKey("SystemNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany("SystemNotificationInteractions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SystemNotification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Ticket", b => + { + b.HasOne("Gameboard.Api.Data.User", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId"); + + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("Tickets") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId"); + + b.HasOne("Gameboard.Api.Data.Player", "Player") + .WithMany("Tickets") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Gameboard.Api.Data.User", "Requester") + .WithMany() + .HasForeignKey("RequesterId"); + + b.Navigation("Assignee"); + + b.Navigation("Challenge"); + + b.Navigation("Creator"); + + b.Navigation("Player"); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.TicketActivity", b => + { + b.HasOne("Gameboard.Api.Data.User", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId"); + + b.HasOne("Gameboard.Api.Data.Ticket", "Ticket") + .WithMany("Activity") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Gameboard.Api.Data.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Assignee"); + + b.Navigation("Ticket"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.User", b => + { + b.HasOne("Gameboard.Api.Data.Sponsor", "Sponsor") + .WithMany("SponsoredUsers") + .HasForeignKey("SponsorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sponsor"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionChallengeSpec", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("FeedbackSubmissions") + .HasForeignKey("ChallengeSpecId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChallengeSpec"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackSubmissionGame", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("FeedbackSubmissions") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ManualChallengeBonus", b => + { + b.HasOne("Gameboard.Api.Data.Challenge", "Challenge") + .WithMany("AwardedManualBonuses") + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedCompetitiveCertificate", b => + { + b.HasOne("Gameboard.Api.Data.Game", "Game") + .WithMany("PublishedCompetitiveCertificates") + .HasForeignKey("GameId"); + + b.HasOne("Gameboard.Api.Data.User", "OwnerUser") + .WithMany("PublishedCompetitiveCertificates") + .HasForeignKey("OwnerUserId") + .HasConstraintName("FK_OwnerUserId_Users_Id"); + + b.Navigation("Game"); + + b.Navigation("OwnerUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.PublishedPracticeCertificate", b => + { + b.HasOne("Gameboard.Api.Data.ChallengeSpec", "ChallengeSpec") + .WithMany("PublishedPracticeCertificates") + .HasForeignKey("ChallengeSpecId"); + + b.HasOne("Gameboard.Api.Data.User", "OwnerUser") + .WithMany("PublishedPracticeCertificates") + .HasForeignKey("OwnerUserId") + .HasConstraintName("FK_OwnerUserId_Users_Id"); + + b.Navigation("ChallengeSpec"); + + b.Navigation("OwnerUser"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Challenge", b => + { + b.Navigation("AwardedBonuses"); + + b.Navigation("AwardedManualBonuses"); + + b.Navigation("Events"); + + b.Navigation("Feedback"); + + b.Navigation("Submissions"); + + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeBonus", b => + { + b.Navigation("AwardedTo"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ChallengeSpec", b => + { + b.Navigation("Bonuses"); + + b.Navigation("Feedback"); + + b.Navigation("FeedbackSubmissions"); + + b.Navigation("PublishedPracticeCertificates"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.ExternalGameHost", b => + { + b.Navigation("UsedByGames"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.FeedbackTemplate", b => + { + b.Navigation("Submissions"); + + b.Navigation("UseAsFeedbackTemplateForGameChallenges"); + + b.Navigation("UseAsFeedbackTemplateForGames"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Game", b => + { + b.Navigation("AdvancedPlayers"); + + b.Navigation("Challenges"); + + b.Navigation("DenormalizedTeamScores"); + + b.Navigation("ExternalGameTeams"); + + b.Navigation("Feedback"); + + b.Navigation("FeedbackSubmissions"); + + b.Navigation("Players"); + + b.Navigation("Prerequisites"); + + b.Navigation("PublishedCompetitiveCertificates"); + + b.Navigation("Specs"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Player", b => + { + b.Navigation("AdvancedToPlayers"); + + b.Navigation("Challenges"); + + b.Navigation("Feedback"); + + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Sponsor", b => + { + b.Navigation("ChildSponsors"); + + b.Navigation("SponsoredPlayers"); + + b.Navigation("SponsoredUsers"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SupportSettings", b => + { + b.Navigation("AutoTags"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.SystemNotification", b => + { + b.Navigation("Interactions"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.Ticket", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Gameboard.Api.Data.User", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("CreatedFeedbackTemplates"); + + b.Navigation("CreatedSystemNotifications"); + + b.Navigation("Enrollments"); + + b.Navigation("EnteredManualBonuses"); + + b.Navigation("Feedback"); + + b.Navigation("FeedbackSubmissions"); + + b.Navigation("PublishedCompetitiveCertificates"); + + b.Navigation("PublishedPracticeCertificates"); + + b.Navigation("SystemNotificationInteractions"); + + b.Navigation("UpdatedPracticeModeSettings"); + + b.Navigation("UpdatedSupportSettings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.cs b/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.cs new file mode 100644 index 00000000..f3537a13 --- /dev/null +++ b/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/20241213195549_AddGameSessionAvailabilityWarningThreshold.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Gameboard.Api.Data.Migrations.SqlServer.GameboardDb +{ + /// + public partial class AddGameSessionAvailabilityWarningThreshold : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SessionAvailabilityWarningThreshold", + table: "Games", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SessionAvailabilityWarningThreshold", + table: "Games"); + } + } +} diff --git a/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/GameboardDbContextSqlServerModelSnapshot.cs b/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/GameboardDbContextSqlServerModelSnapshot.cs index 9c25bd27..86fadeef 100644 --- a/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/GameboardDbContextSqlServerModelSnapshot.cs +++ b/src/Gameboard.Api/Data/Migrations/SqlServer/GameboardDb/GameboardDbContextSqlServerModelSnapshot.cs @@ -860,6 +860,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("nvarchar(64)"); + b.Property("SessionAvailabilityWarningThreshold") + .HasColumnType("int"); + b.Property("SessionLimit") .HasColumnType("int"); diff --git a/src/Gameboard.Api/Features/Game/Game.cs b/src/Gameboard.Api/Features/Game/Game.cs index dbe99360..f53bc105 100644 --- a/src/Gameboard.Api/Features/Game/Game.cs +++ b/src/Gameboard.Api/Features/Game/Game.cs @@ -37,6 +37,7 @@ public class GameDetail public int MaxTeamSize { get; set; } public int SessionMinutes { get; set; } public int SessionLimit { get; set; } + public int? SessionAvailabilityWarningThreshold { get; set; } public int GamespaceLimitPerSession { get; set; } public string ExternalHostId { get; set; } public bool IsPublished { get; set; } @@ -150,3 +151,9 @@ public class GameGroup } public sealed record DeployGameResourcesBody(IEnumerable TeamIds); + +public sealed class GameActiveTeam +{ + public required string TeamId { get; set; } + public required DateTimeOffset SessionEnd { get; set; } +} diff --git a/src/Gameboard.Api/Features/Game/GameController.cs b/src/Gameboard.Api/Features/Game/GameController.cs index 5ed8e8ca..b52b664e 100644 --- a/src/Gameboard.Api/Features/Game/GameController.cs +++ b/src/Gameboard.Api/Features/Game/GameController.cs @@ -79,6 +79,11 @@ public async Task GetChallengeSpecs([FromRoute] string id) return await GameService.RetrieveChallengeSpecs(id); } + [HttpGet("api/game/{gameId}/session-availability")] + [Authorize] + public Task GetSessionAvailability([FromRoute] string gameId) + => _mediator.Send(new GameSessionAvailabilityQuery(gameId)); + [HttpGet("api/game/{id}/sessions")] [Authorize] public async Task GetSessionForecast([FromRoute] string id) diff --git a/src/Gameboard.Api/Features/Game/GameService.cs b/src/Gameboard.Api/Features/Game/GameService.cs index e66d4132..0698fc15 100644 --- a/src/Gameboard.Api/Features/Game/GameService.cs +++ b/src/Gameboard.Api/Features/Game/GameService.cs @@ -25,7 +25,7 @@ public interface IGameService Task Create(NewGame model); Task Export(GameSpecExport model); Task Import(GameSpecImport model); - Task> GetTeamsWithActiveSession(string GameId, CancellationToken cancellationToken); + IQueryable GetTeamsWithActiveSession(string GameId); Task IsUserPlaying(string gameId, string userId); Task> List(GameSearchFilter model = null, bool sudo = false); Task ListGrouped(GameSearchFilter model, bool sudo); @@ -108,28 +108,17 @@ public async Task Retrieve(string id, bool accessHidden = true) return entity; } - public async Task> GetTeamsWithActiveSession(string gameId, CancellationToken cancellationToken) - { - var gameSessionData = await _store - .WithNoTracking() - .Where(g => g.Id == gameId) - .Select(g => new + public IQueryable GetTeamsWithActiveSession(string gameId) + => _store + .WithNoTracking() + .Where(p => p.GameId == gameId) + .Where(p => p.SessionEnd > _now.Get()) + .GroupBy(p => p.TeamId) + .Select(gr => new GameActiveTeam { - g.Id, - g.SessionLimit, - Teams = g - .Players - .Where(p => _now.Get() < p.SessionEnd) - .Select(p => p.TeamId) - .Distinct() - }) - .SingleOrDefaultAsync(cancellationToken); - - if (gameSessionData is not null) - return gameSessionData.Teams; - - return []; - } + TeamId = gr.Key, + SessionEnd = gr.Min(p => p.SessionEnd) + }); public async Task> List(GameSearchFilter model = null, bool sudo = false) { diff --git a/src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailability.cs b/src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailability.cs new file mode 100644 index 00000000..d9c43578 --- /dev/null +++ b/src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailability.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Gameboard.Api.Data; +using Gameboard.Api.Features.Teams; +using Gameboard.Api.Services; +using Gameboard.Api.Structure.MediatR; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Gameboard.Api.Features.Games; + +public record GameSessionAvailabilityQuery(string GameId) : IRequest; + +internal sealed class GameSessionAvailabilityHandler +( + IGameService gameService, + IStore store, + ITeamService teamService, + IValidatorService validator +) : IRequestHandler +{ + private readonly IGameService _gameService = gameService; + private readonly IStore _store = store; + private readonly ITeamService _teamService = teamService; + private readonly IValidatorService _validator = validator; + + public async Task Handle(GameSessionAvailabilityQuery request, CancellationToken cancellationToken) + { + await _validator + .Auth(c => c.RequireAuthentication()) + .AddEntityExistsValidator(request.GameId) + .Validate(cancellationToken); + + var sessionMax = await _store + .WithNoTracking() + .Where(g => g.Id == request.GameId) + .Select(g => g.SessionLimit) + .SingleAsync(cancellationToken); + + var teamsWithActiveSession = await _gameService + .GetTeamsWithActiveSession(request.GameId) + .ToArrayAsync(cancellationToken); + var nextSessionEnd = teamsWithActiveSession + .Select(t => t.SessionEnd) + .Where(d => d != DateTimeOffset.MinValue) + .FirstOrDefault(); + + return new GameSessionAvailibilityResponse + { + SessionsAvailable = Math.Max(sessionMax - teamsWithActiveSession.Length, 0), + NextSessionEnd = nextSessionEnd == DateTimeOffset.MinValue ? null : nextSessionEnd + }; + } +} diff --git a/src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailabilityModels.cs b/src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailabilityModels.cs new file mode 100644 index 00000000..9d521d8e --- /dev/null +++ b/src/Gameboard.Api/Features/Game/Requests/GetGameSessionAvailability/GetGameSessionAvailabilityModels.cs @@ -0,0 +1,9 @@ +using System; + +namespace Gameboard.Api.Features.Games; + +public sealed class GameSessionAvailibilityResponse +{ + public int SessionsAvailable { get; set; } + public DateTimeOffset? NextSessionEnd { get; set; } +} diff --git a/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportLineChart.cs b/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportLineChart.cs index 7b7d4088..d07f8664 100644 --- a/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportLineChart.cs +++ b/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportLineChart.cs @@ -4,9 +4,11 @@ using System.Threading; using System.Threading.Tasks; using Gameboard.Api.Data; +using Gameboard.Api.Features.Games; using Gameboard.Api.Structure.MediatR; using MediatR; using Microsoft.EntityFrameworkCore; +using ServiceStack; namespace Gameboard.Api.Features.Reports; @@ -29,7 +31,9 @@ public async Task Handle(EnrollmentReportLine await _validator.Validate(request.Parameters, cancellationToken); if (request.Parameters.TrendPeriod is null) + { request.Parameters.TrendPeriod = EnrollmentReportLineChartPeriod.All; + } // pull base query but select only what we need var results = await _reportService @@ -43,20 +47,42 @@ public async Task Handle(EnrollmentReportLine }) .WhereDateIsNotEmpty(p => p.EnrollDate) .OrderBy(p => p.EnrollDate) - .ToListAsync(cancellationToken); + .ToArrayAsync(cancellationToken); - // grouping stuff - var totalEnrolledPlayerCount = 0; - var playerGroups = new Dictionary(); + // we send down data by game and just by date, so let's store the player and game data in one place so we don't duplicate it + var gameMap = results.Select(p => p.Game).DistinctBy(g => g.Id).OrderBy(g => g.Name).ToDictionary(g => g.Id, g => g.Name); + var playerMap = results.DistinctBy(p => p.Id).ToDictionary(p => p.Id, p => p); + var byDate = new Dictionary>(); + var byGameByDate = new Dictionary>>(); + var distinctRegistrationDates = results + .Select(p => new DateTimeOffset(p.EnrollDate.Year, p.EnrollDate.Month, p.EnrollDate.Day, 0, 0, 0, p.EnrollDate.Offset)) + .Distinct() + .ToArray(); - foreach (var grouping in results.GroupBy(p => new DateTimeOffset(p.EnrollDate.Year, p.EnrollDate.Month, p.EnrollDate.Day, 0, 0, 0, p.EnrollDate.Offset))) + foreach (var distinctRegistrationDate in distinctRegistrationDates) { - totalEnrolledPlayerCount += grouping.Count(); - playerGroups[grouping.Key] = new EnrollmentReportLineChartGroup + var registeredPlayers = results + .Where + ( + p => + p.EnrollDate.Year <= distinctRegistrationDate.Year && + p.EnrollDate.Month <= distinctRegistrationDate.Month && + p.EnrollDate.Day <= distinctRegistrationDate.Day && + p.EnrollDate.Offset <= distinctRegistrationDate.Offset + ) + .Select(p => new EnrollmentReportLineChartPlayerGame { Id = p.Id, GameId = p.Game.Id }); + + byDate.Add(distinctRegistrationDate, registeredPlayers); + + foreach (var gameId in gameMap.Keys) { - Players = grouping, - TotalCount = totalEnrolledPlayerCount - }; + byGameByDate.EnsureKey(gameId, []); + byGameByDate[gameId].Add + ( + distinctRegistrationDate, + registeredPlayers.Where(p => p.GameId == gameId) + ); + } } return new EnrollmentReportLineChartResponse @@ -64,7 +90,10 @@ public async Task Handle(EnrollmentReportLine PeriodEnd = results.Select(p => p.EnrollDate).Max(), PeriodStart = results.Select(p => p.EnrollDate).Min(), PeriodType = request.Parameters.TrendPeriod.Value, - PlayerGroups = playerGroups + Games = gameMap, + Players = playerMap, + ByDate = byDate, + ByGameByDate = byGameByDate }; } } diff --git a/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportModels.cs b/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportModels.cs index 72bbb530..b604629c 100644 --- a/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportModels.cs +++ b/src/Gameboard.Api/Features/Reports/Queries/EnrollmentReport/EnrollmentReportModels.cs @@ -132,7 +132,10 @@ public class EnrollmentReportManualChallengeBonus public sealed class EnrollmentReportLineChartResponse { - public required IDictionary PlayerGroups { get; set; } + public required Dictionary> ByDate { get; set; } + public required Dictionary>> ByGameByDate { get; set; } + public required Dictionary Games { get; set; } + public required Dictionary Players { get; set; } public required EnrollmentReportLineChartPeriod PeriodType { get; set; } public required DateTimeOffset PeriodStart { get; set; } public required DateTimeOffset PeriodEnd { get; set; } @@ -146,12 +149,6 @@ public enum EnrollmentReportLineChartPeriod Month, } -public sealed class EnrollmentReportLineChartGroup -{ - public required IEnumerable Players { get; set; } - public required int TotalCount { get; set; } -} - public sealed class EnrollmentReportLineChartPlayer { public required string Id { get; set; } @@ -160,6 +157,12 @@ public sealed class EnrollmentReportLineChartPlayer public required SimpleEntity Game { get; set; } } +public sealed class EnrollmentReportLineChartPlayerGame +{ + public required string Id { get; set; } + public required string GameId { get; set; } +} + // this class isn't sent down with the report data, it's just used as an intermediate // while querying to minimize the amount we pull back from the db internal class EnrollmentReportChallengeQueryData diff --git a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs index 88e668db..d208205b 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessions.cs @@ -13,7 +13,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Npgsql.Replication; using ServiceStack; namespace Gameboard.Api.Features.Teams; diff --git a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs index aff68d6b..a43ccdcb 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/StartTeamSessions/StartTeamSessionsValidator.cs @@ -127,7 +127,7 @@ public async Task Validate(StartTeamSessionsCommand request, CancellationToken c ctx.AddValidationException(new GameNotActive(game.Id, game.GameStart, game.GameEnd)); // can't exceed the legal session limit if established - var activeSessions = await _gameService.GetTeamsWithActiveSession(game.Id, cancellationToken); + var activeSessions = await _gameService.GetTeamsWithActiveSession(game.Id).ToArrayAsync(cancellationToken); if (game.SessionLimit > 0 && activeSessions.Count() >= game.SessionLimit) { ctx.AddValidationException(new GameSessionLimitReached(new SimpleEntity { Id = game.Id, Name = game.Name }, game.SessionLimit, activeSessions.Count())); diff --git a/src/Gameboard.Api/Structure/JsonDateTimeConverter.cs b/src/Gameboard.Api/Structure/JsonDateTimeConverter.cs index 96fc3f84..c744ab1c 100644 --- a/src/Gameboard.Api/Structure/JsonDateTimeConverter.cs +++ b/src/Gameboard.Api/Structure/JsonDateTimeConverter.cs @@ -11,7 +11,13 @@ public class JsonDateTimeOffsetConverter : JsonConverter { public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return DateTimeOffset.Parse(reader.GetString()).ToUniversalTime(); + var dateString = reader.GetString(); + if (dateString.IsEmpty()) + { + return default; + } + + return DateTimeOffset.Parse(dateString).ToUniversalTime(); } public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)