diff --git a/Data/Data.csproj b/Data/Data.csproj index aecd12df..135585ae 100644 --- a/Data/Data.csproj +++ b/Data/Data.csproj @@ -10,6 +10,7 @@ + diff --git a/Data/DataModel/Puzzle.cs b/Data/DataModel/Puzzle.cs index e5bd0edf..5fda4126 100644 --- a/Data/DataModel/Puzzle.cs +++ b/Data/DataModel/Puzzle.cs @@ -25,6 +25,7 @@ public Puzzle (Puzzle source) IsPuzzle = source.IsPuzzle; IsMetaPuzzle = source.IsMetaPuzzle; IsFinalPuzzle = source.IsFinalPuzzle; + IsCheatCode = source.IsCheatCode; SolveValue = source.SolveValue; HintCoinsForSolve = source.HintCoinsForSolve; Token = source.Token; @@ -33,6 +34,9 @@ public Puzzle (Puzzle source) IsGloballyVisiblePrerequisite = source.IsGloballyVisiblePrerequisite; MinPrerequisiteCount = source.MinPrerequisiteCount; MinutesToAutomaticallySolve = source.MinutesToAutomaticallySolve; + MinutesOfEventLockout = source.MinutesOfEventLockout; + MaxAnnotationKey = source.MaxAnnotationKey; + SupportEmailAlias = source.SupportEmailAlias; } /// @@ -67,6 +71,11 @@ public Puzzle (Puzzle source) /// public bool IsFinalPuzzle { get; set; } = false; + /// + /// True if this puzzle is a "cheat code" (nee "Fast Forward") that should impact standings + /// + public bool IsCheatCode { get; set; } + /// /// The solve value /// @@ -111,6 +120,11 @@ public Puzzle (Puzzle source) /// public int? MinutesToAutomaticallySolve { get; set; } = null; + /// + /// How long to lock solvers out of the rest of the event + /// + public int MinutesOfEventLockout { get; set; } + /// /// Some puzzles let teams store annotations describing their ongoing work, so they can share those /// with their teammates. However, we don't want to let teams store arbitrary annotation data, @@ -137,6 +151,10 @@ public Puzzle (Puzzle source) /// public string SupportEmailAlias { get; set; } + // + // WARNING: If you add new properties add them to the constructor as well so importing will work. + // + /// /// File for the main puzzle (typically a PDF containing the puzzle) /// @@ -199,5 +217,9 @@ public IEnumerable SolveTokenFiles } public virtual List Submissions { get; set; } + + // + // WARNING: If you add new properties add them to the constructor as well so importing will work. + // } } \ No newline at end of file diff --git a/Data/Migrations/20190218005038_CheatsAndWhistles.Designer.cs b/Data/Migrations/20190218005038_CheatsAndWhistles.Designer.cs new file mode 100644 index 00000000..eaac7e6c --- /dev/null +++ b/Data/Migrations/20190218005038_CheatsAndWhistles.Designer.cs @@ -0,0 +1,940 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ServerCore.DataModel; + +namespace Data.Migrations +{ + [DbContext(typeof(PuzzleServerContext))] + [Migration("20190218005038_CheatsAndWhistles")] + partial class CheatsAndWhistles + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.8-servicing-32085") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(128); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name") + .HasMaxLength(128); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("ServerCore.DataModel.ContentFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventID"); + + b.Property("FileType"); + + b.Property("PuzzleID") + .IsRequired(); + + b.Property("ShortName") + .IsRequired(); + + b.Property("UrlString") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("PuzzleID"); + + b.HasIndex("EventID", "ShortName") + .IsUnique(); + + b.ToTable("ContentFiles"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Event", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AllowFeedback"); + + b.Property("AnswerSubmissionEnd"); + + b.Property("AnswersAvailableBegin"); + + b.Property("ContactEmail"); + + b.Property("EventBegin"); + + b.Property("HomePartial"); + + b.Property("IsInternEvent"); + + b.Property("LockoutDurationMultiplier"); + + b.Property("LockoutIncorrectGuessLimit"); + + b.Property("LockoutIncorrectGuessPeriod"); + + b.Property("MaxExternalsPerTeam"); + + b.Property("MaxNumberOfTeams"); + + b.Property("MaxSubmissionCount"); + + b.Property("MaxTeamSize"); + + b.Property("Name") + .IsRequired(); + + b.Property("ShowFastestSolves"); + + b.Property("StandingsAvailableBegin"); + + b.Property("StandingsOverride"); + + b.Property("TeamDeleteEnd"); + + b.Property("TeamMembershipChangeEnd"); + + b.Property("TeamMiscDataChangeEnd"); + + b.Property("TeamNameChangeEnd"); + + b.Property("TeamRegistrationBegin"); + + b.Property("TeamRegistrationEnd"); + + b.Property("UrlString"); + + b.HasKey("ID"); + + b.HasIndex("UrlString") + .IsUnique() + .HasFilter("[UrlString] IS NOT NULL"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("ServerCore.DataModel.EventAdmins", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Event.ID"); + + b.Property("User.ID"); + + b.HasKey("ID"); + + b.HasIndex("Event.ID") + .IsUnique() + .HasFilter("[Event.ID] IS NOT NULL"); + + b.HasIndex("User.ID"); + + b.ToTable("EventAdmins"); + }); + + modelBuilder.Entity("ServerCore.DataModel.EventAuthors", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Event.ID"); + + b.Property("User.ID"); + + b.HasKey("ID"); + + b.HasIndex("Event.ID") + .IsUnique() + .HasFilter("[Event.ID] IS NOT NULL"); + + b.HasIndex("User.ID"); + + b.ToTable("EventAuthors"); + }); + + modelBuilder.Entity("ServerCore.DataModel.EventTeams", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Event.ID"); + + b.Property("Teams.ID"); + + b.HasKey("ID"); + + b.HasIndex("Event.ID") + .IsUnique() + .HasFilter("[Event.ID] IS NOT NULL"); + + b.HasIndex("Teams.ID"); + + b.ToTable("EventTeams"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Feedback", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Difficulty"); + + b.Property("Fun"); + + b.Property("PuzzleID"); + + b.Property("SubmissionTime"); + + b.Property("SubmitterID"); + + b.Property("WrittenFeedback"); + + b.HasKey("ID"); + + b.HasIndex("PuzzleID"); + + b.HasIndex("SubmitterID"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Hint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Content") + .IsRequired(); + + b.Property("Cost"); + + b.Property("Description") + .IsRequired(); + + b.Property("DisplayOrder"); + + b.Property("PuzzleID"); + + b.HasKey("Id"); + + b.HasIndex("PuzzleID"); + + b.ToTable("Hints"); + }); + + modelBuilder.Entity("ServerCore.DataModel.HintStatePerTeam", b => + { + b.Property("TeamID"); + + b.Property("HintID"); + + b.Property("UnlockTime"); + + b.HasKey("TeamID", "HintID"); + + b.HasIndex("HintID"); + + b.ToTable("HintStatePerTeam"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Invitation", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EmailAddress"); + + b.Property("Expiration"); + + b.Property("InvitationCode"); + + b.Property("InvitationType"); + + b.Property("TeamID"); + + b.HasKey("ID"); + + b.HasIndex("TeamID"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Prerequisites", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("PrerequisiteID"); + + b.Property("PuzzleID"); + + b.HasKey("ID"); + + b.HasIndex("PrerequisiteID"); + + b.HasIndex("PuzzleID"); + + b.ToTable("Prerequisites"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Puzzle", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventID"); + + b.Property("Group"); + + b.Property("HintCoinsForSolve"); + + b.Property("IsCheatCode"); + + b.Property("IsFinalPuzzle"); + + b.Property("IsGloballyVisiblePrerequisite"); + + b.Property("IsMetaPuzzle"); + + b.Property("IsPuzzle"); + + b.Property("MaxAnnotationKey"); + + b.Property("MinPrerequisiteCount"); + + b.Property("MinutesOfEventLockout"); + + b.Property("MinutesToAutomaticallySolve"); + + b.Property("Name") + .IsRequired(); + + b.Property("OrderInGroup"); + + b.Property("SolveValue"); + + b.Property("SupportEmailAlias"); + + b.Property("Token"); + + b.HasKey("ID"); + + b.HasIndex("EventID"); + + b.ToTable("Puzzles"); + }); + + modelBuilder.Entity("ServerCore.DataModel.PuzzleAuthors", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AuthorID"); + + b.Property("PuzzleID"); + + b.HasKey("ID"); + + b.HasIndex("AuthorID"); + + b.HasIndex("PuzzleID"); + + b.ToTable("PuzzleAuthors"); + }); + + modelBuilder.Entity("ServerCore.DataModel.PuzzleStatePerTeam", b => + { + b.Property("PuzzleID"); + + b.Property("TeamID"); + + b.Property("IsEmailOnlyMode"); + + b.Property("LockoutExpiryTime"); + + b.Property("Notes"); + + b.Property("Printed"); + + b.Property("SolvedTime"); + + b.Property("UnlockedTime"); + + b.Property("WrongSubmissionCountBuffer"); + + b.HasKey("PuzzleID", "TeamID"); + + b.HasIndex("TeamID"); + + b.ToTable("PuzzleStatePerTeam"); + }); + + modelBuilder.Entity("ServerCore.DataModel.PuzzleUser", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Email"); + + b.Property("EmployeeAlias"); + + b.Property("IdentityUserId") + .IsRequired(); + + b.Property("IsGlobalAdmin"); + + b.Property("Name"); + + b.Property("PhoneNumber"); + + b.Property("TShirtSize"); + + b.Property("VisibleToOthers"); + + b.HasKey("ID"); + + b.ToTable("PuzzleUsers"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Response", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsSolution"); + + b.Property("Note"); + + b.Property("PuzzleID"); + + b.Property("ResponseText") + .IsRequired(); + + b.Property("SubmittedText") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("PuzzleID"); + + b.ToTable("Responses"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Submission", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("PuzzleID"); + + b.Property("ResponseID"); + + b.Property("SubmissionText") + .IsRequired(); + + b.Property("SubmitterID"); + + b.Property("TeamID"); + + b.Property("TimeSubmitted"); + + b.HasKey("ID"); + + b.HasIndex("PuzzleID"); + + b.HasIndex("ResponseID"); + + b.HasIndex("SubmitterID"); + + b.HasIndex("TeamID"); + + b.ToTable("Submissions"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Team", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CustomRoom"); + + b.Property("EventID"); + + b.Property("HintCoinCount"); + + b.Property("HintCoinsUsed"); + + b.Property("Name"); + + b.Property("PrimaryContactEmail"); + + b.Property("PrimaryPhoneNumber"); + + b.Property("RoomID"); + + b.Property("SecondaryPhoneNumber"); + + b.HasKey("ID"); + + b.HasIndex("EventID"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("ServerCore.DataModel.TeamApplication", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("PlayerID"); + + b.Property("TeamID"); + + b.HasKey("ID"); + + b.HasIndex("PlayerID"); + + b.HasIndex("TeamID"); + + b.ToTable("TeamApplications"); + }); + + modelBuilder.Entity("ServerCore.DataModel.TeamMembers", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Team.ID"); + + b.Property("User.ID"); + + b.HasKey("ID"); + + b.HasIndex("Team.ID"); + + b.HasIndex("User.ID"); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.ContentFile", b => + { + b.HasOne("ServerCore.DataModel.Event", "Event") + .WithMany() + .HasForeignKey("EventID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany("Contents") + .HasForeignKey("PuzzleID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.EventAdmins", b => + { + b.HasOne("ServerCore.DataModel.Event", "Event") + .WithOne("Admins") + .HasForeignKey("ServerCore.DataModel.EventAdmins", "Event.ID"); + + b.HasOne("ServerCore.DataModel.PuzzleUser", "Admin") + .WithMany() + .HasForeignKey("User.ID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.EventAuthors", b => + { + b.HasOne("ServerCore.DataModel.Event", "Event") + .WithOne("Authors") + .HasForeignKey("ServerCore.DataModel.EventAuthors", "Event.ID"); + + b.HasOne("ServerCore.DataModel.PuzzleUser", "Author") + .WithMany() + .HasForeignKey("User.ID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.EventTeams", b => + { + b.HasOne("ServerCore.DataModel.Event", "Event") + .WithOne("Teams") + .HasForeignKey("ServerCore.DataModel.EventTeams", "Event.ID"); + + b.HasOne("ServerCore.DataModel.Team", "Team") + .WithMany() + .HasForeignKey("Teams.ID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Feedback", b => + { + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany() + .HasForeignKey("PuzzleID"); + + b.HasOne("ServerCore.DataModel.PuzzleUser", "Submitter") + .WithMany() + .HasForeignKey("SubmitterID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Hint", b => + { + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany("Hints") + .HasForeignKey("PuzzleID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.HintStatePerTeam", b => + { + b.HasOne("ServerCore.DataModel.Hint", "Hint") + .WithMany() + .HasForeignKey("HintID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ServerCore.DataModel.Team", "Team") + .WithMany() + .HasForeignKey("TeamID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.Invitation", b => + { + b.HasOne("ServerCore.DataModel.Team") + .WithMany("Invitations") + .HasForeignKey("TeamID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Prerequisites", b => + { + b.HasOne("ServerCore.DataModel.Puzzle", "Prerequisite") + .WithMany() + .HasForeignKey("PrerequisiteID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany() + .HasForeignKey("PuzzleID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.Puzzle", b => + { + b.HasOne("ServerCore.DataModel.Event", "Event") + .WithMany() + .HasForeignKey("EventID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.PuzzleAuthors", b => + { + b.HasOne("ServerCore.DataModel.PuzzleUser", "Author") + .WithMany() + .HasForeignKey("AuthorID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany() + .HasForeignKey("PuzzleID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.PuzzleStatePerTeam", b => + { + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany() + .HasForeignKey("PuzzleID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ServerCore.DataModel.Team", "Team") + .WithMany() + .HasForeignKey("TeamID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.Response", b => + { + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany() + .HasForeignKey("PuzzleID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.Submission", b => + { + b.HasOne("ServerCore.DataModel.Puzzle", "Puzzle") + .WithMany("Submissions") + .HasForeignKey("PuzzleID"); + + b.HasOne("ServerCore.DataModel.Response", "Response") + .WithMany() + .HasForeignKey("ResponseID"); + + b.HasOne("ServerCore.DataModel.PuzzleUser", "Submitter") + .WithMany() + .HasForeignKey("SubmitterID"); + + b.HasOne("ServerCore.DataModel.Team", "Team") + .WithMany("Submissions") + .HasForeignKey("TeamID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.Team", b => + { + b.HasOne("ServerCore.DataModel.Event", "Event") + .WithMany() + .HasForeignKey("EventID"); + }); + + modelBuilder.Entity("ServerCore.DataModel.TeamApplication", b => + { + b.HasOne("ServerCore.DataModel.PuzzleUser", "Player") + .WithMany() + .HasForeignKey("PlayerID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ServerCore.DataModel.Team", "Team") + .WithMany() + .HasForeignKey("TeamID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ServerCore.DataModel.TeamMembers", b => + { + b.HasOne("ServerCore.DataModel.Team", "Team") + .WithMany() + .HasForeignKey("Team.ID"); + + b.HasOne("ServerCore.DataModel.PuzzleUser", "Member") + .WithMany() + .HasForeignKey("User.ID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/20190218005038_CheatsAndWhistles.cs b/Data/Migrations/20190218005038_CheatsAndWhistles.cs new file mode 100644 index 00000000..37fe0b0e --- /dev/null +++ b/Data/Migrations/20190218005038_CheatsAndWhistles.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Data.Migrations +{ + public partial class CheatsAndWhistles : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsCheatCode", + table: "Puzzles", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MinutesOfEventLockout", + table: "Puzzles", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsCheatCode", + table: "Puzzles"); + + migrationBuilder.DropColumn( + name: "MinutesOfEventLockout", + table: "Puzzles"); + } + } +} diff --git a/Data/Migrations/PuzzleServerContextModelSnapshot.cs b/Data/Migrations/PuzzleServerContextModelSnapshot.cs index 326854c2..159af1af 100644 --- a/Data/Migrations/PuzzleServerContextModelSnapshot.cs +++ b/Data/Migrations/PuzzleServerContextModelSnapshot.cs @@ -15,7 +15,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.1.4-rtm-31024") + .HasAnnotation("ProductVersion", "2.1.8-servicing-32085") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); @@ -463,6 +463,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("HintCoinsForSolve"); + b.Property("IsCheatCode"); + b.Property("IsFinalPuzzle"); b.Property("IsGloballyVisiblePrerequisite"); @@ -475,6 +477,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MinPrerequisiteCount"); + b.Property("MinutesOfEventLockout"); + b.Property("MinutesToAutomaticallySolve"); b.Property("Name") diff --git a/ServerCore/Pages/Events/CreateDemo.cshtml.cs b/ServerCore/Pages/Events/CreateDemo.cshtml.cs index 702af8b6..8c41b43d 100644 --- a/ServerCore/Pages/Events/CreateDemo.cshtml.cs +++ b/ServerCore/Pages/Events/CreateDemo.cshtml.cs @@ -163,6 +163,44 @@ public async Task OnPostAsync() }; _context.Puzzles.Add(other); + Puzzle cheat = new Puzzle + { + Name = "You're Despicable (cheat code)", + Event = Event, + IsPuzzle = true, + IsCheatCode = true, + SolveValue = -1, + Group = "Daffy's Delights", + OrderInGroup = 2, + MinPrerequisiteCount = 1 + }; + _context.Puzzles.Add(cheat); + + Puzzle lockIntro = new Puzzle + { + Name = "Wouldn't you know... (whistle stop intro)", + Event = Event, + IsPuzzle = true, + SolveValue = 0, + Group = "Roger's Railway", + OrderInGroup = 1, + MinPrerequisiteCount = 1 + }; + _context.Puzzles.Add(lockIntro); + + Puzzle lockPuzzle = new Puzzle + { + Name = "...Locked! (whistle stop, lasts 5 minutes)", + Event = Event, + IsPuzzle = true, + SolveValue = 0, + Group = "Roger's Railway", + OrderInGroup = 2, + MinPrerequisiteCount = 1, + MinutesOfEventLockout = 5 + }; + _context.Puzzles.Add(lockPuzzle); + await _context.SaveChangesAsync(); // @@ -178,6 +216,12 @@ public async Task OnPostAsync() _context.Responses.Add(new Response() { Puzzle = meta, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = other, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = other, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); + _context.Responses.Add(new Response() { Puzzle = cheat, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); + _context.Responses.Add(new Response() { Puzzle = cheat, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); + _context.Responses.Add(new Response() { Puzzle = lockIntro, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); + _context.Responses.Add(new Response() { Puzzle = lockIntro, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); + _context.Responses.Add(new Response() { Puzzle = lockPuzzle, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); + _context.Responses.Add(new Response() { Puzzle = lockPuzzle, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); string hint1Description = "Tell me about the rabbits, George."; string hint1Content = "O.K. Some day – we’re gonna get the jack together and we’re gonna have a little house and a couple of acres an’ a cow and some pigs and..."; @@ -206,6 +250,9 @@ public async Task OnPostAsync() _context.Prerequisites.Add(new Prerequisites() { Puzzle = meta, Prerequisite = intermediate }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = meta, Prerequisite = hard }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = other, Prerequisite = start }); + _context.Prerequisites.Add(new Prerequisites() { Puzzle = cheat, Prerequisite = start }); + _context.Prerequisites.Add(new Prerequisites() { Puzzle = lockIntro, Prerequisite = start }); + _context.Prerequisites.Add(new Prerequisites() { Puzzle = lockPuzzle, Prerequisite = lockIntro }); await _context.SaveChangesAsync(); diff --git a/ServerCore/Pages/Events/Map.cshtml b/ServerCore/Pages/Events/Map.cshtml index 004bd073..968b47f6 100644 --- a/ServerCore/Pages/Events/Map.cshtml +++ b/ServerCore/Pages/Events/Map.cshtml @@ -28,7 +28,7 @@ @foreach (var puzzle in Model.Puzzles) { - @(puzzle.Puzzle.Name) (@puzzle.SolveCount) + @(puzzle.Puzzle.Name) (@puzzle.SolveCount) } @@ -41,7 +41,7 @@ @(team.Rank) - @(team.Team.Name) + @(team.Team.Name) @Html.DisplayFor(modelItem => team.SolveCount) diff --git a/ServerCore/Pages/Events/Map.cshtml.cs b/ServerCore/Pages/Events/Map.cshtml.cs index 2eff1e5e..98f9a8c6 100644 --- a/ServerCore/Pages/Events/Map.cshtml.cs +++ b/ServerCore/Pages/Events/Map.cshtml.cs @@ -67,7 +67,13 @@ public async Task OnGetAsync() team.SolveCount++; team.Score += puzzle.Puzzle.SolveValue; - if (puzzle.Puzzle.IsFinalPuzzle) + if (puzzle.Puzzle.IsCheatCode) + { + team.CheatCodeUsed = true; + team.FinalMetaSolveTime = DateTime.MaxValue; + } + + if (puzzle.Puzzle.IsFinalPuzzle && !team.CheatCodeUsed) { team.FinalMetaSolveTime = state.SolvedTime.Value; } @@ -118,6 +124,7 @@ public class TeamStats public int Score { get; set; } public int SortOrder { get; set; } public int? Rank { get; set; } + public bool CheatCodeUsed { get; set; } public DateTime FinalMetaSolveTime { get; set; } = DateTime.MaxValue; } diff --git a/ServerCore/Pages/Events/Standings.cshtml.cs b/ServerCore/Pages/Events/Standings.cshtml.cs index 994ed2e9..d59df570 100644 --- a/ServerCore/Pages/Events/Standings.cshtml.cs +++ b/ServerCore/Pages/Events/Standings.cshtml.cs @@ -32,7 +32,9 @@ public async Task OnGetAsync(SortOrder? sort) Team = g.Key, SolveCount = g.Count(), Score = g.Sum(s => s.Puzzle.SolveValue), - FinalMetaSolveTime = g.Where(s => s.Puzzle.IsFinalPuzzle).Select(s => s.SolvedTime).FirstOrDefault() ?? DateTime.MaxValue + FinalMetaSolveTime = g.Where(s => s.Puzzle.IsCheatCode).Any() ? + DateTime.MaxValue : + (g.Where(s => s.Puzzle.IsFinalPuzzle).Select(s => s.SolvedTime).FirstOrDefault() ?? DateTime.MaxValue) }) .OrderBy(t => t.FinalMetaSolveTime).ThenByDescending(t => t.Score).ThenBy(t => t.Team.Name) .ToListAsync(); diff --git a/ServerCore/Pages/Puzzles/Create.cshtml b/ServerCore/Pages/Puzzles/Create.cshtml index 917ba1dc..4214bad9 100644 --- a/ServerCore/Pages/Puzzles/Create.cshtml +++ b/ServerCore/Pages/Puzzles/Create.cshtml @@ -45,6 +45,13 @@ +
+
+ +
+
@@ -87,6 +94,11 @@
+
+ + + +
diff --git a/ServerCore/Pages/Puzzles/Delete.cshtml b/ServerCore/Pages/Puzzles/Delete.cshtml index 7490f6ab..8bde177c 100644 --- a/ServerCore/Pages/Puzzles/Delete.cshtml +++ b/ServerCore/Pages/Puzzles/Delete.cshtml @@ -45,6 +45,12 @@
@Html.DisplayFor(model => model.Puzzle.IsFinalPuzzle)
+
+ @Html.DisplayNameFor(model => model.Puzzle.IsCheatCode) +
+
+ @Html.DisplayFor(model => model.Puzzle.IsCheatCode) +
@Html.DisplayNameFor(model => model.Puzzle.SolveValue)
@@ -87,6 +93,12 @@
@Html.DisplayFor(model => model.Puzzle.MinutesToAutomaticallySolve)
+
+ @Html.DisplayNameFor(model => model.Puzzle.MinutesOfEventLockout) +
+
+ @Html.DisplayFor(model => model.Puzzle.MinutesOfEventLockout) +
@Html.DisplayNameFor(model => model.Puzzle.MaxAnnotationKey)
diff --git a/ServerCore/Pages/Puzzles/Edit.cshtml b/ServerCore/Pages/Puzzles/Edit.cshtml index a14596d5..7a7ca5a7 100644 --- a/ServerCore/Pages/Puzzles/Edit.cshtml +++ b/ServerCore/Pages/Puzzles/Edit.cshtml @@ -49,6 +49,13 @@
+
+
+ +
+
@@ -91,6 +98,11 @@
+
+ + + +
diff --git a/ServerCore/Pages/Puzzles/Status.cshtml b/ServerCore/Pages/Puzzles/Status.cshtml index e4b4da86..d3421482 100644 --- a/ServerCore/Pages/Puzzles/Status.cshtml +++ b/ServerCore/Pages/Puzzles/Status.cshtml @@ -16,17 +16,17 @@ - + @Html.DisplayNameFor(model => model.PuzzleStatePerTeam[0].Team) @Html.DisplayNameFor(model => model.PuzzleStatePerTeam[0].Team.Name) - + @Html.DisplayNameFor(model => model.PuzzleStatePerTeam[0].UnlockedTime) - + @Html.DisplayNameFor(model => model.PuzzleStatePerTeam[0].SolvedTime) @@ -43,7 +43,7 @@ { - @Html.DisplayFor(modelItem => item.Team.Name) + @Html.DisplayFor(modelItem => item.Team.Name) @if (item.UnlockedTime == null) diff --git a/ServerCore/Pages/Submissions/Index.cshtml b/ServerCore/Pages/Submissions/Index.cshtml index 6735d32e..5d80da86 100644 --- a/ServerCore/Pages/Submissions/Index.cshtml +++ b/ServerCore/Pages/Submissions/Index.cshtml @@ -17,44 +17,50 @@
- @if (!string.IsNullOrEmpty(Model.AnswerToken)) - { - - } - else if (!@Model.Event.IsAnswerSubmissionActive) - { -
-

This event is not in session. No submissions will be accepted at this time.

-
- } - else if (Model.PuzzleState.IsEmailOnlyMode) - { - - } - else if (Model.PuzzleState.IsTeamLockedOut) - { - - } - else - { -
-
-
- - - + @if (!string.IsNullOrEmpty(Model.AnswerToken)) + { + -
- + } + else if (!@Model.Event.IsAnswerSubmissionActive) + { +
+

This event is not in session. No submissions will be accepted at this time.

- - } + } + else if (Model.PuzzleState.IsEmailOnlyMode) + { + + } + else if (Model.PuzzleState.IsTeamLockedOut) + { + + } + else if (Model.PuzzlesCausingGlobalLockout.Count != 0 && !Model.PuzzlesCausingGlobalLockout.Contains(Model.Puzzle)) + { + + } + else + { +
+
+
+ + + +
+
+ +
+
+ }
diff --git a/ServerCore/Pages/Submissions/Index.cshtml.cs b/ServerCore/Pages/Submissions/Index.cshtml.cs index 4ed364ec..0d0b2ee4 100644 --- a/ServerCore/Pages/Submissions/Index.cshtml.cs +++ b/ServerCore/Pages/Submissions/Index.cshtml.cs @@ -33,6 +33,8 @@ public IndexModel(PuzzleServerContext serverContext, UserManager u public string AnswerToken { get; set; } + public IList PuzzlesCausingGlobalLockout { get; set; } + public async Task OnPostAsync(int puzzleId, int teamId) { if (!this.Event.IsAnswerSubmissionActive) @@ -168,6 +170,8 @@ private async Task SetupContext(int puzzleId, int teamId) s.Puzzle.ID == puzzleId) .OrderBy(submission => submission.TimeSubmitted) .ToListAsync(); + + PuzzlesCausingGlobalLockout = await PuzzleStateHelper.PuzzlesCausingGlobalLockout(_context, Event, team).ToListAsync(); } /// diff --git a/ServerCore/Pages/Teams/Play.cshtml.cs b/ServerCore/Pages/Teams/Play.cshtml.cs index 110f6d4d..8d4a20e5 100644 --- a/ServerCore/Pages/Teams/Play.cshtml.cs +++ b/ServerCore/Pages/Teams/Play.cshtml.cs @@ -48,6 +48,13 @@ public async Task OnGetAsync(SortOrder? sort) // all puzzles for this event that are real puzzles var puzzlesInEventQ = _context.Puzzles.Where(puzzle => puzzle.Event.ID == this.Event.ID && puzzle.IsPuzzle); + // unless we're in a global lockout, then filter to those! + var puzzlesCausingGlobalLockoutQ = PuzzleStateHelper.PuzzlesCausingGlobalLockout(_context, Event, myTeam); + if (await puzzlesCausingGlobalLockoutQ.AnyAsync()) + { + puzzlesInEventQ = puzzlesCausingGlobalLockoutQ; + } + // all puzzle states for this team that are unlocked (note: IsUnlocked bool is going to harm perf, just null check the time here) // Note that it's OK if some puzzles do not yet have a state record; those puzzles are clearly still locked and hence invisible. var stateForTeamQ = _context.PuzzleStatePerTeam.Where(state => state.TeamID == this.TeamID && state.UnlockedTime != null); diff --git a/ServerCore/PuzzleStateHelper.cs b/ServerCore/PuzzleStateHelper.cs index ffc41e35..d06b9931 100644 --- a/ServerCore/PuzzleStateHelper.cs +++ b/ServerCore/PuzzleStateHelper.cs @@ -185,7 +185,7 @@ public static async Task SetSolveStateAsync( } // Award hint coins - if (value != null && puzzle.HintCoinsForSolve != 0) + if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0) { if (team != null) { @@ -340,6 +340,17 @@ public static async Task CheckForTimedUnlocksAsync( } } + public static IQueryable PuzzlesCausingGlobalLockout( + PuzzleServerContext context, + Event eventObj, + Team team) + { + DateTime now = DateTime.UtcNow; + return PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team) + .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesOfEventLockout != 0 && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesOfEventLockout) > now) + .Select((s) => s.Puzzle); + } + /// /// Get a writable query of puzzle state. In the course of constructing the query, it will instantiate any state records that are missing on the server. ///