From 4bb4203038c6ddda62afbca8a8ff8a56fcb454f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org> Date: Thu, 26 Jan 2023 00:05:41 +0100 Subject: [PATCH] RV voting calc - Octopus GraphQL syncing, select2-ified UI --- Makefile | 2 +- env.example | 3 + .../member_group_size_calc/index.html | 9 +- package.json | 2 +- requirements/base.txt | 2 + requirements/{prod.txt => production.txt} | 0 .../templates/rv_voting_calc/index.html | 27 ++++- rv_voting_calc/views.py | 48 +++++++- rybicka/settings/base.py | 8 ++ rybicka/settings/{prod.py => production.py} | 0 static_src/member_group_size_calc.js | 4 - static_src/rv_voting_calc.js | 104 +++++++++++++++++- 12 files changed, 198 insertions(+), 11 deletions(-) rename requirements/{prod.txt => production.txt} (100%) rename rybicka/settings/{prod.py => production.py} (100%) diff --git a/Makefile b/Makefile index a21515d..8a03c83 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ venv: .venv/bin/python ${PYTHON} -m venv ${VENV} install: venv - ${VENV}/bin/pip install -r requirements/base.txt -r requirements/prod.txt + ${VENV}/bin/pip install -r requirements/base.txt -r requirements/production.txt ${VENV}/bin/nodeenv --python-virtualenv --node=19.3.0 ${VENV}/bin/npm install diff --git a/env.example b/env.example index 79d4cf2..3b703be 100644 --- a/env.example +++ b/env.example @@ -2,5 +2,8 @@ DATABASE_URL="postgresql://rybicka:rybicka@localhost:5432/rybicka" SECRET_KEY="%@=^sip3=tqn6d_-xvvidc1@-t0t3&*kab@vr4c4" +CHOBOTNICE_API_URL="https://chobotnice.pirati.cz/graphql/" +CHOBOTNICE_RV_GID="R3JvdXBUeXBlOjYyNQ==" + # Production settings ALLOWED_HOSTS="tools.pirati.cz" diff --git a/member_group_size_calc/templates/member_group_size_calc/index.html b/member_group_size_calc/templates/member_group_size_calc/index.html index 1484849..6ee5581 100644 --- a/member_group_size_calc/templates/member_group_size_calc/index.html +++ b/member_group_size_calc/templates/member_group_size_calc/index.html @@ -7,6 +7,11 @@ {% block description %}Výpočet velikosti skupiny členů podle jednoacího řádu.{% endblock %} {% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.11.x/css/styles.css" + > + {% render_bundle "member_group_size_calc" %} {% endblock %} @@ -15,7 +20,7 @@ <h1 class="text-6xl font-bebas mb-5">Kalkulačka velikosti skupiny členů</h1> <div class="bg-amber-100 p-4 flex flex-row items-center gap-4 mb-4 lg:w-[768px] md:w-full"> - <i class="fa-solid fa-lightbulb text-3xl text-amber-800"></i> + <i class="ico--book text-3xl text-amber-800"></i> <div class="text-amber-800"> Tato kalkulačka slouží pro výpočet skupiny členů podle <a class="underline text-amber-900" @@ -25,7 +30,7 @@ </div> <div class="bg-gray-100 p-4 flex flex-row items-start gap-4 mb-4 lg:w-[768px] md:w-full"> - <i class="fa-solid fa-circle-info text-3xl text-gray-800"></i> + <i class="ico--info text-3xl text-gray-800"></i> <div class="text-gray-800"> <h2 class="text-lg font-bold mb-3">Jednací řád celostátního fóra</h2> diff --git a/package.json b/package.json index 62c00d3..509de41 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "", "private": true, "dependencies": { - "@fortawesome/fontawesome-free": "^6.2.1", "css-loader": "^6.7.3", "jquery": "^3.6.3", + "select2": "^4.1.0-rc.0", "style-loader": "^3.3.1", "tailwindcss": "^3.2.4", "webpack": "^5.75.0", diff --git a/requirements/base.txt b/requirements/base.txt index 150dea4..9efe299 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,3 +4,5 @@ django-environ==0.9.0 psycopg2-binary==2.9.5 django-webpack-loader==1.8.0 nodeenv==1.7.0 +gql[requests]==3.4.0 +requests==2.28.2 diff --git a/requirements/prod.txt b/requirements/production.txt similarity index 100% rename from requirements/prod.txt rename to requirements/production.txt diff --git a/rv_voting_calc/templates/rv_voting_calc/index.html b/rv_voting_calc/templates/rv_voting_calc/index.html index 4a0ef68..a2e3bc9 100644 --- a/rv_voting_calc/templates/rv_voting_calc/index.html +++ b/rv_voting_calc/templates/rv_voting_calc/index.html @@ -12,6 +12,31 @@ {% block content %} <main> - + <h1 class="text-6xl font-bebas mb-5">Kalkulačka hlasování RV</h1> + + <div class="grid grid-cols-2 gap-4"> + <div> + <h2 class="text-2xl font-bebas mb-5">Hlasy členů</h2> + <ul class="flex flex-col gap-2"> + {% for member in rv_members %} + <li class="flex gap-4 items-center"> + <div class="basis-56 flex items-center"> + <i class="ico--user text-xl mr-2"></i> + {{ member.displayName }} + </div> + <select class="grow __vote-selection" multiple="multiple"></select> + </li> + {% endfor %} + </ul> + </div> + + <div> + <h2 class="text-2xl font-bebas mb-5">Výsledky</h2> + </div> + </div> </main> + + <script> + const RV_MEMBERS = {{ rv_members|safe }}; + </script> {% endblock %} diff --git a/rv_voting_calc/views.py b/rv_voting_calc/views.py index b40ece2..2267894 100644 --- a/rv_voting_calc/views.py +++ b/rv_voting_calc/views.py @@ -1,9 +1,55 @@ +import json + +import gql +import requests + +from gql.transport.requests import RequestsHTTPTransport + +from django.conf import settings from django.shortcuts import render # Create your views here. def index(request): + transport = RequestsHTTPTransport(url=settings.CHOBOTNICE_API_URL) + client = gql.Client( + transport=transport, + fetch_schema_from_transport=True, + ) + + # Get members from query + query = gql.gql( + f""" + {{ + group(id: "{settings.CHOBOTNICE_RV_GID}") {{ + memberships {{ + person {{ + username + displayName + officialLastName + }} + }} + }} + }} + """ + ) + + result = client.execute(query) + rv_members = [] + + # Convert to a nicer format + for member in result["group"]["memberships"]: + rv_members.append(member["person"]) + + # Sort alphabetically + rv_members.sort(key=lambda value: value["displayName"]) + return render( request, - "rv_voting_calc/index.html" + "rv_voting_calc/index.html", + { + "rv_members": rv_members, + # JS-Readable format + "json_rv_members": json.dumps(rv_members), + } ) diff --git a/rybicka/settings/base.py b/rybicka/settings/base.py index 1b182f8..2ee813f 100644 --- a/rybicka/settings/base.py +++ b/rybicka/settings/base.py @@ -113,3 +113,11 @@ WEBPACK_LOADER = { "IGNORE": [r".+\.hot-update.js", r".+\.map"], } } + +# Chobotnice + +CHOBOTNICE_API_URL=env.str( + "CHOBOTNICE_API_URL", + "https://chobotnice.pirati.cz/graphql/" +) +CHOBOTNICE_RV_GID=env.str("CHOBOTNICE_RV_GID") diff --git a/rybicka/settings/prod.py b/rybicka/settings/production.py similarity index 100% rename from rybicka/settings/prod.py rename to rybicka/settings/production.py diff --git a/static_src/member_group_size_calc.js b/static_src/member_group_size_calc.js index 85bf140..be1f821 100644 --- a/static_src/member_group_size_calc.js +++ b/static_src/member_group_size_calc.js @@ -1,9 +1,5 @@ import $ from "jquery"; -import "@fortawesome/fontawesome-free/js/fontawesome"; -import "@fortawesome/fontawesome-free/js/solid"; -import "@fortawesome/fontawesome-free/js/regular"; - $(window).ready( () => { $("#member-count").on( diff --git a/static_src/rv_voting_calc.js b/static_src/rv_voting_calc.js index 8d1c8b6..8ba4c3b 100644 --- a/static_src/rv_voting_calc.js +++ b/static_src/rv_voting_calc.js @@ -1 +1,103 @@ - +import jQuery from "jquery"; + +Object.assign(window, { $: jQuery, jQuery }); + +require("select2/dist/js/i18n/cs"); +import "select2/dist/js/select2.full"; +import "select2/dist/css/select2.min.css"; + +$(window).ready( + () => { + $(".__vote-selection").select2({ + tags: true, + tokenSeparators: [",", " "], + // https://stackoverflow.com/a/28657702 - Thanks to Artur Filipiak! + createTag: tag => { + return { + id: tag.term, + text: tag.term, + isNew : true + }; + } + }); + + $(".__vote-selection").on( + "select2:select", + event => { + //// Sync the tag option with other selectors. + + // If the tag isn't new for this selection do nothing. + if (!event.params.data.isNew) { + return; + } + + const tagName = event.params.data.text; + + const addedTag = new Option( + tagName, + tagName, + false, + false + ); + + // Get all other selections. + const unfilteredSelections = $(".__vote-selection").not(event.target); + let filteredSelections = []; + + // Check if they contain the tag. If they do, ignore them. + for (const selection of unfilteredSelections) { + if ($(selection).children(`option[value=${tagName}]`).length === 0) { + filteredSelections.push(selection); + } + } + + // Add the new tag to all selections that don't have it yet. + $(filteredSelections).append(addedTag).trigger("change"); + } + ); + + $(".__vote-selection").on( + "select2:unselect", + event => { + //// Remove the tag option if it's not selected anywhere. + + const tagName = event.params.data.text; + + // Get all other selections. + const selections = $(".__vote-selection"); + + // Check if any of them have the tag selected. If they do, end the function. + for (const selection of selections) { + for (const data of $(selection).select2("data")) { + if (data.selected && data.id == tagName) { + // Workaround - add the option back to this select (TODO - improve) + $(event.target).append( + new Option( + tagName, + tagName, + false, + false + ) + ).trigger("change"); + + return; + } + } + } + + // If the function has not ended by now, we can remove the tag. + for (const selection of selections) { + $(selection).children(`option[value=${tagName}]`).remove(); + $(selection).trigger("change"); + } + } + ) + + $(".__vote-selection").on( + "change.select2", + event => { + console.log("change"); + } + ); + } +) -- GitLab