/// <summary> /// Sets the MayBeAdminOrAuthor bit for all admins and authors of all events /// </summary> public async Task <IActionResult> OnPostMigrateAdminsAsync() { using (var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable)) { List <PuzzleUser> allAdmins = await(from admin in _context.EventAdmins select admin.Admin).ToListAsync(); List <PuzzleUser> allAuthors = await(from author in _context.EventAuthors select author.Author).ToListAsync(); foreach (PuzzleUser admin in allAdmins) { admin.MayBeAdminOrAuthor = true; } foreach (PuzzleUser author in allAuthors) { author.MayBeAdminOrAuthor = true; } await _context.SaveChangesAsync(); transaction.Commit(); } Status = "Admins and authors migrated"; return(Page()); }
public async Task <IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return(Page()); } var user = await _userManager.GetUserAsync(User); if (user == null) { return(NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.")); } var thisPuzzleUser = await PuzzleUser.GetPuzzleUserForCurrentUser(_context, User, _userManager); // enforce access rights, do not let these change! Input.ID = thisPuzzleUser.ID; Input.IsGlobalAdmin = thisPuzzleUser.IsGlobalAdmin; Input.IdentityUserId = thisPuzzleUser.IdentityUserId; _context.Entry(thisPuzzleUser).State = EntityState.Detached; _context.Attach(Input).State = EntityState.Modified; await _context.SaveChangesAsync(true); await _signInManager.RefreshSignInAsync(user); StatusMessage = "Your profile has been updated"; return(RedirectToPage()); }
public async Task <IActionResult> OnPostAsync(string returnUrl = null) { if (string.IsNullOrEmpty(PuzzleUser.Email)) { ModelState.AddModelError("PuzzleUser.Email", "An email is required."); } else if (!MailHelper.IsValidEmail(PuzzleUser.Email)) { ModelState.AddModelError("PuzzleUser.Email", "This email address is not valid."); } if (!ModelState.IsValid) { return(Page()); } var thisPuzzleUser = await PuzzleUser.GetPuzzleUserForCurrentUser(_context, User, _userManager); if (thisPuzzleUser != null) { _context.Entry(thisPuzzleUser).State = EntityState.Detached; } _context.Attach(PuzzleUser).State = EntityState.Modified; await _context.SaveChangesAsync(); return(Redirect(returnUrl)); }
/// <summary> /// Helper for deleting teams that correctly deletes dependent objects /// </summary> /// <param name="team">Team to delete</param> public static async Task DeleteTeamAsync(PuzzleServerContext context, Team team) { var puzzleStates = from puzzleState in context.PuzzleStatePerTeam where puzzleState.TeamID == team.ID select puzzleState; context.PuzzleStatePerTeam.RemoveRange(puzzleStates); var hintStates = from hintState in context.HintStatePerTeam where hintState.TeamID == team.ID select hintState; context.HintStatePerTeam.RemoveRange(hintStates); var submissions = from submission in context.Submissions where submission.Team == team select submission; context.Submissions.RemoveRange(submissions); var annotations = from annotation in context.Annotations where annotation.Team == team select annotation; context.Annotations.RemoveRange(annotations); context.Teams.Remove(team); await context.SaveChangesAsync(); }
public static async Task UpdateTeamsWhoSentResponse(PuzzleServerContext context, Response response) { using (IDbContextTransaction transaction = context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable)) { var submissionsThatMatchResponse = await(from PuzzleStatePerTeam pspt in context.PuzzleStatePerTeam join Submission sub in context.Submissions on pspt.Team equals sub.Team where pspt.PuzzleID == response.PuzzleID && sub.SubmissionText == response.SubmittedText select new { State = pspt, Submission = sub }).ToListAsync(); if (submissionsThatMatchResponse.Count > 0) { Puzzle puzzle = await context.Puzzles.Where((p) => p.ID == response.PuzzleID).FirstOrDefaultAsync(); foreach (var s in submissionsThatMatchResponse) { s.Submission.Response = response; context.Attach(s.Submission).State = EntityState.Modified; if (response.IsSolution && s.State.SolvedTime == null) { await SetSolveStateAsync(context, puzzle.Event, puzzle, s.State.Team, s.Submission.TimeSubmitted); } } await context.SaveChangesAsync(); transaction.Commit(); } } }
public async Task <IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return(Page()); } _context.Events.Add(Event); var loggedInUser = await PuzzleUser.GetPuzzleUserForCurrentUser(_context, User, _userManager); if (loggedInUser != null) { _context.EventAdmins.Add(new EventAdmins() { Event = Event, Admin = loggedInUser }); _context.EventAuthors.Add(new EventAuthors() { Event = Event, Author = loggedInUser }); } await _context.SaveChangesAsync(); return(RedirectToPage("./Index")); }
public async Task <IActionResult> OnPostConfirmationAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { ErrorMessage = "Error loading external login information during confirmation."; return(RedirectToPage("./Login", new { ReturnUrl = returnUrl })); } using (var transaction = _context.Database.BeginTransaction()) { var user = new IdentityUser { UserName = Input.Email, Email = Input.Email }; var result = await _userManager.CreateAsync(user); if (result.Succeeded) { result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) { Input.IdentityUserId = user.Id; if (ModelState.IsValid) { // If there are no GlobalAdmins make them the GlobalAdmin if ((Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development) && !_context.PuzzleUsers.Where(u => u.IsGlobalAdmin).Any()) { Input.IsGlobalAdmin = true; } _context.PuzzleUsers.Add(Input); await _context.SaveChangesAsync(true); transaction.Commit(); await _signInManager.SignInAsync(user, isPersistent : false); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); return(LocalRedirect(returnUrl)); } } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } } LoginProvider = info.LoginProvider; ReturnUrl = returnUrl; return(Page()); }
/// <summary> /// Set the solve state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are querying from</param> /// <param name="puzzle"> /// The puzzle; if null, get all puzzles in the event. /// </param> /// <param name="team"> /// The team; if null, get all the teams in the event. /// </param> /// <param name="value">The solve time (null if unsolving)</param> /// <param name="author"></param> /// <returns> /// A task that can be awaited for the solve/unsolve operation /// </returns> public static async Task SetSolveStateAsync( PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = PuzzleStateHelper .GetFullReadWriteQuery(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { // Only allow solved time to be modified if it is being marked as unsolved (set to null) or if it is being solved for the first time if (value == null || states[i].SolvedTime == null) { // Unlock puzzles when solving them if (value != null && states[i].UnlockedTime == null) { states[i].UnlockedTime = value; } states[i].SolvedTime = value; } } // Award hint coins if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0) { if (team != null) { team.HintCoinCount += puzzle.HintCoinsForSolve; } else { var allTeams = from Team curTeam in context.Teams where curTeam.Event == eventObj select curTeam; foreach (Team curTeam in allTeams) { curTeam.HintCoinCount += puzzle.HintCoinsForSolve; } } } await context.SaveChangesAsync(); // if this puzzle got solved, look for others to unlock if (puzzle != null && value != null) { await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context, eventObj, puzzle, team, value.Value); } }
public static async Task SetTeamQualificationAsync( PuzzleServerContext context, Team team, bool value) { team.IsDisqualified = value; await context.SaveChangesAsync(); }
/// <summary> /// Adds a user to a team after performing a number of checks to make sure that the change is valid /// </summary> /// <param name="context">The context to update</param> /// <param name="Event">The event that the team is for</param> /// <param name="EventRole">The event role of the user that is making this change</param> /// <param name="teamId">The id of the team the player should be added to</param> /// <param name="userId">The user that should be added to the team</param> /// <returns> /// A tuple where the first element is a boolean that indicates whether the player was successfully /// added to the team and the second element is a message to display that explains the error in the /// case where the user was not successfully added to the team /// </returns> public static async Task <Tuple <bool, string> > AddMemberAsync(PuzzleServerContext context, Event Event, EventRole EventRole, int teamId, int userId) { Team team = await context.Teams.FirstOrDefaultAsync(m => m.ID == teamId); if (team == null) { return(new Tuple <bool, string>(false, $"Could not find team with ID '{teamId}'. Check to make sure the team hasn't been removed.")); } var currentTeamMembers = await context.TeamMembers.Where(members => members.Team.ID == team.ID).ToListAsync(); if (currentTeamMembers.Count >= Event.MaxTeamSize && EventRole != EventRole.admin) { return(new Tuple <bool, string>(false, $"The team '{team.Name}' is full.")); } PuzzleUser user = await context.PuzzleUsers.FirstOrDefaultAsync(m => m.ID == userId); if (user == null) { return(new Tuple <bool, string>(false, $"Could not find user with ID '{userId}'. Check to make sure the user hasn't been removed.")); } if (user.EmployeeAlias == null && currentTeamMembers.Where((m) => m.Member.EmployeeAlias == null).Count() >= Event.MaxExternalsPerTeam) { return(new Tuple <bool, string>(false, $"The team '{team.Name}' is already at its maximum count of non-employee players, and '{user.Email}' has no registered alias.")); } if (await(from teamMember in context.TeamMembers where teamMember.Member == user && teamMember.Team.Event == Event select teamMember).AnyAsync()) { return(new Tuple <bool, string>(false, $"'{user.Email}' is already on a team in this event.")); } TeamMembers Member = new TeamMembers(); Member.Team = team; Member.Member = user; // Remove any applications the user might have started for this event var allApplications = from app in context.TeamApplications where app.Player == user && app.Team.Event == Event select app; context.TeamApplications.RemoveRange(allApplications); context.TeamMembers.Add(Member); await context.SaveChangesAsync(); return(new Tuple <bool, string>(true, "")); }
/// <summary> /// Set the unlock state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are querying from</param> /// <param name="puzzle">The puzzle; if null, get all puzzles in the event.</param> /// <param name="team">The team; if null, get all the teams in the event.</param> /// <param name="value">The unlock time (null if relocking)</param> /// <returns>A task that can be awaited for the unlock/lock operation</returns> public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { states[i].UnlockedTime = value; } await context.SaveChangesAsync(); }
/// <summary> /// Set the solve state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are querying from</param> /// <param name="puzzle"> /// The puzzle; if null, get all puzzles in the event. /// </param> /// <param name="team"> /// The team; if null, get all the teams in the event. /// </param> /// <param name="value">The solve time (null if unsolving)</param> /// <param name="author"></param> /// <returns> /// A task that can be awaited for the solve/unsolve operation /// </returns> public static async Task SetSolveStateAsync( PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper .GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { states[i].SolvedTime = value; } // Award hint coins if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0) { if (team != null) { team.HintCoinCount += puzzle.HintCoinsForSolve; } else { var allTeams = from Team curTeam in context.Teams where curTeam.Event == eventObj select curTeam; foreach (Team curTeam in allTeams) { curTeam.HintCoinCount += puzzle.HintCoinsForSolve; } } } await context.SaveChangesAsync(); // if this puzzle got solved, look for others to unlock if (value != null) { await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context, eventObj, puzzle, team, value.Value); } }
/// <summary> /// Set the unlock state of some puzzle state records. In the course of setting the state, instantiate any state records that are missing on the server. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are querying from</param> /// <param name="puzzle">The puzzle; if null, get all puzzles in the event.</param> /// <param name="team">The team; if null, get all the teams in the event.</param> /// <param name="value">The unlock time (null if relocking)</param> /// <returns>A task that can be awaited for the unlock/lock operation</returns> public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = PuzzleStateHelper.GetFullReadWriteQuery(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { // Only allow unlock time to be modified if we were relocking it (setting it to null) or unlocking it for the first time if (value == null || states[i].UnlockedTime == null) { states[i].UnlockedTime = value; } } await context.SaveChangesAsync(); }
/// <summary> /// Helper for deleting puzzles that correctly deletes dependent objects /// </summary> /// <param name="puzzle">Puzzle to delete</param> public static async Task DeletePuzzleAsync(PuzzleServerContext context, Puzzle puzzle) { // Delete all files associated with this puzzle foreach (ContentFile content in puzzle.Contents) { await FileManager.DeleteBlobAsync(content.Url); } // Delete all Prerequisites where this puzzle depends on or is depended upon by another puzzle foreach (Prerequisites thisPrerequisite in context.Prerequisites.Where((r) => r.Puzzle == puzzle || r.Prerequisite == puzzle)) { context.Prerequisites.Remove(thisPrerequisite); } context.Puzzles.Remove(puzzle); await context.SaveChangesAsync(); }
/// <summary> /// Helper for deleting teams that correctly deletes dependent objects /// </summary> /// <param name="team">Team to delete</param> public static async Task DeleteTeamAsync(PuzzleServerContext context, Team team, bool sendEmail = true) { var memberEmails = await(from teamMember in context.TeamMembers where teamMember.Team == team select teamMember.Member.Email).ToListAsync(); var applicationEmails = await(from app in context.TeamApplications where app.Team == team select app.Player.Email).ToListAsync(); var puzzleStates = from puzzleState in context.PuzzleStatePerTeam where puzzleState.TeamID == team.ID select puzzleState; context.PuzzleStatePerTeam.RemoveRange(puzzleStates); var hintStates = from hintState in context.HintStatePerTeam where hintState.TeamID == team.ID select hintState; context.HintStatePerTeam.RemoveRange(hintStates); var submissions = from submission in context.Submissions where submission.Team == team select submission; context.Submissions.RemoveRange(submissions); var annotations = from annotation in context.Annotations where annotation.Team == team select annotation; context.Annotations.RemoveRange(annotations); context.Teams.Remove(team); await context.SaveChangesAsync(); if (sendEmail) { MailHelper.Singleton.SendPlaintextBcc(memberEmails.Union(applicationEmails), $"{team.Event.Name}: Team '{team.Name}' has been deleted", $"Sorry! You can apply to another team if you wish, or create your own."); } }
public async Task <IActionResult> OnPostAsync(string returnUrl = null) { if (!ModelState.IsValid) { return(Page()); } var thisPuzzleUser = await PuzzleUser.GetPuzzleUserForCurrentUser(_context, User, _userManager); if (thisPuzzleUser != null) { _context.Entry(thisPuzzleUser).State = EntityState.Detached; } _context.Attach(PuzzleUser).State = EntityState.Modified; await _context.SaveChangesAsync(); return(Redirect(returnUrl)); }
public static async Task UpdateTeamsWhoSentResponse(PuzzleServerContext context, Response response) { using (IDbContextTransaction transaction = context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable)) { var submissionsThatMatchResponse = await(from PuzzleStatePerTeam pspt in context.PuzzleStatePerTeam join Submission sub in context.Submissions on pspt.Team equals sub.Team where pspt.PuzzleID == response.PuzzleID && sub.PuzzleID == response.PuzzleID && sub.SubmissionText == response.SubmittedText select new { State = pspt, Submission = sub }).ToListAsync(); if (submissionsThatMatchResponse.Count > 0) { Puzzle puzzle = await context.Puzzles.Where((p) => p.ID == response.PuzzleID).FirstOrDefaultAsync(); foreach (var s in submissionsThatMatchResponse) { s.Submission.Response = response; context.Attach(s.Submission).State = EntityState.Modified; if (response.IsSolution && s.State.SolvedTime == null) { await SetSolveStateAsync(context, puzzle.Event, puzzle, s.State.Team, s.Submission.TimeSubmitted); } } await context.SaveChangesAsync(); transaction.Commit(); var teamMembers = await(from TeamMembers tm in context.TeamMembers join Submission sub in context.Submissions on tm.Team equals sub.Team where sub.PuzzleID == response.PuzzleID && sub.SubmissionText == response.SubmittedText select tm.Member.Email).ToListAsync(); MailHelper.Singleton.SendPlaintextBcc(teamMembers, $"{puzzle.Event.Name}: {response.Puzzle.Name} Response updated for '{response.SubmittedText}'", $"The new response for this submission is: '{response.ResponseText}'."); } } }
/// <summary> /// This routine takes a single annotation that the requester has uploaded, and inserts /// it if it doesn't exist and updates it otherwise. /// </summary> private async Task InsertOrUpdateOneAnnotation(SyncResponse response, int puzzleId, int teamId, int key, string contents) { // Check to see if the annotation already exists. If so, update it and we're done. Annotation existingAnnotation = await context.Annotations.FirstOrDefaultAsync(a => a.PuzzleID == puzzleId && a.TeamID == teamId && a.Key == key); if (existingAnnotation != null) { context.Entry(existingAnnotation).State = EntityState.Detached; await UpdateOneAnnotation(response, puzzleId, teamId, key, contents); } else { // The annotation doesn't exist yet. So, try to generate a new annotation, with version 1. Annotation annotation = new Annotation { PuzzleID = puzzleId, TeamID = teamId, Key = key, Version = 1, Contents = contents, Timestamp = DateTime.Now }; try { context.Annotations.Add(annotation); await context.SaveChangesAsync(); } catch (DbUpdateException) { // If the insert fails, there must already be an annotation there with the // same puzzle ID, team ID, and key. (This means there was a race condition: // between the time we checked for the existence of a matching annotation and // now, there was another insert.) So, we need to update the existing one. // But first, we need to detach the annotation from the context so the context // doesn't think the annotation is in the database. context.Entry(annotation).State = EntityState.Detached; await UpdateOneAnnotation(response, puzzleId, teamId, key, contents); } } }
/// <summary> /// Set the email only mode of some puzzle state records. In the course /// of setting the state, instantiate any state records that are /// missing on the server. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are querying from</param> /// <param name="puzzle"> /// The puzzle; if null, get all puzzles in the event. /// </param> /// <param name="team"> /// The team; if null, get all the teams in the event. /// </param> /// <param name="value">The new email only state for the puzzle</param> /// <param name="author"></param> /// <returns> /// A task that can be awaited for the lockout operation /// </returns> public static async Task SetEmailOnlyModeAsync( PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, bool value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper .GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { states[i].IsEmailOnlyMode = value; if (value == true) { states[i].WrongSubmissionCountBuffer += 50; } } await context.SaveChangesAsync(); }
/// <summary> /// Adds a user to a team after performing a number of checks to make sure that the change is valid /// </summary> /// <param name="context">The context to update</param> /// <param name="Event">The event that the team is for</param> /// <param name="EventRole">The event role of the user that is making this change</param> /// <param name="teamId">The id of the team the player should be added to</param> /// <param name="userId">The user that should be added to the team</param> /// <returns> /// A tuple where the first element is a boolean that indicates whether the player was successfully /// added to the team and the second element is a message to display that explains the error in the /// case where the user was not successfully added to the team /// </returns> public static async Task <Tuple <bool, string> > AddMemberAsync(PuzzleServerContext context, Event Event, EventRole EventRole, int teamId, int userId) { Team team = await context.Teams.FirstOrDefaultAsync(m => m.ID == teamId); if (team == null) { return(new Tuple <bool, string>(false, $"Could not find team with ID '{teamId}'. Check to make sure the team hasn't been removed.")); } var currentTeamMembers = await context.TeamMembers.Where(members => members.Team.ID == team.ID).ToListAsync(); if (currentTeamMembers.Count >= Event.MaxTeamSize && EventRole != EventRole.admin) { return(new Tuple <bool, string>(false, $"The team '{team.Name}' is full.")); } PuzzleUser user = await context.PuzzleUsers.FirstOrDefaultAsync(m => m.ID == userId); if (user == null) { return(new Tuple <bool, string>(false, $"Could not find user with ID '{userId}'. Check to make sure the user hasn't been removed.")); } if (user.EmployeeAlias == null && currentTeamMembers.Where((m) => m.Member.EmployeeAlias == null).Count() >= Event.MaxExternalsPerTeam) { return(new Tuple <bool, string>(false, $"The team '{team.Name}' is already at its maximum count of non-employee players, and '{user.Email}' has no registered alias.")); } if (await(from teamMember in context.TeamMembers where teamMember.Member == user && teamMember.Team.Event == Event select teamMember).AnyAsync()) { return(new Tuple <bool, string>(false, $"'{user.Email}' is already on a team in this event.")); } TeamMembers Member = new TeamMembers(); Member.Team = team; Member.Member = user; // Remove any applications the user might have started for this event var allApplications = from app in context.TeamApplications where app.Player == user && app.Team.Event == Event select app; context.TeamApplications.RemoveRange(allApplications); context.TeamMembers.Add(Member); await context.SaveChangesAsync(); MailHelper.Singleton.SendPlaintextWithoutBcc(new string[] { team.PrimaryContactEmail, user.Email }, $"{Event.Name}: {user.Name} has now joined {team.Name}!", $"Have a great time!"); var teamCount = await context.TeamMembers.Where(members => members.Team.ID == team.ID).CountAsync(); if (teamCount >= Event.MaxTeamSize) { var extraApplications = await(from app in context.TeamApplications where app.Team == team select app).ToListAsync(); context.TeamApplications.RemoveRange(extraApplications); var extraApplicationMails = from app in extraApplications select app.Player.Email; MailHelper.Singleton.SendPlaintextBcc(extraApplicationMails.ToList(), $"{Event.Name}: You can no longer join {team.Name} because it is now full", $"Sorry! You can apply to another team if you wish."); } return(new Tuple <bool, string>(true, "")); }
/// <summary> /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are working in</param> /// <param name="puzzleJustSolved">The puzzle just solved</param> /// <param name="team">The team that just solved; if null, all the teams in the event.</param> /// <param name="unlockTime">The time that the puzzle should be marked as unlocked.</param> /// <returns></returns> private static async Task UnlockAnyPuzzlesThatThisSolveUnlockedAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime) { // a simple query for all puzzle IDs in the event - will be used at least once below IQueryable <int> allPuzzleIDsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID); // get the prerequisites for all puzzles that need an update // information we get per puzzle: { id, min count, number of solved prereqs including this one } var prerequisiteDataForNeedsUpdatePuzzles = (from possibleUnlock in context.Prerequisites join unlockedBy in context.Prerequisites on possibleUnlock.PuzzleID equals unlockedBy.PuzzleID join pspt in context.PuzzleStatePerTeam on unlockedBy.PrerequisiteID equals pspt.PuzzleID join puz in context.Puzzles on unlockedBy.PrerequisiteID equals puz.ID where possibleUnlock.Prerequisite == puzzleJustSolved && (team == null || pspt.TeamID == team.ID) && pspt.SolvedTime != null group puz by new { unlockedBy.PuzzleID, unlockedBy.Puzzle.MinPrerequisiteCount, pspt.TeamID } into g select new { PuzzleID = g.Key.PuzzleID, TeamID = g.Key.TeamID, g.Key.MinPrerequisiteCount, TotalPrerequisiteCount = g.Sum(p => (p.PrerequisiteWeight ?? 1)) }).ToList(); // Are we updating one team or all teams? List <Team> teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List <Team>() { team }; // Update teams one at a time foreach (Team t in teamsToUpdate) { // Collect the IDs of all solved/unlocked puzzles for this team // sparse lookup is fine since if the state is missing it isn't unlocked or solved! var puzzleStateForTeamT = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, t) .Select(state => new { state.PuzzleID, state.UnlockedTime, state.SolvedTime }) .ToListAsync(); // Make a hash set out of them for easy lookup in case we have several prerequisites to chase HashSet <int> unlockedPuzzleIDsForTeamT = new HashSet <int>(); HashSet <int> solvedPuzzleIDsForTeamT = new HashSet <int>(); foreach (var puzzleState in puzzleStateForTeamT) { if (puzzleState.UnlockedTime != null) { unlockedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID); } if (puzzleState.SolvedTime != null) { solvedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID); } } // now loop through all puzzles and count up who needs to be unlocked foreach (var puzzleToUpdate in prerequisiteDataForNeedsUpdatePuzzles) { // already unlocked? skip if (unlockedPuzzleIDsForTeamT.Contains(puzzleToUpdate.PuzzleID)) { continue; } // Enough puzzles unlocked by count? Let's unlock it if (puzzleToUpdate.TeamID == t.ID && puzzleToUpdate.TotalPrerequisiteCount >= puzzleToUpdate.MinPrerequisiteCount) { PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.PuzzleID == puzzleToUpdate.PuzzleID && s.Team == t).FirstAsync(); state.UnlockedTime = unlockTime; } } } // after looping through all teams, send one update with all changes made await context.SaveChangesAsync(); }
/// <summary> /// 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. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are querying from</param> /// <param name="puzzle">The puzzle; if null, get all puzzles in the event.</param> /// <param name="team">The team; if null, get all the teams in the event.</param> /// <returns>A query of PuzzleStatePerTeam objects that can be sorted and instantiated, but you can't edit the results.</returns> public static async Task <IQueryable <PuzzleStatePerTeam> > GetFullReadWriteQueryAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team) { if (context == null) { throw new ArgumentNullException("Context required."); } if (eventObj == null) { throw new ArgumentNullException("Event required."); } if (puzzle != null && team != null) { PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.Puzzle == puzzle && s.Team == team).FirstOrDefaultAsync(); if (state == null) { context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam() { Puzzle = puzzle, Team = team }); } } else if (puzzle != null) { var teamIdsQ = context.Teams.Where(p => p.Event == eventObj).Select(p => p.ID); var puzzleStateTeamIdsQ = context.PuzzleStatePerTeam.Where(s => s.Puzzle == puzzle).Select(s => s.TeamID); var teamIdsWithoutState = await teamIdsQ.Except(puzzleStateTeamIdsQ).ToListAsync(); if (teamIdsWithoutState.Count > 0) { for (int i = 0; i < teamIdsWithoutState.Count; i++) { context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam() { Puzzle = puzzle, TeamID = teamIdsWithoutState[i] }); } } } else if (team != null) { var puzzleIdsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID); var puzzleStatePuzzleIdsQ = context.PuzzleStatePerTeam.Where(s => s.Team == team).Select(s => s.PuzzleID); var puzzleIdsWithoutState = await puzzleIdsQ.Except(puzzleStatePuzzleIdsQ).ToListAsync(); if (puzzleIdsWithoutState.Count > 0) { for (int i = 0; i < puzzleIdsWithoutState.Count; i++) { context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam() { Team = team, PuzzleID = puzzleIdsWithoutState[i] }); } } } else if (puzzle == null && team == null) { throw new NotImplementedException("Full event query is NYI and may never be needed"); } await context.SaveChangesAsync(); // query below will not return these unless we save // now this query is no longer sparse because we just filled it all out! return(GetSparseQuery(context, eventObj, puzzle, team)); }
public async Task <IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return(Page()); } using (var transaction = _context.Database.BeginTransaction()) { // // Add the event and save, so the event gets an ID. // Event.TeamRegistrationBegin = DateTime.UtcNow; Event.TeamRegistrationEnd = Event.AnswerSubmissionEnd; Event.TeamNameChangeEnd = Event.AnswerSubmissionEnd; Event.TeamMembershipChangeEnd = Event.AnswerSubmissionEnd; Event.TeamMiscDataChangeEnd = Event.AnswerSubmissionEnd; Event.TeamDeleteEnd = Event.AnswerSubmissionEnd; Event.AnswersAvailableBegin = Event.AnswerSubmissionEnd; Event.StandingsAvailableBegin = DateTime.UtcNow; Event.LockoutIncorrectGuessLimit = 5; Event.LockoutIncorrectGuessPeriod = 1; Event.LockoutDurationMultiplier = 2; Event.MaxSubmissionCount = 50; Event.MaxNumberOfTeams = 120; Event.MaxExternalsPerTeam = 9; Event.MaxTeamSize = 12; _context.Events.Add(Event); await _context.SaveChangesAsync(); // // Add start puzzle, three module puzzles, and one module meta (marked as the final event puzzle for this demo) // Puzzle start = new Puzzle { Name = "!!!Get Hopping!!!", Event = Event, IsPuzzle = false, IsGloballyVisiblePrerequisite = true, Description = "Start the event", }; _context.Puzzles.Add(start); Puzzle easy = new Puzzle { Name = "Bunny Slope", Event = Event, IsPuzzle = true, SolveValue = 10, HintCoinsForSolve = 1, Group = "Thumper's Stumpers", OrderInGroup = 1, MinPrerequisiteCount = 1, Description = "Bunsweeper", }; _context.Puzzles.Add(easy); Puzzle intermediate = new Puzzle { Name = "Rabbit Run (automatically solves in ~3 mins)", Event = Event, IsPuzzle = true, SolveValue = 10, HintCoinsForSolve = 2, Group = "Thumper's Stumpers", OrderInGroup = 2, MinPrerequisiteCount = 1, MinutesToAutomaticallySolve = 3, Description = "Rabbit's Cube", }; _context.Puzzles.Add(intermediate); Puzzle hard = new Puzzle { Name = "Hare-Raising", Event = Event, IsPuzzle = true, SolveValue = 10, HintCoinsForSolve = 3, Group = "Thumper's Stumpers", OrderInGroup = 3, MinPrerequisiteCount = 1, Description = "Lateral Leaping", }; _context.Puzzles.Add(hard); Puzzle meta = new Puzzle { Name = "Lagomorph Meta", Event = Event, IsPuzzle = true, IsMetaPuzzle = true, IsFinalPuzzle = true, SolveValue = 100, Group = "Thumper's Stumpers", OrderInGroup = 99, MinPrerequisiteCount = 2, Description = "Word Hutch", }; _context.Puzzles.Add(meta); Puzzle other = new Puzzle { Name = "Rabbit Season", Event = Event, IsPuzzle = true, SolveValue = 10, Group = "Daffy's Delights", OrderInGroup = 1, MinPrerequisiteCount = 1, Description = "Hip Hop Identification", CustomURL = "https://www.bing.com/images/search?q=%22rabbit%22", }; _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, Description = "Duck Konundrum", }; _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, Description = "Whistle Hop Intro", }; _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, Description = "Whistle Hop", }; _context.Puzzles.Add(lockPuzzle); Puzzle kitchenSyncPuzzle = new Puzzle { Name = "Kitchen Sync", Event = Event, IsPuzzle = true, SolveValue = 10, Group = "Sync Test", OrderInGroup = 1, MinPrerequisiteCount = 1 }; _context.Puzzles.Add(kitchenSyncPuzzle); Puzzle heatSyncPuzzle = new Puzzle { Name = "Heat Sync", Event = Event, IsPuzzle = true, SolveValue = 10, Group = "Sync Test", OrderInGroup = 2, MinPrerequisiteCount = 1 }; _context.Puzzles.Add(heatSyncPuzzle); Puzzle lipSyncPuzzle = new Puzzle { Name = "Lip Sync", Event = Event, IsPuzzle = true, SolveValue = 10, Group = "Sync Test", OrderInGroup = 3, MinPrerequisiteCount = 1 }; _context.Puzzles.Add(lipSyncPuzzle); Puzzle syncTestMetapuzzle = new Puzzle { Name = "Sync Test", Event = Event, IsPuzzle = true, IsMetaPuzzle = true, SolveValue = 50, Group = "Sync Test", OrderInGroup = 99, MinPrerequisiteCount = 1, MaxAnnotationKey = 400 }; _context.Puzzles.Add(syncTestMetapuzzle); await _context.SaveChangesAsync(); // // Add responses, PARTIAL is a partial, ANSWER is the answer. // _context.Responses.Add(new Response() { Puzzle = easy, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = easy, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = intermediate, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = intermediate, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = hard, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = hard, SubmittedText = "ANSWER", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = meta, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _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 }); _context.Responses.Add(new Response() { Puzzle = kitchenSyncPuzzle, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = kitchenSyncPuzzle, SubmittedText = "SYNC", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = heatSyncPuzzle, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = heatSyncPuzzle, SubmittedText = "OR", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = lipSyncPuzzle, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = lipSyncPuzzle, SubmittedText = "SWIM", ResponseText = "Correct!", IsSolution = true }); _context.Responses.Add(new Response() { Puzzle = syncTestMetapuzzle, SubmittedText = "PARTIAL", ResponseText = "Keep going..." }); _context.Responses.Add(new Response() { Puzzle = syncTestMetapuzzle, SubmittedText = "SYNCORSWIM", 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..."; string hint2Description = "Go on... George. How I get to tend the rabbits."; string hint2Content = "Well, we’ll have a big vegetable patch and a rabbit-hutch and chickens."; _context.Hints.Add(new Hint() { Puzzle = easy, Description = hint1Description, DisplayOrder = 0, Cost = 0, Content = hint1Content }); _context.Hints.Add(new Hint() { Puzzle = easy, Description = hint2Description, DisplayOrder = 1, Cost = 1, Content = hint2Content }); _context.Hints.Add(new Hint() { Puzzle = intermediate, Description = hint1Description, DisplayOrder = 0, Cost = 0, Content = hint1Content }); _context.Hints.Add(new Hint() { Puzzle = intermediate, Description = hint2Description, DisplayOrder = 1, Cost = 1, Content = hint2Content }); _context.Hints.Add(new Hint() { Puzzle = hard, Description = hint1Description, DisplayOrder = 0, Cost = 0, Content = hint1Content }); _context.Hints.Add(new Hint() { Puzzle = hard, Description = hint2Description, DisplayOrder = 1, Cost = 1, Content = hint2Content }); _context.Hints.Add(new Hint() { Puzzle = meta, Description = hint1Description, DisplayOrder = 0, Cost = 0, Content = hint1Content }); _context.Hints.Add(new Hint() { Puzzle = meta, Description = hint2Description, DisplayOrder = 1, Cost = 1, Content = hint2Content }); await _context.SaveChangesAsync(); // // Set up prequisite links. // The first two depend on start puzzle, then the third depends on one of the first two, then the meta depends on two of the first three. // _context.Prerequisites.Add(new Prerequisites() { Puzzle = easy, Prerequisite = start }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = intermediate, Prerequisite = start }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = hard, Prerequisite = easy }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = hard, Prerequisite = intermediate }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = meta, Prerequisite = easy }); _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 }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = kitchenSyncPuzzle, Prerequisite = start }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = heatSyncPuzzle, Prerequisite = start }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = lipSyncPuzzle, Prerequisite = start }); _context.Prerequisites.Add(new Prerequisites() { Puzzle = syncTestMetapuzzle, Prerequisite = start }); await _context.SaveChangesAsync(); // // Create puzzle pieces. // _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 0, 1, "xxx xxxx'x x xXxxx!", 3, "What Crocodile Dundee might say")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 1, 2, "xxx xxxx xxxx xxx Xxxxx", 1, "In <i>Hey Diddle Diddle</i>, what the dish did")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 1, 3, "xxXx", 1, "It smells")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 2, 4, "xxX", 4, "You reach things by extending it")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 2, 5, "Xxx xxxxx", 1, "Result of the <i>Exxon Valdez</i> crash")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 2, 6, "xxxxX", 2, "What you make out of turkey drippings")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 3, 7, "xxxXx xxx", 4, "Another name for a slow cooker")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 4, 8, "xXxxxx", 3, "The index is one of them")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 4, 9, "xxxxX", 2, "It can suffer when you play tennis")); _context.Pieces.Add(MakePiece(syncTestMetapuzzle, 4, 10, "xxxxxxX xxxxxxx", 2, "What Chekov asked strangers the location of in Star Trek IV, garnering suspicion due to his Russian accent")); await _context.SaveChangesAsync(); // // Create teams. Can we add players to these? // Team team1 = new Team { Name = "Team Bugs", Event = Event }; _context.Teams.Add(team1); Team team2 = new Team { Name = "Team Babs", Event = Event }; _context.Teams.Add(team2); Team team3 = new Team { Name = "Team Buster", Event = Event }; _context.Teams.Add(team3); Team teamLoneWolf = null; if (AddCreatorToLoneWolfTeam) { teamLoneWolf = new Team { Name = "Lone Wolf", Event = Event }; _context.Teams.Add(teamLoneWolf); } var demoCreatorUser = await PuzzleUser.GetPuzzleUserForCurrentUser(_context, User, _userManager); if (demoCreatorUser != null) { // // Event admin/author // _context.EventAdmins.Add(new EventAdmins() { Event = Event, Admin = demoCreatorUser }); _context.EventAuthors.Add(new EventAuthors() { Event = Event, Author = demoCreatorUser }); // // Puzzle author (for Thumper module only) // _context.PuzzleAuthors.Add(new PuzzleAuthors() { Puzzle = easy, Author = demoCreatorUser }); _context.PuzzleAuthors.Add(new PuzzleAuthors() { Puzzle = intermediate, Author = demoCreatorUser }); _context.PuzzleAuthors.Add(new PuzzleAuthors() { Puzzle = hard, Author = demoCreatorUser }); _context.PuzzleAuthors.Add(new PuzzleAuthors() { Puzzle = meta, Author = demoCreatorUser }); } // TODO: Files (need to know how to detect whether local blob storage is configured) // Is there a point to adding Feedback or is that quick/easy enough to demo by hand? await _context.SaveChangesAsync(); if (teamLoneWolf != null) { _context.TeamMembers.Add(new TeamMembers() { Team = teamLoneWolf, Member = demoCreatorUser }); } // line up all hints var teams = await _context.Teams.Where((t) => t.Event == Event).ToListAsync(); var hints = await _context.Hints.Where((h) => h.Puzzle.Event == Event).ToListAsync(); var puzzles = await _context.Puzzles.Where(p => p.Event == Event).ToListAsync(); foreach (Team team in teams) { foreach (Hint hint in hints) { _context.HintStatePerTeam.Add(new HintStatePerTeam() { Hint = hint, Team = team }); } foreach (Puzzle puzzle in puzzles) { _context.PuzzleStatePerTeam.Add(new PuzzleStatePerTeam() { PuzzleID = puzzle.ID, TeamID = team.ID }); } } await _context.SaveChangesAsync(); // // Mark the start puzzle as solved if we were asked to. // if (StartTheEvent) { await PuzzleStateHelper.SetSolveStateAsync(_context, Event, start, null, DateTime.UtcNow); } transaction.Commit(); } return(RedirectToPage("./Index")); }
/// <summary> /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite. /// </summary> /// <param name="context">The puzzle DB context</param> /// <param name="eventObj">The event we are working in</param> /// <param name="puzzleJustSolved">The puzzle just solved; if null, all the puzzles in the event (which will make more sense once we add per author filtering)</param> /// <param name="team">The team that just solved; if null, all the teams in the event.</param> /// <param name="unlockTime">The time that the puzzle should be marked as unlocked.</param> /// <returns></returns> private static async Task UnlockAnyPuzzlesThatThisSolveUnlockedAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime) { // a simple query for all puzzle IDs in the event - will be used at least once below IQueryable <int> allPuzzleIDsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID); // if we solved a group of puzzles, every puzzle needs an update. // if we solved a single puzzle, only update the puzzles that have that one as a prerequisite. IQueryable <int> needsUpdatePuzzleIDsQ = puzzleJustSolved == null ? allPuzzleIDsQ : context.Prerequisites.Where(pre => pre.Prerequisite == puzzleJustSolved).Select(pre => pre.PuzzleID).Distinct(); // get the prerequisites for all puzzles that need an update // information we get per puzzle: { id, min count, prerequisite IDs } var prerequisiteDataForNeedsUpdatePuzzles = await context.Prerequisites .Where(pre => needsUpdatePuzzleIDsQ.Contains(pre.PuzzleID)) .GroupBy(pre => pre.Puzzle) .Select(g => new { PuzzleID = g.Key.ID, g.Key.MinPrerequisiteCount, PrerequisiteIDs = g.Select(pre => pre.PrerequisiteID) }) .ToListAsync(); // Are we updating one team or all teams? List <Team> teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List <Team>() { team }; // Update teams one at a time foreach (Team t in teamsToUpdate) { // Collect the IDs of all solved/unlocked puzzles for this team // sparse lookup is fine since if the state is missing it isn't unlocked or solved! var puzzleStateForTeamT = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, t) .Select(state => new { state.PuzzleID, state.UnlockedTime, state.SolvedTime }) .ToListAsync(); // Make a hash set out of them for easy lookup in case we have several prerequisites to chase HashSet <int> unlockedPuzzleIDsForTeamT = new HashSet <int>(); HashSet <int> solvedPuzzleIDsForTeamT = new HashSet <int>(); foreach (var puzzleState in puzzleStateForTeamT) { if (puzzleState.UnlockedTime != null) { unlockedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID); } if (puzzleState.SolvedTime != null) { solvedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID); } } // now loop through all puzzles and count up who needs to be unlocked foreach (var puzzleToUpdate in prerequisiteDataForNeedsUpdatePuzzles) { // already unlocked? skip if (unlockedPuzzleIDsForTeamT.Contains(puzzleToUpdate.PuzzleID)) { continue; } // Enough puzzles unlocked by count? Let's unlock it if (puzzleToUpdate.PrerequisiteIDs.Where(id => solvedPuzzleIDsForTeamT.Contains(id)).Count() >= puzzleToUpdate.MinPrerequisiteCount) { PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.PuzzleID == puzzleToUpdate.PuzzleID && s.Team == t).FirstOrDefaultAsync(); if (state == null) { context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam() { PuzzleID = puzzleToUpdate.PuzzleID, Team = t, UnlockedTime = unlockTime }); } else { state.UnlockedTime = unlockTime; } } } } // after looping through all teams, send one update with all changes made await context.SaveChangesAsync(); }
/// <summary> /// This routine stores in the database any annotations the requester has uploaded. /// </summary> private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) { if (request.AnnotationRequests == null) { return; } foreach (var annotationRequest in request.AnnotationRequests) { // Try to generate this as a new annotation, with version 1. Annotation annotation = new Annotation(); annotation.PuzzleID = puzzleId; annotation.TeamID = teamId; annotation.Key = annotationRequest.key; annotation.Version = 1; annotation.Contents = annotationRequest.contents; annotation.Timestamp = DateTime.Now; try { context.Annotations.Add(annotation); await context.SaveChangesAsync(); } catch (DbUpdateException) { // If the insert fails, there must already be an annotation there with the // same puzzle ID, team ID, and key. So we need to update the existing one. // As we do so, we increment its version number and update its timestamp. // // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal" // Entity Framework database functions. The answer is that we need to atomically // update the Version field of the record, and Entity Framework has no way of // expressing that directly. // // The reason we want to update the version number atomically is that we rely // on the version number being a unique identifier of an annotation. We don't // want the following scenario: // // Alice tries to set the annotation for key 17 to A, and simultaneously Bob // tries to set it to B. Each reads the current version number, finds it to be // 3, and updates the annotation to have version 4. Both of these updates may // succeed, but one will overwrite the other; let's say Bob's write happens last // and "wins". So Alice may believe that version 4 is A when actually version 4 // is B. When Alice asks for the current version, she'll be told it's version 4, // and Alice will believe this means it's A. So Alice will believe that A is // what's stored in the database even though it's not. Alice and Bob's computers // will display different annotations for the same key, indefinitely. // // Note that we need a version number because the timestamp isn't guaranteed to // be unique. So in the example above Alice and Bob might wind up updating with // the same timestamp. // // You may also wonder why we use DateTime.Now instead of letting the database // assign the timestamp itself. The reason is that the database might be running // on a different machine than the puzzle server, and it might be using a different // time zone. // First, detach the annotation from the context so the context doesn't think the annotation is in the database. context.Entry(annotation).State = EntityState.Detached; try { var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key"; int result = await context.Database.ExecuteSqlCommandAsync(sqlCommand, new SqlParameter("@Contents", annotationRequest.contents), new SqlParameter("@Timestamp", DateTime.Now), new SqlParameter("@PuzzleID", puzzleId), new SqlParameter("@TeamID", teamId), new SqlParameter("@Key", annotationRequest.key)); if (result != 1) { response.AddError("Annotation update failed."); } } catch (DbUpdateException) { response.AddError("Encountered error while trying to update annotation."); } catch (Exception) { response.AddError("Miscellaneous error while trying to update annotation."); } } } }
/// <summary> /// Evaulates player submissions then either saves them to the database or returns an error to the caller /// </summary> public static async Task <SubmissionResponse> EvaluateSubmission(PuzzleServerContext context, PuzzleUser loggedInUser, Event thisEvent, int puzzleId, string submissionText, bool allowFreeformSharing) { //Query data needed to process submission Team team = await UserEventHelper.GetTeamForPlayer(context, thisEvent, loggedInUser); Puzzle puzzle = await context.Puzzles.Where( (p) => p.ID == puzzleId).FirstOrDefaultAsync(); PuzzleStatePerTeam puzzleState = await(PuzzleStateHelper .GetFullReadOnlyQuery( context, thisEvent, puzzle, team)) .FirstAsync(); List <Puzzle> puzzlesCausingGlobalLockout = await PuzzleStateHelper.PuzzlesCausingGlobalLockout(context, thisEvent, team).ToListAsync(); // Return early for cases when there's obviously nothing we can do with the submission // The submission text is empty if (String.IsNullOrWhiteSpace(submissionText)) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.EmptySubmission }); } // The puzzle is locked if (puzzle == null || puzzleState.UnlockedTime == null) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.PuzzleLocked }); } // The user or team isn't known if (loggedInUser == null || team == null) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.Unauthorized }); } // The event hasn't started yet if (DateTime.UtcNow < thisEvent.EventBegin) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.Unauthorized }); } // The team is locked out if (puzzleState.IsTeamLockedOut || puzzleState.IsEmailOnlyMode) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.TeamLockedOut }); } // The puzzle has already been solved if (puzzleState.SolvedTime != null) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.AlreadySolved }); } // The team is under a global lockout if (puzzlesCausingGlobalLockout.Count != 0 && !puzzlesCausingGlobalLockout.Contains(puzzle)) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.TeamLockedOut }); } List <SubmissionView> submissionViews = await(from sub in context.Submissions join user in context.PuzzleUsers on sub.Submitter equals user join r in context.Responses on sub.Response equals r into responses from response in responses.DefaultIfEmpty() where sub.Team == team && sub.Puzzle == puzzle orderby sub.TimeSubmitted select new SubmissionView() { Submission = sub, Response = response, SubmitterName = user.Name, FreeformReponse = sub.FreeformResponse, IsFreeform = puzzle.IsFreeform }).ToListAsync(); List <Submission> submissions = new List <Submission>(submissionViews.Count); foreach (SubmissionView submissionView in submissionViews) { submissions.Add(submissionView.Submission); } // The submission is a duplicate bool duplicateSubmission = (from sub in submissions where sub.SubmissionText == Response.FormatSubmission(submissionText) select sub).Any(); if (duplicateSubmission) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.DuplicateSubmission }); } // Create submission and add it to list Submission submission = new Submission { TimeSubmitted = DateTime.UtcNow, Puzzle = puzzleState.Puzzle, Team = puzzleState.Team, Submitter = loggedInUser, AllowFreeformSharing = allowFreeformSharing }; string submissionTextToCheck = Response.FormatSubmission(submissionText); if (puzzle.IsFreeform) { submission.UnformattedSubmissionText = submissionText; } else { submission.SubmissionText = submissionText; } submission.Response = await context.Responses.Where( r => r.Puzzle.ID == puzzleId && submissionTextToCheck == r.SubmittedText) .FirstOrDefaultAsync(); submissions.Add(submission); // Update puzzle state if submission was correct if (submission.Response != null && submission.Response.IsSolution) { await PuzzleStateHelper.SetSolveStateAsync(context, thisEvent, submission.Puzzle, submission.Team, submission.TimeSubmitted); } else if (!puzzle.IsFreeform && submission.Response == null && thisEvent.IsAnswerSubmissionActive) { // We also determine if the puzzle should be set to email-only mode. if (IsPuzzleSubmissionLimitReached( thisEvent, submissions, puzzleState)) { await PuzzleStateHelper.SetEmailOnlyModeAsync(context, thisEvent, submission.Puzzle, submission.Team, true); var authors = await context.PuzzleAuthors.Where((pa) => pa.Puzzle == submission.Puzzle).Select((pa) => pa.Author.Email).ToListAsync(); MailHelper.Singleton.SendPlaintextBcc(authors, $"{thisEvent.Name}: Team {submission.Team.Name} is in email mode for {submission.Puzzle.Name}", ""); } else { // If the submission was incorrect and not a partial solution, // we will do the lockout computations now. DateTime?lockoutExpiryTime = ComputeLockoutExpiryTime( thisEvent, submissions, puzzleState); if (lockoutExpiryTime != null) { await PuzzleStateHelper.SetLockoutExpiryTimeAsync(context, thisEvent, submission.Puzzle, submission.Team, lockoutExpiryTime); } } } context.Submissions.Add(submission); await context.SaveChangesAsync(); // Send back responses for cases where the database has been updated // Correct response if (submission.Response != null && submission.Response.IsSolution) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.Correct, CompleteResponse = submission.Response }); } // Freeform response if (puzzle.IsFreeform) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.Freeform, FreeformResponse = submission.FreeformResponse }); } // Partial response if (submission.Response != null && !submission.Response.IsSolution) { return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.Partial, CompleteResponse = submission.Response }); } // Default to incorrect return(new SubmissionResponse() { ResponseCode = SubmissionResponseCode.Incorrect }); }