From c1a8e826210b3ac7f09adbdf6ce19deaa9dffa8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Bedna=C5=99=C3=ADk?= <jan.bednarik@gmail.com>
Date: Wed, 16 Aug 2023 00:47:19 +0200
Subject: [PATCH] API with elections and voters

---
 api/__init__.py              |   0
 api/apps.py                  |   6 ++
 api/templates/api/index.html |  21 +++++++
 api/urls.py                  |  11 ++++
 api/views.py                 | 103 +++++++++++++++++++++++++++++++++++
 manage.py                    |   0
 settings.py                  |   1 +
 urls.py                      |   2 +
 8 files changed, 144 insertions(+)
 create mode 100644 api/__init__.py
 create mode 100644 api/apps.py
 create mode 100644 api/templates/api/index.html
 create mode 100644 api/urls.py
 create mode 100644 api/views.py
 mode change 100644 => 100755 manage.py

diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/apps.py b/api/apps.py
new file mode 100644
index 0000000..6d524a7
--- /dev/null
+++ b/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class HeliosConfig(AppConfig):
+    name = "api"
+    verbose_name = "API"
diff --git a/api/templates/api/index.html b/api/templates/api/index.html
new file mode 100644
index 0000000..0826374
--- /dev/null
+++ b/api/templates/api/index.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+<h1>Helios API</h1>
+<h2>Examples</h2>
+<hr>
+<pre>
+GET /api/user/&lt;username&gt;/elections/
+GET /api/user/&lt;username&gt;/elections/?limit=10
+
+GET /api/elections/
+GET /api/elections/?limit=10
+
+GET /api/elections/&lt;uuid&gt;/voters/
+</pre>
+<hr>
+<p>* default limits are 100 objects</p>
+
+</body>
+</html>
diff --git a/api/urls.py b/api/urls.py
new file mode 100644
index 0000000..531a45a
--- /dev/null
+++ b/api/urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls import url
+
+from . import views
+
+
+urlpatterns = [
+    url(r"^$", views.IndexView.as_view()),
+    url(r"^user/(?P<username>[^/]+)/elections/$", views.UserElectionsView.as_view()),
+    url(r"^elections/$", views.ElectionsView.as_view()),
+    url(r"^elections/(?P<uuid>[^/]+)/voters/$", views.ElectionVotersView.as_view()),
+]
diff --git a/api/views.py b/api/views.py
new file mode 100644
index 0000000..e6fc9f7
--- /dev/null
+++ b/api/views.py
@@ -0,0 +1,103 @@
+import pytz
+from django.http import JsonResponse
+from django.views.generic import TemplateView, View
+
+from helios.models import Election
+from helios_auth.models import User
+
+
+DEFAULT_LIMIT = 100
+
+
+class JsonView(View):
+    def get_payload(self, request, *args, **kwargs):
+        raise NotImplementedError
+
+    def get(self, request, *args, **kwargs):
+        return JsonResponse(self.get_payload(request, *args, **kwargs))
+
+
+class IndexView(TemplateView):
+    template_name = "api/index.html"
+
+
+def election_as_dict(election):
+    voting_start_at = (
+        election.voting_start_at.replace(tzinfo=pytz.UTC)
+        if election.voting_start_at
+        else None
+    )
+    voting_end_at = (
+        election.voting_end_at.replace(tzinfo=pytz.UTC)
+        if election.voting_end_at
+        else None
+    )
+    return {
+        "name": election.name,
+        "uuid": election.uuid,
+        "short_name": election.short_name,
+        "url": election.url,
+        "created_at": election.created_at,
+        "voting_has_started": election.voting_has_started(),
+        "voting_has_stopped": election.voting_has_stopped(),
+        "voting_start_at": voting_start_at,
+        "voting_end_at": voting_end_at,
+    }
+
+
+class ElectionsView(JsonView):
+    def get_payload(self, request, *args, **kwargs):
+        limit = int(request.GET.get("limit", DEFAULT_LIMIT))
+        qs = Election.objects.exclude(frozen_at=None).order_by("-created_at")[:limit]
+
+        elections = []
+        for election in qs:
+            elections.append(election_as_dict(election))
+
+        return {"elections": elections}
+
+
+class UserElectionsView(JsonView):
+    def get_payload(self, request, username, *args, **kwargs):
+        try:
+            user = User.objects.get(user_id__iexact=username)
+        except User.DoesNotExist:
+            return {}
+
+        limit = int(request.GET.get("limit", DEFAULT_LIMIT))
+        qs = (
+            user.voter_set.all()
+            .order_by("-election__created_at")
+            .select_related("election")[:limit]
+        )
+
+        elections = []
+        for voter in qs:
+            election = election_as_dict(voter.election)
+            election["user_has_voted"] = voter.vote_hash is not None
+            elections.append(election)
+
+        return {"username": username, "elections": elections}
+
+
+class ElectionVotersView(JsonView):
+    def get_payload(self, request, uuid, *args, **kwargs):
+        try:
+            election = Election.objects.get(uuid=uuid)
+        except Election.DoesNotExist:
+            return {}
+
+        result = election_as_dict(election)
+        result["voters"] = []
+
+        voters = (
+            election.voter_set.all()
+            .values_list("user__user_id", "vote_hash")
+            .order_by("user__user_id")
+        )
+        for user_id, vote_hash in voters:
+            result["voters"].append(
+                {"username": user_id, "has_voted": vote_hash is not None}
+            )
+
+        return result
diff --git a/manage.py b/manage.py
old mode 100644
new mode 100755
diff --git a/settings.py b/settings.py
index a158572..8b25fc2 100644
--- a/settings.py
+++ b/settings.py
@@ -166,6 +166,7 @@ INSTALLED_APPS = (
     'helios_auth',
     'helios',
     'server_ui',
+    'api',
 )
 
 ANYMAIL = {
diff --git a/urls.py b/urls.py
index 8c29e83..05ea89a 100644
--- a/urls.py
+++ b/urls.py
@@ -15,5 +15,7 @@ urlpatterns = [
     url(r'static/helios/(?P<path>.*)$', serve, {'document_root' : settings.ROOT_PATH + '/helios/media'}),
     url(r'static/(?P<path>.*)$', serve, {'document_root' : settings.ROOT_PATH + '/server_ui/media'}),
 
+    url(r'^api/', include('api.urls')),
+
     url(r'^', include('server_ui.urls')),
 ]
-- 
GitLab