diff --git a/rv_voting_calc/templates/rv_voting_calc/includes/option_list.html b/rv_voting_calc/templates/rv_voting_calc/includes/option_list.html index 49acdf69f7841078f61bcacc1e70f1097cff11ec..642694ecd7aefc1e0f46716d9be6e5ab7c7b2b87 100644 --- a/rv_voting_calc/templates/rv_voting_calc/includes/option_list.html +++ b/rv_voting_calc/templates/rv_voting_calc/includes/option_list.html @@ -2,31 +2,33 @@ <ul class="flex flex-col gap-2"> {% for option_key, option in options.items %} - <h3 class="flex gap-2 items-end text-xl font-semibold"> - {{ option_key }} - <span class="text-sm text-gray-600"> - {{ option.ticket_count }} lístků, {{ option.vote_count }} hlasů - </span> - {% if show_has_support and not option.has_support %} - <span class="text-sm text-red-600">(Nemá nadpoloviční podporu)</span> - {% endif %} - </h3> + {% if option.has_support or show_has_support %} + <h3 class="flex gap-2 items-end text-xl font-semibold"> + {{ option_key }} + <span class="text-sm text-gray-600"> + {{ option.ticket_count }} lístků, {{ option.vote_count }} hlasů + </span> + {% if show_has_support and not option.has_support %} + <span class="text-sm text-red-600">(Nemá nadpoloviční podporu)</span> + {% endif %} + </h3> - {% if option.votes|length != 0 %} - <ul class="flex gap-3 mt-1"> - {% for vote in option.votes %} - <li class="flex"> - <div class="px-4 py-2 bg-gray-300"> - {{ vote.member }} - </div> - <ul class="px-4 py-2 flex gap-1 bg-gray-200"> - {% for option in options_by_member|index:vote.member %} - <li>{{ option }}{% if not forloop.last %}, {% endif %}</li> - {% endfor %} - </ul> - </li> - {% endfor %} - </ul> + {% if option.votes|length != 0 %} + <ul class="flex gap-3 mt-1 flex-wrap"> + {% for vote in option.votes %} + <li class="flex"> + <div class="px-4 py-2 bg-gray-300"> + {{ rv_members|index:vote.member|index:"displayName" }} + </div> + <ul class="px-4 py-2 flex gap-1 bg-gray-200"> + {% for option in options_by_member|index:vote.member %} + <li>{{ option }}{% if not forloop.last %}, {% endif %}</li> + {% endfor %} + </ul> + </li> + {% endfor %} + </ul> + {% endif %} {% endif %} {% endfor %} </ul> diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/found_popularity_winner.html b/rv_voting_calc/templates/rv_voting_calc/steps/found_popularity_winner.html new file mode 100644 index 0000000000000000000000000000000000000000..09738a53692c9e0ade2e236a00f5ff4a20fa0731 --- /dev/null +++ b/rv_voting_calc/templates/rv_voting_calc/steps/found_popularity_winner.html @@ -0,0 +1,3 @@ +<li class="bg-green-200 drop-shadow-md p-4 font-semibold text-center text-xl"> + Možnost {{ option_key }} vyhrává s {{ option.vote_count }} hlasy. +</li> diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html b/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html index e93bb2ca594f605854e49a67a8e9455ac521daad..097d7606bc6f0f800cd4c3e79785fdaa9026fc47 100644 --- a/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html +++ b/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html @@ -7,5 +7,5 @@ </small> </div> - {% include "rv_voting_calc/includes/option_list.html" with options=options options_by_member=options_by_member show_has_support=True %} + {% include "rv_voting_calc/includes/option_list.html" with options=options options_by_member=options_by_member show_proposers=True show_has_support=True %} </li> diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/no_winner.html b/rv_voting_calc/templates/rv_voting_calc/steps/no_winner.html new file mode 100644 index 0000000000000000000000000000000000000000..bb9560d3854f8013c2868f1c0005164684b5fb62 --- /dev/null +++ b/rv_voting_calc/templates/rv_voting_calc/steps/no_winner.html @@ -0,0 +1,3 @@ +<li class="bg-red-200 drop-shadow-md p-4 font-semibold text-center text-xl"> + Ani jedna možnost nevyhrává. +</li> diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/removed_option.html b/rv_voting_calc/templates/rv_voting_calc/steps/removed_option.html new file mode 100644 index 0000000000000000000000000000000000000000..8bd68a2ff1d86d021dfcda961c692d00b0fe9a59 --- /dev/null +++ b/rv_voting_calc/templates/rv_voting_calc/steps/removed_option.html @@ -0,0 +1,6 @@ +<li class="bg-gray-100 drop-shadow-md p-4"> + <div class="pb-2 mb-2 border-b border-gray-300"> + <h2 class="text-2xl font-semibold font-bebas">{{ iteration|add:1 }}. vyřazovací kolo</h2> + </div> + <p>Odstraněna {% if randomly %}(losováním){% else %}nepopulární{% endif %} možnost {{ option_key }}</p> +</li> diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html b/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html index c90da9e6b049643818658b21f66f8bb6ae77bb99..f40b5dfee2e03d8f9fac30fe51fdbee34ee2fdab 100644 --- a/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html +++ b/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html @@ -3,5 +3,9 @@ <h2 class="text-2xl font-semibold font-bebas">Po vyřazení možností bez nadpoloviční podpory</h2> </div> - {% include "rv_voting_calc/includes/option_list.html" with options=options options_by_member=options_by_member %} + {% if options|length != 0 %} + {% include "rv_voting_calc/includes/option_list.html" with options=options options_by_member=options_by_member %} + {% else %} + <span class="text-gray-800 mt-1">Žádné možnosti nemají nadpoloviční podporu.</span> + {% endif %} </li> diff --git a/rv_voting_calc/views.py b/rv_voting_calc/views.py index 752108c77d7702124ecbcf4a9b3d4b243fde5ed6..9143bedf6a58d5e606fe6114fa281115cff3b06d 100644 --- a/rv_voting_calc/views.py +++ b/rv_voting_calc/views.py @@ -1,5 +1,6 @@ import json import math +import random import gql import requests @@ -76,6 +77,257 @@ def index(request): ) +def do_step_b_through_d( + request, + iteration: int, + steps: list, + options: dict, + options_by_member: dict, + options_without_support: list, + rv_members: dict, + winner_treshold: int, +) -> tuple: + ## BEGIN Step B + + #print("start", options) + + for option in options.values(): + votes_to_remove = [] + + for vote_list_position, vote in enumerate(option["votes"]): + if vote.get("was_moved", False): + continue + + for other_acceptable_option_position, other_acceptable_option in ( + enumerate(options_by_member[vote["member"]]) + ): + if not options[other_acceptable_option]["has_support"]: + continue + + if ( + other_acceptable_option_position >= vote["position"] + and option["has_support"] + ): + break + + already_voted_for_acceptable_option = False + + for other_acceptable_option_vote in options[other_acceptable_option]["votes"]: + if other_acceptable_option_vote["member"] == vote["member"]: + already_voted_for_acceptable_option = True + + if already_voted_for_acceptable_option: + votes_to_remove.append(vote_list_position) + break + + vote["was_moved"] = True + options[other_acceptable_option]["votes"].append(vote) + + break + + index_offset = 0 + + for vote_list_position in votes_to_remove: + option["votes"].pop(vote_list_position - index_offset) + + index_offset += 1 + + for option_key in options_without_support: + options.pop(option_key) + + if len(options) == 0: + steps.append( + render_to_string( + "rv_voting_calc/steps/no_winner.html", + {}, + request=request, + ) + ) + + return options, True + + ## END Step B + + ## BEGIN Step C + + total_vote_count = 0 + lowest_vote_count = math.inf + highest_vote_count = 0 + + for option in options.values(): + if option["vote_count"] < lowest_vote_count: + lowest_vote_count = option["vote_count"] + + if option["vote_count"] > highest_vote_count: + highest_vote_count = option["vote_count"] + + total_vote_count += option["vote_count"] + + # Remove was_moved for next iteration + option.pop("was_moved", None) + + options = { + key: value + for key, value + in sorted( + options.items(), + reverse=True, + key=lambda option: option[1]["vote_count"], + ) + } + + found_winner = False + winners = [] + + # Try to find the most popular option + for option_key, option in options.items(): + option["is_popularity_winner"] = (option["vote_count"] >= winner_treshold) + + if option["is_popularity_winner"]: + found_winner = True + winners.append(option_key) + + # If more than 1 winners, delete one randomly + if found_winner: + if len(winners) == 1: + steps.append( + render_to_string( + "rv_voting_calc/steps/found_popularity_winner.html", + { + "option_key": winners[0], + "option": options[winners[0]], + }, + request=request, + ) + ) + + return options, True + else: + winners_to_eliminate = random.choices(winners, k=len(winners) - 1) + + for eliminated_winner in winners_to_eliminate: + steps.append( + render_to_string( + "rv_voting_calc/steps/removed_option.html", + { + "iteration": iteration, + "option_key": eliminated_winner, + "option": options[eliminated_winner], + "randomly": True + }, + request=request, + ) + ) + + del options[eliminated_winner] + + steps.append( + render_to_string( + "rv_voting_calc/steps/found_popularity_winner.html", + { + "option_key": winners[0], + "option": options[winners[0]] + }, + request=request, + ) + ) + + return options, True + + ## END Step C + + ## BEGIN Step D + + # If no most popular option was found, remove the least popular one + most_unpopular_options = [] + + for option_key, option in options.items(): + if ( + option["vote_count"] < highest_vote_count + and option["vote_count"] == lowest_vote_count + ): + most_unpopular_options.append(option_key) + + if len(most_unpopular_options) == 1: + steps.append( + render_to_string( + "rv_voting_calc/steps/removed_option.html", + { + "iteration": iteration, + "option_key": option_key, + "option": option + }, + request=request, + ) + ) + + del options[most_unpopular_options[0]] + + if len(options) > 1: + # Continue through to next loop + return options, False + + steps.append( + render_to_string( + "rv_voting_calc/steps/found_popularity_winner.html", + { + "option_key": option_key, + "option": option + }, + request=request, + ) + ) + + return options, True + + # Remove options with least tickets + min_unpopular_option_ticket_count = math.inf + + for unpopular_option in most_unpopular_options: + if options[unpopular_option]["ticket_count"] < min_unpopular_option_ticket_count: + min_unpopular_option_ticket_count = options[unpopular_option]["ticket_count"] + + unpopular_options_to_remove = [] + + for unpopular_option in most_unpopular_options: + if options[unpopular_option]["ticket_count"] == min_unpopular_option_ticket_count: + unpopular_options_to_remove.append(unpopular_option) + + for option in unpopular_options_to_remove: + steps.append( + render_to_string( + "rv_voting_calc/steps/removed_option.html", + { + "iteration": iteration, + "option_key": option, + "option": options[option] + }, + request=request, + ) + ) + + del options[option] + + ## END Step D + + if len(options) == 0: + steps.append( + render_to_string( + "rv_voting_calc/steps/no_winner.html", + {}, + request=request, + ) + ) + + return options, True + + #print(options) + #import time + #time.sleep(1) + + return options, False + + def get_calculated_votes(request): options_by_member = request.GET.get("votes") @@ -118,7 +370,8 @@ def get_calculated_votes(request): # END Input validation - # BEGIN Sorting + ## BEGIN Step A + # Sorting steps = [] @@ -144,6 +397,7 @@ def get_calculated_votes(request): "ticket_count": 0, "vote_count": 0, "points": 0, + "is_popularity_winner": False, "votes": [], "has_support": False } @@ -160,19 +414,17 @@ def get_calculated_votes(request): option_is_ticket = False - # END Sorting - - # BEGIN Filtering out options without enough support and figuring out + # Filtering out options without enough support and figuring out # the most popular options - options_to_remove = [] + options_without_support = [] for option_key, option in options.items(): if option["vote_count"] >= has_support_treshold: option["has_support"] = True if not option["has_support"]: - options_to_remove.append(option_key) + options_without_support.append(option_key) continue option["points"] += option["ticket_count"] * (max_vote_position + 1) @@ -194,52 +446,26 @@ def get_calculated_votes(request): ) } - for option in options.values(): + first_step_options = options.copy() + + for option in first_step_options.values(): votes_to_remove = [] for vote_list_position, vote in enumerate(option["votes"]): - if vote.get("was_moved", False): - continue - - for other_acceptable_option_position, other_acceptable_option in ( - enumerate(options_by_member[vote["member"]]) - ): - if not options[other_acceptable_option]["has_support"]: - continue - - if ( - other_acceptable_option_position >= vote["position"] - and option["has_support"] - ): - break - - already_voted_for_acceptable_option = False - - for other_acceptable_option_vote in options[other_acceptable_option]["votes"]: - if other_acceptable_option_vote["member"] == vote["member"]: - already_voted_for_acceptable_option = True - - if already_voted_for_acceptable_option: - votes_to_remove.append(vote_list_position) - break - - vote["was_moved"] = True - options[other_acceptable_option]["votes"].append(vote) - - break + if vote["position"] != 0: + votes_to_remove.append(vote_list_position) index_offset = 0 for vote_list_position in votes_to_remove: option["votes"].pop(vote_list_position - index_offset) - index_offset += 1 steps.append( render_to_string( "rv_voting_calc/steps/initial_sort.html", { - "options": options, + "options": first_step_options, "options_by_member": options_by_member, "rv_members": rv_members, "total_ticket_count": total_ticket_count, @@ -249,8 +475,7 @@ def get_calculated_votes(request): ) ) - for option_key in options_to_remove: - options.pop(option_key) + del first_step_options steps.append( render_to_string( @@ -264,50 +489,24 @@ def get_calculated_votes(request): ) ) - # END Filtering out options without enough support and figuring out - # the most popular options - - return HttpResponse("\n".join(steps)) - - #acceptable_options = [] - #voting_member_count = len(options_by_member) - - #for option, members in members_by_option.items(): - #if len(members) >= math.floor(voting_member_count * 0.5): - #acceptable_options.append(option) - - #steps = [] - - #if len(acceptable_options) == 0: - #result = { - #"type": "final:no_acceptable_options", - #"voting_member_count": voting_member_count, - #"options": [], - #} + ## END Step A - #for option, members in members_by_option.items(): - #result["options"].append({ - #"name": option, - #"vote_count": len(members), - #}) + iteration = 0 - #steps.append(result) - - #return JsonResponse(steps, safe=False) - - #acceptable_options_step = { - #"type": "progress:acceptable_options", - #"voting_member_count": voting_member_count, - #"options": [] - #} - - #for option, members in members_by_option.items(): - #if option in acceptable_options: - #acceptable_options_step["options"].append({ - #"name": option, - #"vote_count": len(members), - #}) + while True: + options, ended = do_step_b_through_d( + request, + iteration, + steps, + options, + options_by_member, + options_without_support, + rv_members, + has_support_treshold, + ) - #steps.append(acceptable_options_step) + if ended: + return HttpResponse("\n".join(steps)) - # END Acceptable options + # Step E + iteration += 1