public void Poll_MultipleChoiceResultsAreRight() { var poll = new MeetingPoll() { ClosedAt = DateTime.UtcNow, ParsedData = new PollData() { Choices = new Dictionary <int, PollData.PollChoice>() { { 1, new PollData.PollChoice(1, "Name1") }, { 2, new PollData.PollChoice(2, "Name2") }, { 3, new PollData.PollChoice(3, "Name3") }, }, MultipleChoiceOption = new PollData.MultipleChoice(), }, }; var votes = new List <MeetingPollVote>() { new() { IsTiebreaker = true, VotingPower = 2, ParsedVoteContent = new PollVoteData() { SelectedOptions = new List <int>() { 1, 2 }, }, }, new() { ParsedVoteContent = new PollVoteData() { SelectedOptions = new List <int>() { 2 }, }, }, new() { ParsedVoteContent = new PollVoteData() { SelectedOptions = new List <int>() { 3, 2, 1 }, }, }, new() { VotingPower = 2, ParsedVoteContent = new PollVoteData() { SelectedOptions = new List <int>() { 3 }, }, }, }; poll.CalculateResults(votes); Assert.NotNull(poll.PollResults); var resultData = poll.ParsedResults; Assert.NotNull(resultData); Assert.NotNull(resultData.Results); Assert.Null(resultData.TiebreakInFavourOf); Assert.Equal(10, resultData.TotalVotes); Assert.Equal(3, resultData.Results.Count); // Results check Assert.Equal(2, resultData.Results[0].Item1); Assert.Equal(4, resultData.Results[0].Item2); Assert.Equal(1, resultData.Results[1].Item1); Assert.Equal(3, resultData.Results[1].Item2); Assert.Equal(3, resultData.Results[2].Item1); Assert.Equal(3, resultData.Results[2].Item2); }
public async Task <ActionResult <List <MeetingPollDTO> > > VoteInPoll([Required] long id, [Required] long pollId, [Required][FromBody] PollVoteData request) { var errors = new List <ValidationResult>(); if (!Validator.TryValidateObject(request, new ValidationContext(request), errors)) { logger.LogError("Poll vote data didn't pass validation:"); foreach (var error in errors) { logger.LogError("Failure: {Error}", error); } // TODO: send errors to client? return(BadRequest("Invalid vote JSON data")); } if (request.SelectedOptions.GroupBy(i => i).Any(g => g.Count() > 1)) { return(BadRequest("Can't vote multiple times for the same option")); } var access = GetCurrentUserAccess(); var meeting = await GetMeetingWithReadAccess(id, access); if (meeting == null) { return(NotFound()); } var user = HttpContext.AuthenticatedUser() !; var member = await GetMeetingMember(meeting.Id, user.Id); if (member == null) { return(this.WorkingForbid("You need to join a meeting before voting in it")); } var poll = await database.MeetingPolls.FindAsync(id, pollId); if (poll == null) { return(NotFound()); } if (poll.ClosedAt != null) { return(BadRequest("The poll is closed")); } var parsedData = poll.ParsedData; if (parsedData.WeightedChoices != null) { if (request.SelectedOptions.Count < 1 && !parsedData.WeightedChoices.CanSelectNone) { return(BadRequest("You need to select at least one option")); } } else if (parsedData.MultipleChoiceOption != null) { var min = parsedData.MultipleChoiceOption.MinimumSelections; var max = parsedData.MultipleChoiceOption.MaximumSelections; if (request.SelectedOptions.Count < min || request.SelectedOptions.Count > max) { return(BadRequest($"This poll requires you to select between {min} to {max} options")); } } else if (parsedData.SingleChoiceOption != null) { if (request.SelectedOptions.Count > 1) { return(BadRequest("This poll allows only a single option to be selected")); } if (request.SelectedOptions.Count < 1 && parsedData.SingleChoiceOption.CanSelectNone != true) { return(BadRequest("You must select at least one option")); } } long?president = null; switch (poll.TiebreakType) { case VotingTiebreakType.President: // Fetch this data here before checking if the vote is a duplicate president = await GetAssociationPresidentUserId(); break; } // Fail if already voted if (await GetPollVotingRecord(meeting.Id, poll.PollId, user.Id) != null) { return(BadRequest("You have already voted in this poll")); } // Voting power is doubled if person is or has been a board member (as defined in the association rules) float votingPower = 1; if (user.AssociationMember?.HasBeenBoardMember == true) { votingPower = 2; } // These should execute within a single transaction so only one of these can get through var votingRecord = new MeetingPollVotingRecord() { MeetingId = meeting.Id, PollId = poll.PollId, UserId = user.Id, }; var vote = new MeetingPollVote() { MeetingId = meeting.Id, PollId = poll.PollId, VotingPower = votingPower, ParsedVoteContent = request, }; switch (poll.TiebreakType) { case VotingTiebreakType.Random: // Don't store tiebreak data when it is not needed break; case VotingTiebreakType.Chairman: vote.IsTiebreaker = user.Id == meeting.ChairmanId; break; case VotingTiebreakType.President: // Association's current president is the tiebreaker vote.IsTiebreaker = user.Id == president; break; default: throw new ArgumentOutOfRangeException(); } await database.MeetingPollVotingRecords.AddAsync(votingRecord); await database.MeetingPollVotes.AddAsync(vote); await database.SaveChangesAsync(); logger.LogInformation("User {Email} has voted in poll {Id} at {UtcNow}", user.Email, poll.PollId, DateTime.UtcNow); return(Ok()); }