diff --git a/package.json b/package.json index 509de41c2abe10c1ad71fcb4dfc239894f13de21..4a07c0ecbe08e9ffec6d3ceb929877ce237b3726 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "css-loader": "^6.7.3", "jquery": "^3.6.3", + "js-cookie": "^3.0.1", "select2": "^4.1.0-rc.0", "style-loader": "^3.3.1", "tailwindcss": "^3.2.4", diff --git a/requirements/base.txt b/requirements/base.txt index 9efe2998543d7d21e6b74c9e33931f53e22db887..fe6b002554d53683818bd6e20d728659bead4bb7 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,8 +1,9 @@ Django==4.1.5 django-database-url==1.0.3 django-environ==0.9.0 -psycopg2-binary==2.9.5 django-webpack-loader==1.8.0 -nodeenv==1.7.0 +django-http-exceptions==1.4.0 gql[requests]==3.4.0 +nodeenv==1.7.0 +psycopg2-binary==2.9.5 requests==2.28.2 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 642694ecd7aefc1e0f46716d9be6e5ab7c7b2b87..1e77bca301e571b13d271aa0f3572d5381b7a8ff 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,32 +2,36 @@ <ul class="flex flex-col gap-2"> {% for option_key, option in options.items %} - {% 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 not option.marked_for_deletion or show_marked_for_deletion %} + {% if option.has_support or show_has_support %} + <h3 class="flex gap-2 items-end text-xl font-semibold {% if option.marked_for_deletion or not option.has_support %}text-red-600{% endif %}"> + {{ option_key }} + <span class="text-sm {% if option.marked_for_deletion or not option.has_support %}text-red-600{% else %}text-gray-600{% endif %}"> + {{ 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 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> + {% if option.ticket_votes|length != 0 %} + <ul class="flex gap-3 mt-1 flex-wrap"> + {% for vote in option.ticket_votes %} + {% if not vote.hidden %} + <li class="flex"> + <div class="px-4 py-2 {% if not option.marked_for_deletion and option.has_support %}bg-gray-300{% else %}bg-red-300{% endif %}"> + {{ rv_members|index:vote.member|index:"displayName" }} + </div> + <ul class="px-4 py-2 flex gap-1 {% if not option.marked_for_deletion and option.has_support %}bg-gray-200{% else %}bg-red-200{% endif %}"> + {% for option in options_by_member|index:vote.member %} + <li>{{ option }}{% if not forloop.last %}, {% endif %}</li> + {% endfor %} + </ul> + </li> + {% endif %} + {% endfor %} + </ul> + {% endif %} {% endif %} {% endif %} {% endfor %} diff --git a/rv_voting_calc/templates/rv_voting_calc/index.html b/rv_voting_calc/templates/rv_voting_calc/index.html index 7ae060f41213f3b1a02e9e7d7b61843a91d337e3..effaff14d1819a90e24e6d8483f3959476cd719e 100644 --- a/rv_voting_calc/templates/rv_voting_calc/index.html +++ b/rv_voting_calc/templates/rv_voting_calc/index.html @@ -43,12 +43,21 @@ {% endfor %} </ul> - <ul - class="flex flex-col gap-5" - id="result" - > - - </ul> + <div> + <ul + class="flex flex-col gap-5" + id="result" + > + + </ul> + <div class="mt-3"> + <a + class="hidden underline" + id="permalink" + href="" + >Permalink</a> + </div> + </div> </div> </main> @@ -56,5 +65,21 @@ <script> const VOTE_CALCULATION_ENDPOINT = "{% url "rv_voting_calc:get_calculated_votes" %}" + + {% if options_by_member %} + {% for member, selected_options in options_by_member.items %} + {% for selected_option in selected_options %} + $(`#${$.escapeSelector("{{ member}}")}-selection`).append( + new Option( + "{{ selected_option }}", + "{{ selected_option }}", + true, + true, + ) + ); + {% endfor %} + $(`#${$.escapeSelector("{{ member}}")}-selection`).trigger("change"); + {% endfor %} + {% endif %} </script> {% endblock %} 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 index 8bd68a2ff1d86d021dfcda961c692d00b0fe9a59..062e07c61a4e3f49d8e3c68e80fe29b8b8d97acb 100644 --- a/rv_voting_calc/templates/rv_voting_calc/steps/removed_option.html +++ b/rv_voting_calc/templates/rv_voting_calc/steps/removed_option.html @@ -1,6 +1,11 @@ +{% load index %} + <li class="bg-gray-100 drop-shadow-md p-4"> - <div class="pb-2 mb-2 border-b border-gray-300"> + <div class="pb-2 mb-3 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> + <p class="mb-2 text-red-600 font-semibold"> + Možnost {{ option_key }} bude {% if randomly %}losováním{% else %}kvůli nepopularitě{% endif %} odstraněna + </p> + {% include "rv_voting_calc/includes/option_list.html" with options=options options_by_member=options_by_member show_proposers=True show_marked_for_deletion=True %} </li> diff --git a/rv_voting_calc/views.py b/rv_voting_calc/views.py index 9143bedf6a58d5e606fe6114fa281115cff3b06d..ddb24299826766842b80bfb43ea0cff95805ed86 100644 --- a/rv_voting_calc/views.py +++ b/rv_voting_calc/views.py @@ -1,30 +1,104 @@ +import base64 +import binascii import json import math import random +import secrets import gql import requests +from gql.transport.exceptions import TransportQueryError from gql.transport.requests import RequestsHTTPTransport from django.conf import settings -from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import render from django.template.loader import render_to_string +from django_http_exceptions import HTTPExceptions -def get_rv_members() -> list: +def get_options_by_member(source, rv_members) -> dict: + try: + options_by_member = json.loads(source) + + if not isinstance(options_by_member, dict): + raise ValueError + except ValueError: + raise HTTPExceptions.BAD_REQUEST + + rv_members = convert_rv_members_to_dict(rv_members) + + for member, options in options_by_member.items(): + # `member` is not a string + if not isinstance(member, str): + raise HTTPExceptions.BAD_REQUEST + + # `member` is not in the list of known members + if member not in options_by_member: + raise HTTPExceptions.BAD_REQUEST + + # `options` is not a list of 1+ length + if not isinstance(options, list) and len(options) != 0: + raise HTTPExceptions.BAD_REQUEST + + # all items in `options` are strings + for item in options: + if not isinstance(item, str): + raise HTTPExceptions.BAD_REQUEST + + return options_by_member + + +def index(request): + rv_members = get_rv_members(request) + keyed_rv_members = convert_rv_members_to_dict(rv_members) + + # Get votes from a possible permalink + options_by_member = request.GET.get("votes") + + # BEGIN Input validation + if options_by_member is not None: + options_by_member = get_options_by_member( + options_by_member, + rv_members=rv_members, + ) + + return render( + request, + "rv_voting_calc/index.html", + { + "rv_members": rv_members, + "keyed_rv_members": keyed_rv_members, + "options_by_member": options_by_member, + } + ) + + +def get_rv_members(request, get_rv_gid: bool = False): transport = RequestsHTTPTransport(url=settings.CHOBOTNICE_API_URL) client = gql.Client( transport=transport, fetch_schema_from_transport=True, ) + rv_gid = None + + if "rv_gid" in request.GET: + try: + base64.b64decode(request.GET["rv_gid"], validate=True) + except binascii.Error: + raise HTTPExceptions.BAD_REQUEST + + rv_gid = request.GET["rv_gid"] + else: + rv_gid = settings.CHOBOTNICE_RV_GID + # Get members from query query = gql.gql( f""" {{ - group(id: "{settings.CHOBOTNICE_RV_GID}") {{ + group(id: "{rv_gid}") {{ memberships {{ person {{ username @@ -37,7 +111,12 @@ def get_rv_members() -> list: """ ) - result = client.execute(query) + try: + result = client.execute(query) + except TransportQueryError: + # rv_gid was not found + raise HTTPExceptions.BAD_REQUEST + rv_members = [] # Convert to a nicer format @@ -47,7 +126,10 @@ def get_rv_members() -> list: # Sort alphabetically rv_members.sort(key=lambda value: value["displayName"]) - return rv_members + if get_rv_gid: + return rv_members, rv_gid + else: + return rv_members def convert_rv_members_to_dict(source: list) -> dict: @@ -63,78 +145,80 @@ def convert_rv_members_to_dict(source: list) -> dict: return rv_members -def index(request): - rv_members = get_rv_members() - keyed_rv_members = convert_rv_members_to_dict(rv_members) - - return render( - request, - "rv_voting_calc/index.html", - { - "rv_members": rv_members, - "keyed_rv_members": keyed_rv_members, - } - ) - - def do_step_b_through_d( request, iteration: int, steps: list, options: dict, - options_by_member: dict, options_without_support: list, + options_by_member: dict, rv_members: dict, - winner_treshold: int, ) -> tuple: ## BEGIN Step B - #print("start", options) + options_marked_for_deletion = [] - for option in options.values(): + # Move ticket votes to the next most popular option, if necessary + for option_key, option in options.items(): votes_to_remove = [] - for vote_list_position, vote in enumerate(option["votes"]): - if vote.get("was_moved", False): - continue + if option["marked_for_deletion"]: + options_marked_for_deletion.append(option_key) - for other_acceptable_option_position, other_acceptable_option in ( - enumerate(options_by_member[vote["member"]]) - ): - if not options[other_acceptable_option]["has_support"]: + # If the option has support and isn't marked for deletion, we don't need + # to do anything. + if option["has_support"] and not option["marked_for_deletion"]: + continue + + # For all of the option's votes... + for vote_list_position, vote in enumerate(option["ticket_votes"]): + # Find the other acceptable options listed within... + for other_acceptable_option in vote["other_acceptable"]: + # ... if the other acceptable option no longer exists, do nothing. + if other_acceptable_option not in options: continue + # ... if the other acceptable option has support and isn't marked + # for deletion, move this ticket vote there. We don't need to check + # positions, since the list we are looping through is ordered + # by popularity. if ( - other_acceptable_option_position >= vote["position"] - and option["has_support"] + options[other_acceptable_option]["has_support"] + and not options[other_acceptable_option]["marked_for_deletion"] ): - 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) + options[other_acceptable_option]["ticket_votes"].append(vote) + options[other_acceptable_option]["ticket_count"] += 1 break - vote["was_moved"] = True - options[other_acceptable_option]["votes"].append(vote) - - break - + # Remove votes that have been moved from this option. index_offset = 0 for vote_list_position in votes_to_remove: - option["votes"].pop(vote_list_position - index_offset) + option["ticket_votes"].pop(vote_list_position - index_offset) index_offset += 1 - for option_key in options_without_support: - options.pop(option_key) + # If we are on the first iteration, show the list of options with those that + # don't have support removed. + if iteration == 0: + steps.append( + render_to_string( + "rv_voting_calc/steps/with_support.html", + { + "options": options, + "options_by_member": options_by_member, + "rv_members": rv_members + }, + request=request, + ) + ) + # Delete options without support and those marked for deletion. + for option_key in (options_without_support + options_marked_for_deletion): + options.pop(option_key, None) + + # If no options remain, show that there are no winners and end everything. if len(options) == 0: steps.append( render_to_string( @@ -150,22 +234,7 @@ def do_step_b_through_d( ## 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) - + # Sort options by their vote count. options = { key: value for key, value @@ -176,140 +245,136 @@ def do_step_b_through_d( ) } - found_winner = False - winners = [] + # Find winners. + winner_vote_count = options[next(iter(options))]["vote_count"] + winner_count = 0 + winning_option_keys = [] - # 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, - ) - ) + if option["vote_count"] == winner_vote_count: + winning_option_keys.append(option_key) + winner_count += 1 - 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, - ) + # If there is exactly 1 winner, show the winner and end everything. + if winner_count == 1: + steps.append( + render_to_string( + "rv_voting_calc/steps/found_popularity_winner.html", + { + "option_key": winning_option_keys[0], + "option": options[winning_option_keys[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) + return options, True + # If all options are winners, randomly eliminate one and continue + # to the next iteration. + elif winner_count == len(options): + eliminated_winner = random.choice(winning_option_keys) + + options[eliminated_winner]["marked_for_deletion"] = True - 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 + "option_key": eliminated_winner, + "option": options[eliminated_winner], + "options": options, + "options_by_member": options_by_member, + "rv_members": rv_members, + "randomly": True }, request=request, ) ) - del options[most_unpopular_options[0]] + return options, False - if len(options) > 1: - # Continue through to next loop - return options, False + ## END Step C + + ## BEGIN Step D + + # Sort options by their vote count, with lowest ranking ones first. + options = { + key: value + for key, value + in sorted( + options.items(), + key=lambda option: option[1]["vote_count"], + ) + } + + # Find losing options. + loser_vote_count = options[next(iter(options))]["vote_count"] + loser_count = 0 + losing_option_keys = [] + + for option_key, option in options.items(): + if option["vote_count"] == loser_vote_count: + losing_option_keys.append(option_key) + loser_count += 1 + + # If there is exactly one losing option, delete it and move on to the next + # iteration. + if loser_count == 1: + loser_key = next(iter(options)) + + options[loser_key]["marked_for_deletion"] = True steps.append( render_to_string( - "rv_voting_calc/steps/found_popularity_winner.html", + "rv_voting_calc/steps/removed_option.html", { - "option_key": option_key, - "option": option + "iteration": iteration, + "option_key": loser_key, + "option": options[loser_key], + "options": options, + "options_by_member": options_by_member, + "rv_members": rv_members, }, 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 = [] + return options, False + else: + # If there are multiple losing options, order them by the amount of + # ticket votes they have. + losing_option_keys.sort( + key=lambda key: options[key]["ticket_count"] + ) - 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) + options[losing_option_keys[0]]["marked_for_deletion"] = True - for option in unpopular_options_to_remove: + # Delete the vote with the least ticket votes. There maybe more, but + # those should be taken care of in the next iteration. steps.append( render_to_string( "rv_voting_calc/steps/removed_option.html", { "iteration": iteration, - "option_key": option, - "option": options[option] + "option_key": losing_option_keys[0], + "option": options[losing_option_keys[0]], + "options": options, + "options_by_member": options_by_member, + "rv_members": rv_members, }, request=request, ) ) - del options[option] + # End this iteration and move to the next one. + return options, False ## END Step D + # (TODO: Will this function ever actually reach the code below?) + + # If there are no winners, show that and end everything. if len(options) == 0: steps.append( render_to_string( @@ -321,10 +386,7 @@ def do_step_b_through_d( return options, True - #print(options) - #import time - #time.sleep(1) - + # Continue on to the next iteration. return options, False @@ -334,39 +396,14 @@ def get_calculated_votes(request): # BEGIN Input validation if options_by_member is None: - return HttpResponseBadRequest("Nebyly zadány žádné hlasy") - - try: - options_by_member = json.loads(options_by_member) + raise HTTPExceptions.BAD_REQUEST - if not isinstance(options_by_member, dict): - raise ValueError - except ValueError: - return HttpResponseBadRequest("Formát hlasů není validní") - - rv_members = convert_rv_members_to_dict(get_rv_members()) - - integrity_check_failed = False - - for member, options in options_by_member.items(): - # `member` is not in the list of known members - if member not in options_by_member: - integrity_check_failed = True - break - - # `options` is a list - if not isinstance(options, list): - integrity_check_failed = True - break - - # all items in `options` are strings - for item in options: - if not isinstance(item, str): - integrity_check_failed = True - break - - if integrity_check_failed: - return HttpResponseBadRequest("Formát hlasů není validní") + rv_members, rv_gid = get_rv_members(request, get_rv_gid=True) + options_by_member = get_options_by_member( + options_by_member, + rv_members=rv_members, + ) + rv_members = convert_rv_members_to_dict(rv_members) # END Input validation @@ -383,40 +420,48 @@ def get_calculated_votes(request): if total_ticket_count % 2 == 0: has_support_treshold += 1 - max_vote_position = 0 - + # For each member's selected options... for member, selected_options in options_by_member.items(): - option_is_ticket = True - for position, option in enumerate(selected_options): - if position > max_vote_position: - max_vote_position = position - + # Add the option to the options list if it isn't there already. if option not in options: options[option] = { "ticket_count": 0, "vote_count": 0, - "points": 0, - "is_popularity_winner": False, - "votes": [], - "has_support": False + "marked_for_deletion": False, + "has_support": False, + "ticket_votes": [], } - options[option]["votes"].append({ - "position": position, + # If this vote isn't a ticket vote, don't do anything else. + if position != 0: + continue + + # Add this vote to the option's ticket votes. + options[option]["ticket_votes"].append({ "member": member, + "other_acceptable": selected_options[1:] }) - - if option_is_ticket: - options[option]["ticket_count"] += 1 - + # Increase the vote and ticket vote counters. + options[option]["ticket_count"] += 1 options[option]["vote_count"] += 1 - option_is_ticket = False - - # Filtering out options without enough support and figuring out - # the most popular options - + # For all other acceptable options the member selected... + for other_acceptable_option in selected_options[1:]: + # If the option isn't among in the options list, add it. + if other_acceptable_option not in options: + options[other_acceptable_option] = { + "ticket_count": 0, + "vote_count": 0, + "marked_for_deletion": False, + "has_support": False, + "ticket_votes": [], + } + + # Increase the option's vote counter. + options[other_acceptable_option]["vote_count"] += 1 + + # Mark options that don't meet the >half acceptability treshold as such. options_without_support = [] for option_key, option in options.items(): @@ -427,45 +472,23 @@ def get_calculated_votes(request): options_without_support.append(option_key) continue - option["points"] += option["ticket_count"] * (max_vote_position + 1) - - for vote in option["votes"]: - option["points"] += ( - max_vote_position - - vote["position"] - + 1 - ) - + # Order the options based on whether they have support or not. + # This is purely for aesthetic purpose. options = { key: value for key, value in sorted( options.items(), reverse=True, - key=lambda option: option[1]["points"], + key=lambda option: option[1]["has_support"], ) } - 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["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": first_step_options, + "options": options, "options_by_member": options_by_member, "rv_members": rv_members, "total_ticket_count": total_ticket_count, @@ -475,22 +498,25 @@ def get_calculated_votes(request): ) ) - del first_step_options + ## END Step A - steps.append( - render_to_string( - "rv_voting_calc/steps/with_support.html", - { - "options": options, - "options_by_member": options_by_member, - "rv_members": rv_members - }, - request=request, + # Make sure we can always repeat a previously made calculation. This is also + # useful for permalinks. + + # First, try to find the seed in the URL parameters + seed = request.GET.get( + "seed", + # Then, try to find it in the cookies + request.COOKIES.get( + "seed", + # If neither are set, generate a default. + base64.b64encode(secrets.token_bytes()).decode("utf-8") ) ) + random.seed(seed) - ## END Step A - + # Continue until steps B - D have reached a final conclusion and count + # the amount of times the function ran to achieve this. iteration = 0 while True: @@ -499,14 +525,19 @@ def get_calculated_votes(request): iteration, steps, options, - options_by_member, options_without_support, + options_by_member, rv_members, - has_support_treshold, ) if ended: - return HttpResponse("\n".join(steps)) + response = HttpResponse("\n".join(steps)) + # Set the random seed cookie to the one used in the calculation. + response.set_cookie("seed", seed) + # Do the same for RV's GID. + response.set_cookie("rv_gid", rv_gid) + + return response # Step E iteration += 1 diff --git a/rybicka/settings/base.py b/rybicka/settings/base.py index 2ee813f7e68ac15cbf213be0d3ed8c374f2654c5..9d59ae05d40982c7e2da515a79e225197e7ba1de 100644 --- a/rybicka/settings/base.py +++ b/rybicka/settings/base.py @@ -57,6 +57,9 @@ MIDDLEWARE = [ "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + + "django_http_exceptions.middleware.ExceptionHandlerMiddleware", + "django_http_exceptions.middleware.ThreadLocalRequestMiddleware", ] ROOT_URLCONF = "rybicka.urls" diff --git a/static_src/rv_voting_calc.js b/static_src/rv_voting_calc.js index 881051eff0c78afa54200be6e2f0bc0ef630bbe3..58d6e09305478f3e9f0d1195aa234f061dfb62f1 100644 --- a/static_src/rv_voting_calc.js +++ b/static_src/rv_voting_calc.js @@ -2,6 +2,8 @@ import jQuery from "jquery"; Object.assign(window, { $: jQuery, jQuery }); +import Cookies from "js-cookie"; + require("select2/dist/js/i18n/cs"); import "select2/dist/js/select2.full"; import "select2/dist/css/select2.min.css"; @@ -134,8 +136,21 @@ $(window).ready( } } + const urlParams = new URL(window.location).searchParams; + const rvGid = urlParams.get("rv_gid"); + const seed = urlParams.get("seed"); + let response = await fetch( - `${VOTE_CALCULATION_ENDPOINT}?votes=${encodeURIComponent(JSON.stringify(votes))}` + VOTE_CALCULATION_ENDPOINT + + `?votes=${encodeURIComponent(JSON.stringify(votes))}` + + ( + (rvGid !== null) ? + `&rv_gid=${rvGid}` : "" + ) + + ( + (seed !== null) ? + `&seed=${seed}` : "" + ) ); if (!response.ok) { @@ -144,6 +159,18 @@ $(window).ready( } $("#result").html(await response.text()); + + $("#permalink").show(); + $("#permalink").attr( + "href", + ( + // Base URL + window.location.href.split('?')[0] + + `?votes=${encodeURIComponent(JSON.stringify(votes))}` + + `&rv_gid=${Cookies.get("rv_gid")}` + + `&seed=${Cookies.get("seed")}` + ) + ); } ); }