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