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/<username>/elections/ +GET /api/user/<username>/elections/?limit=10 + +GET /api/elections/ +GET /api/elections/?limit=10 + +GET /api/elections/<uuid>/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