public void ModifyTask(VotePartition vote1, string newTask, VoteType type) { var votes = GetVoteEntries(type); var vote2 = vote1.ModifyTask(newTask); if (vote1 == vote2) { return; } if (!votes.TryGetValue(vote1, out var voters1)) { voters1 = new IdentitySet(); votes[vote1] = voters1; } if (!votes.TryGetValue(vote2, out var voters2)) { voters2 = new IdentitySet(); votes[vote2] = voters2; } // Save prior state to allow an undo PreserveMerge(vote1, vote2, voters1, voters2, type); // Update the votes->identity lookup voters2.UnionWith(voters1); voters1.Clear(); }
/// <summary> /// Initializes a new instance of the <see cref="Plan"/> class. /// </summary> /// <param name="planName">Name of the plan.</param> /// <param name="sourceIdentity">The identity of the originating post.</param> /// <param name="content">The content (<see cref="VotePartition"/>) of the plan.</param> /// <param name="planType">Type of the plan.</param> public Plan(string planName, Identity sourceIdentity, VotePartition content, PlanType planType) { if (string.IsNullOrEmpty(planName)) { throw new ArgumentNullException(nameof(planName)); } Identity = new Identity(planName, sourceIdentity, IdentityType.Plan); Content = content ?? throw new ArgumentNullException(nameof(content)); PlanType = planType; }
public bool Remove(VotePartition vote1, VoteType type) { var votes = GetVoteEntries(type); bool removed = false; if (votes.TryGetValue(vote1, out var voters1)) { if (voters1.Count > 0) { removed = true; PreserveRemovedVote(vote1, voters1, type); voters1.Clear(); } } return(removed); }
public UndoItem(UndoItemType undoType, VoteType voteType, VotePartition vote1 = null, VotePartition vote2 = null, Identity voter1 = null, Identity voter2 = null, IEnumerable <VotePartition> votes1 = null, IEnumerable <VotePartition> votes2 = null, IEnumerable <Identity> voters1 = null, IEnumerable <Identity> voters2 = null) { UndoType = undoType; VoteType = voteType; Vote1 = vote1; Vote2 = vote2; Votes1 = votes1?.ToList(); Votes2 = votes2?.ToList(); Voter1 = voter1; Voter2 = voter2; Voters1 = voters1?.ToList(); Voters2 = voters2?.ToList(); }
public bool Matches(VotePartition other) { return(VoteLines.Equals(other.VoteLines)); }
/// <summary> /// Take the initial value of a post, and substitute plans and /// proxy vote lines as appropriate. /// </summary> /// <param name="post">The original post.</param> /// <returns>Returns a list of VoteLines to use for the purpose of processing the vote.</returns> public static List <VotePartition> GetVotePartitions(Post post, IQuest quest) { if (post == null) { throw new ArgumentNullException(nameof(post)); } if (quest == null) { throw new ArgumentNullException(nameof(quest)); } List <VotePartition> results = new List <VotePartition>(); VotePartition partition = null; VoteLine parent = null; // Move through the vote lines by index so we can modify the // index pointer directly when doing comparisons. var voteLineArray = post.Vote.VoteLines.ToArray(); int index = 0; while (index < voteLineArray.Length) { // Current line. VoteLine line = voteLineArray[index]; // First check if the line is a proxy vote for a plan or a user Match m = startsWithPlan.Match(line.ComparableContent); // "Plan XYZ" if (m.Success) { string planname = m.Groups["planname"].Value; // Plan has priority, in case the plan is named after the user. if (VotingRecords.Instance.HasPlanName(planname)) { // Get the collection of variants of this plan name. var plans = VotingRecords.Instance.GetPlans(planname); Plan plan = null; // Make sure we don't go over the limit of the remaining post vote lines. int remainingLines = voteLineArray.Length - index - 1; if (remainingLines == 0) { // If there are no remaining lines in the current post, we // just take the provided variant 0. plan = plans.First(p => p.Identity.Number == 0); } else { // OK, there are lines that may potentially be copied from the plan. // Limit the plans we're looking at to those that can fit in our remaining line space. var limitedPlans = plans.Where(p => p.Content.VoteLines.Count <= remainingLines).ToList(); if (limitedPlans.Any()) { foreach (var p in limitedPlans) { // In each of the plans that can fit in the remaining vote space, // compare them with an equal-sized segment of the vote. // If it matches, that's a copy of the plan, and we can skip past it. var segment = new ArraySegment <VoteLine>(voteLineArray, index, p.Content.VoteLines.Count); if (segment.SequenceEqual(p.Content.VoteLines)) { plan = p; break; } } // However if no sequence match was found, we only have the naming line to // copy, and we can move on to the next line of the post. if (plan == null) { plan = limitedPlans.First(p => p.Identity.Number == 0); } else { // If we did find the full match, update the index so that we increment past that. index += plan.Content.VoteLines.Count - 1; } } else { // None of the selected plans can fit in the remaining lines of the post. // Thus we don't need to try to match, and just take the named line. plan = plans.First(p => p.Identity.Number == 0); } } // If we found a plan, add its contents to the results and go to move next. // Otherwise, drop out and treat this as a normal line. if (plan != null) { List <VotePartition> planPartitions = VotingRecords.Instance.GetPartitionsForIdentity(plan.Identity); results.AddRange(planPartitions); goto moveNext; } } // Otherwise check if it's a user proxy. Usernames are unlikely to be allowed to be greater // than 20 characters, but giving a fair buffer just in case. Don't check for very long strings // as usernames. Also check global options in case proxy votes are disabled. else if (planname.Length <= 40 && !quest.DisableProxyVotes) { List <VotePartition> proxyPartitions = GetProxyPartitions(planname, false, post.Identity); // There are no matching identities. Treat as a normal line. if (proxyPartitions == null) { goto normalLine; } // If there are no recorded partitions for this identity, it hasn't been // processed yet, and this is a future reference, or a past vote that itself // has future references and hasn't completed processing yet. if (!proxyPartitions.Any()) { // If we aren't forcing the processing of this post, just bail and return null. // If we *are* forcing it, we have to treat this as a normal line. if (post.ForceProcess) { goto normalLine; } else { VotingRecords.Instance.NoteFutureReference(post); return(null); } } // Add whatever proxy partitions we got to he results and move on. results.AddRange(proxyPartitions); goto moveNext; } } // If it doesn't start with 'plan', also check if it's a (possibly pinned) username proxy. // Usernames are unlikely to be allowed to be greater than 20 characters, but giving // a fair buffer just in case. Don't check for very long strings as usernames. // Also check global options in case proxy votes are disabled. else { // Proxy regex might start with ^ to indicate a pinned name, and have a // username length of no more than 40 characters. m = proxyRegex.Match(line.ComparableContent); if (m.Success && !quest.DisableProxyVotes) { bool pin = m.Groups["pin"].Success; string username = m.Groups["username"].Value; List <VotePartition> proxyPartitions = GetProxyPartitions(username, pin, post.Identity); // There are no matching identities. Treat as a normal line. if (proxyPartitions == null) { goto normalLine; } // If there are no recorded partitions for this identity, it hasn't been // processed yet, and this is a future reference, or a past vote that itself // has future references and hasn't completed processing yet. if (!proxyPartitions.Any()) { // If we aren't forcing the processing of this post, just bail and return null. // If we *are* forcing it, we have to treat this as a normal line. if (post.ForceProcess) { goto normalLine; } else { VotingRecords.Instance.NoteFutureReference(post); return(null); } } // Add whatever proxy partitions we got to he results and move on. results.AddRange(proxyPartitions); goto moveNext; } // If we have a plan name that doesn't have the 'plan' prefix, redo the logic // of skipping past any copied content. else if (VotingRecords.Instance.HasPlanName(line.ComparableContent)) { string planname = line.ComparableContent; // Get the collection of variants of this plan name. var plans = VotingRecords.Instance.GetPlans(planname); Plan plan = null; // Make sure we don't go over the limit of the remaining post vote lines. int remainingLines = voteLineArray.Length - index - 1; if (remainingLines == 0) { // If there are no remaining lines in the current post, we // just take the provided variant 0. plan = plans.First(p => p.Identity.Number == 0); } else { // OK, there are lines that may potentially be copied from the plan. // Limit the plans we're looking at to those that can fit in our remaining line space. var limitedPlans = plans.Where(p => p.Content.VoteLines.Count <= remainingLines).ToList(); if (limitedPlans.Any()) { foreach (var p in limitedPlans) { // In each of the plans that can fit in the remaining vote space, // compare them with an equal-sized segment of the vote. // If it matches, that's a copy of the plan, and we can skip past it. var segment = new ArraySegment <VoteLine>(voteLineArray, index, p.Content.VoteLines.Count); if (segment.SequenceEqual(p.Content.VoteLines)) { plan = p; break; } } // However if no sequence match was found, we only have the naming line to // copy, and we can move on to the next line of the post. if (plan == null) { plan = limitedPlans.First(p => p.Identity.Number == 0); } else { // If we did find the full match, update the index so that we increment past that. index += plan.Content.VoteLines.Count - 1; } } else { // None of the selected plans can fit in the remaining lines of the post. // Thus we don't need to try to match, and just take the named line. plan = plans.First(p => p.Identity.Number == 0); } } // If we found a plan, add its contents to the results and go to move next. // Otherwise, drop out and treat this as a normal line. if (plan != null) { List <VotePartition> planPartitions = VotingRecords.Instance.GetPartitionsForIdentity(plan.Identity); results.AddRange(planPartitions); goto moveNext; } } } // Not a plan proxy, and not a user proxy. Handle normal partitioning. normalLine: if (partition == null) { partition = new VotePartition(); } if (parent == null) { parent = line; } switch (quest.PartitionMode) { case PartitionMode.None: partition.AddLine(line); break; case PartitionMode.ByLine: if (partition.VoteLines.Count > 0) { results.Add(partition); } partition = new VotePartition(line, VoteType.Vote); break; case PartitionMode.ByLineTask: if (partition.VoteLines.Count > 0) { results.Add(partition); } if (line.Prefix.Length == 0) { parent = line; partition = new VotePartition(line, VoteType.Vote); } else if (parent.Task != string.Empty && line.Task == string.Empty) { var mLine = line.Modify(task: parent.Task); partition = new VotePartition(mLine, VoteType.Vote); } else { partition = new VotePartition(line, VoteType.Vote); } break; case PartitionMode.ByBlock: case PartitionMode.ByBlockAll: if (line.Prefix.Length == 0) { if (partition.VoteLines.Count > 0) { results.Add(partition); } partition = new VotePartition(line, VoteType.Vote); } else { partition.AddLine(line); } break; default: break; } // Exit point for next enumerator line. moveNext: index++; } if (partition != null && partition.VoteLines.Count > 0) { results.Add(partition); } return(results); }
/// <summary> /// Initializes a new instance of the <see cref="Plan" /> class. /// </summary> /// <param name="identity">The identity object for the plan.</param> /// <param name="content">The content (<see cref="VotePartition"/>) of the plan.</param> /// <param name="planType">Type of the plan.</param> public Plan(Identity identity, VotePartition content, PlanType planType) { Identity = identity ?? throw new ArgumentNullException(nameof(identity)); Content = content ?? throw new ArgumentNullException(nameof(content)); PlanType = planType; }
/// <summary> /// Gets the plans from a vote. /// </summary> /// <param name="vote">The vote.</param> /// <returns>Returns a list of all plans found within a vote.</returns> public static List <Plan> GetPlansFromVote(Vote vote) { if (vote == null) { throw new ArgumentNullException(nameof(vote)); } if (!vote.IsValid) { throw new InvalidOperationException("This is not a valid vote."); } List <Plan> plans = new List <Plan>(); var voteGrouping = vote.GetVoteMarkerGroups(); bool checkForBasePlans = true; // Base plans while (checkForBasePlans && voteGrouping.Any()) { var voteGroup = voteGrouping.First(); Match m = anyPlanRegex.Match(voteGroup.Key); if (m.Groups["base"].Success && voteGroup.First().MarkerType == MarkerType.Vote && voteGroup.Count() > 1) { var partition = new VotePartition(voteGroup, VoteType.Plan); plans.Add(new Plan(m.Groups["planname"].Value, vote.Post.Identity, partition, PlanType.Base)); voteGrouping = voteGrouping.Skip(1); } else { checkForBasePlans = false; } } // Vote labels if (voteGrouping.Any()) { var voteGroup = voteGrouping.First(); Match m = anyPlanRegex.Match(voteGroup.Key); if (m.Success && m.Groups["base"].Success == false && voteGroup.Count() == 1 && voteGroup.First().MarkerType == MarkerType.Vote) { var labeledGroups = voteGrouping.TakeWhile(g => g.First().MarkerType == MarkerType.Vote || g.First().MarkerType == MarkerType.Approval); var flattenedLines = labeledGroups.SelectMany(a => a).ToList(); var partition = new VotePartition(flattenedLines, VoteType.Plan); PlanType planType = labeledGroups.Skip(1).Any() ? PlanType.Label : PlanType.SingleLine; voteGrouping = voteGrouping.Skip(labeledGroups.Count()); plans.Add(new Plan(m.Groups["planname"].Value, vote.Post.Identity, partition, planType)); } } // Any other defined plans with content foreach (var voteGroup in voteGrouping) { Match m = anyPlanRegex.Match(voteGroup.Key); if (m.Success && voteGroup.Skip(1).Any()) { var partition = new VotePartition(voteGroup, VoteType.Plan); plans.Add(new Plan(m.Groups["planname"].Value, vote.Post.Identity, partition, PlanType.Content)); } } vote.StorePlans(plans); // Return all collected plans return(plans); }
private void PreserveRemovedVote(VotePartition vote1, IEnumerable <Identity> vote1Voters, VoteType type) { var undoItem = new UndoItem(UndoItemType.RemoveVote, type, vote1: vote1, voters1: vote1Voters); UndoBuffer.Push(undoItem); }
/// <summary> /// Preserves the specified state prior to a vote merge, to allow undoing it. /// </summary> /// <param name="vote1">The vote1.</param> /// <param name="vote2">The vote2.</param> /// <param name="voters1">The voters1.</param> /// <param name="voters2">The voters2.</param> /// <param name="type">The type.</param> private void PreserveMerge(VotePartition vote1, VotePartition vote2, IEnumerable <Identity> voters1, IEnumerable <Identity> voters2, VoteType type) { var undoItem = new UndoItem(UndoItemType.Merge, type, vote1: vote1, vote2: vote2, voters1: voters1, voters2: voters2); UndoBuffer.Push(undoItem); }