diff --git a/wahlfang/settings/development.py b/wahlfang/settings/development.py index 5471b5cbba39265c5b63a98c79a3253301def698..19ebd3e1ccf477b6fe828cb50096f6b6af49cc8e 100644 --- a/wahlfang/settings/development.py +++ b/wahlfang/settings/development.py @@ -32,6 +32,12 @@ EMAIL_SENDER = 'no-reply@stusta.de' EMAIL_PORT = 25 # LDAP +AUTHENTICATION_BACKENDS = { + 'vote.authentication.AccessCodeBackend', + 'management.authentication.ManagementBackend', + 'management.authentication.ManagementBackendLDAP', # this authentication backend must be enabled for ldap auth + 'django.contrib.auth.backends.ModelBackend' +} AUTH_LDAP_SERVER_URI = "ldap://ldap.stusta.de" AUTH_LDAP_USER_DN_TEMPLATE = "cn=%(user)s,ou=account,ou=pnyx,dc=stusta,dc=mhn,dc=de" AUTH_LDAP_START_TLS = True diff --git a/wahlfang_api/serializers.py b/wahlfang_api/serializers.py index 8a0f0205edcfb01afe4a8d300d1f61b3fe50eb88..c3cafc9dc22d1f550172ed98e5c33eaf55ee7a2b 100644 --- a/wahlfang_api/serializers.py +++ b/wahlfang_api/serializers.py @@ -93,6 +93,14 @@ class ElectionSerializer(serializers.ModelSerializer): return self.context['request'].user.can_vote(obj) +class SpectatorElectionSerializer(serializers.ModelSerializer): + election_summary = ElectionSummarySerializer(source='public_election_summary', many=True, read_only=True) + + class Meta: + model = Election + fields = '__all__' + + class SessionSerializer(serializers.ModelSerializer): class Meta: model = Session @@ -105,3 +113,11 @@ class VoterDetailSerializer(serializers.ModelSerializer): class Meta: model = Voter exclude = ['logged_in', 'password'] + + +class SpectatorSessionSerializer(serializers.ModelSerializer): + elections = SpectatorElectionSerializer(many=True, read_only=True) + + class Meta: + model = Session + fields = ['title', 'meeting_link', 'elections'] diff --git a/wahlfang_api/urls.py b/wahlfang_api/urls.py index 4e250be9cdca227eb7b06e5aebf718f2c6d60f30..08aa32ddcb53682b27c3942f88d229f7536b7f09 100644 --- a/wahlfang_api/urls.py +++ b/wahlfang_api/urls.py @@ -4,11 +4,13 @@ from rest_framework_simplejwt.views import ( TokenRefreshView, TokenVerifyView, ) + from wahlfang_api.views import ( TokenObtainVoterView, TokenObtainElectionManagerView, ElectionViewset, VoterInfoView, + SpectatorView, ) app_name = 'rest_api' @@ -18,8 +20,8 @@ router.register('vote/elections', ElectionViewset) urlpatterns = [ path('', include(router.urls)), + path('vote/spectator/<str:uuid>/', SpectatorView.as_view(), name='spectator'), path('vote/voter_info/', VoterInfoView.as_view(), name='voter_info'), - # path('vote/elections/', ElectionList.as_view(), name='election_list'), path('auth/code/token/', TokenObtainVoterView.as_view(), name='token_obtain_access_code'), path('auth/token/', TokenObtainElectionManagerView.as_view(), name='token_obtain_pair'), path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), diff --git a/wahlfang_api/views.py b/wahlfang_api/views.py index e49d8b398d66e7601aea1f63cab70dec3d211f80..bde55eec1f329a77814afa92873578f84912c558 100644 --- a/wahlfang_api/views.py +++ b/wahlfang_api/views.py @@ -6,15 +6,17 @@ from rest_framework.response import Response from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.views import TokenViewBase +from vote.forms import VoteForm +from vote.models import Election, Voter, Application, Session from wahlfang_api.authentication import IsVoter from wahlfang_api.serializers import ( TokenObtainVoterSerializer, TokenObtainElectionManagerSerializer, ElectionSerializer, - VoterDetailSerializer, EditApplicationSerializer + VoterDetailSerializer, + EditApplicationSerializer, + SpectatorSessionSerializer ) -from vote.forms import VoteForm -from vote.models import Election, Voter, Application class TokenObtainVoterView(TokenViewBase): @@ -44,6 +46,14 @@ class VoterInfoView(generics.RetrieveAPIView): return self.request.user +class SpectatorView(generics.RetrieveAPIView): + queryset = Session.objects.all() + serializer_class = SpectatorSessionSerializer + + def get_object(self): + return self.queryset.get(spectator_token=self.kwargs['uuid']) + + class ElectionViewset(viewsets.ReadOnlyModelViewSet): queryset = Election.objects.all() permission_classes = [IsVoter] diff --git a/wahlfang_web/src/api/index.js b/wahlfang_web/src/api/index.js index 94b19617248497b4966d74f57d010ae7e9639a13..926ac6c9f6a4408a5911c746ef0d50389ef44d14 100644 --- a/wahlfang_web/src/api/index.js +++ b/wahlfang_web/src/api/index.js @@ -85,6 +85,15 @@ export const logoutVoter = async () => { return true; } +export const fetchSpectatorInfo = async (uuid) => { + const response = await makeRequest(`/vote/spectator/${uuid}/`, 'GET'); + if (response.status === 200) { + return response.json(); + } else { + throw Error("could not fetch spectator view") + } +} + export const fetchVoterInfo = async () => { const response = await makeAuthenticatedVoterRequest(voteAPIRoutes.voterInfo, 'GET'); return await response.json(); diff --git a/wahlfang_web/src/components/SpectatorElection.js b/wahlfang_web/src/components/SpectatorElection.js new file mode 100644 index 0000000000000000000000000000000000000000..716cdaa42abe2ede8d6f0f89a43cf267f346d23b --- /dev/null +++ b/wahlfang_web/src/components/SpectatorElection.js @@ -0,0 +1,67 @@ +import React from "react"; +import moment from "moment"; + +export default function SpectatorElection({election}) { + const isOpen = election.start_date !== null && (new Date(election.start_date) <= new Date() && (election.end_date === null || new Date(election.end_date) > new Date())); + const isClosed = election.end_date !== null && new Date(election.end_date) <= new Date(); + + return ( + <div className="card mb-2"> + <div className="card-body"> + <h4 className="d-flex justify-content-between"> + <span>{election.title}</span> + </h4> + {election.end_date ? ( + <> + <small className="text-muted">Voting Period: {moment(election.start_date).format('llll')} + - {moment(election.end_date).format('llll')}</small> + </> + ) : null} + <hr/> + <div className="list-group mt-3"> + {isClosed && election.result_published === '1' ? ( + <div className="alert alert-info" role="alert"> + <h4 className="alert-heading">Voting Ended:</h4> + <hr/> + <table className="table table-striped"> + <thead className="thead-dark"> + <tr> + <th scope="col">#</th> + <th scope="col">{election.voters_self_apply ? "Applicant" : "Option"}</th> + <th scope="col">Yes</th> + <th scope="col">No</th> + <th scope="col">Abstention</th> + </tr> + </thead> + <tbody> + {election.election_summary.map((application, index) => ( + <tr key={index}> + <th scope="row">{index + 1}</th> + <td>{application.display_name}</td> + <td>{application.votes_accept}</td> + <td>{application.votes_reject}</td> + <td>{application.votes_abstention}</td> + </tr> + ))} + </tbody> + </table> + </div> + ) : !isOpen && !isClosed ? ( + <button className="btn btn-outline-dark disabled"> + Election has not started yet. + </button> + ) : isOpen && !isClosed ? ( + <button className="btn btn-outline-dark disabled"> + Election is currently ongoing. + </button> + ) : ( + <button className="btn btn-outline-dark disabled"> + Election is over but results haven't been published yet. Please ask your election manager to + do so and refresh the page. + </button> + )} + </div> + </div> + </div> + ) +} \ No newline at end of file diff --git a/wahlfang_web/src/pages/ManagementApp.js b/wahlfang_web/src/pages/ManagementApp.js index 9e46f9a1a78702c9d1fc3f003587a24f658c97e0..6fc35e63f26743be26abc4f8845c0aa1115b252f 100644 --- a/wahlfang_web/src/pages/ManagementApp.js +++ b/wahlfang_web/src/pages/ManagementApp.js @@ -16,29 +16,27 @@ export default function ManagementApp() { const {path} = useRouteMatch(); useEffect(() => { - if (loading && !authenticated) { - const authToken = loadManagerToken(); - if (authToken && isTokenValid(authToken.access)) { - setAuthenticated(true); - setLoading(false); - managementWS.initWs(); - console.log("found valid access token"); - } else if (authToken && isTokenValid(authToken.refresh)) { - console.log("found valid refresh token"); - refreshManagerToken() - .then(() => { - setAuthenticated(true); - setLoading(false); - managementWS.initWs(); - }) - .catch(() => { - setLoading(false); - }) - } else { - setLoading(false); - } + const authToken = loadManagerToken(); + if (authToken && isTokenValid(authToken.access)) { + setAuthenticated(true); + setLoading(false); + managementWS.initWs(); + console.log("found valid access token"); + } else if (authToken && isTokenValid(authToken.refresh)) { + console.log("found valid refresh token"); + refreshManagerToken() + .then(() => { + setAuthenticated(true); + setLoading(false); + managementWS.initWs(); + }) + .catch(() => { + setLoading(false); + }) + } else { + setLoading(false); } - }, [loading, setLoading, authenticated, setAuthenticated]) + }, [setLoading, setAuthenticated]) return ( <> diff --git a/wahlfang_web/src/pages/SpectatorView.js b/wahlfang_web/src/pages/SpectatorView.js index e97ffd23c48c41e1615c35dc68cb9741142243a9..228dd93266a3416cc56a5196b918151afb3ac722 100644 --- a/wahlfang_web/src/pages/SpectatorView.js +++ b/wahlfang_web/src/pages/SpectatorView.js @@ -1,5 +1,120 @@ +import Layout from "../components/Layout"; +import React, {useEffect, useState} from "react"; +import {useParams} from "react-router-dom"; +import {fetchSpectatorInfo} from "../api"; +import Loading from "../components/Loading"; +import SpectatorElection from "../components/SpectatorElection"; +import Header from "../components/Header"; -export default function SpectatorView () { +export default function SpectatorView() { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState({}); + // TODO: error handling, reloading + + const {uuid} = useParams(); + + useEffect(() => { + fetchSpectatorInfo(uuid) + .then((result) => { + setData(result); + setLoading(false) + }) + .catch((err) => { + setError(err.toString()); + setLoading(false); + }) + }, [uuid, setLoading, setData, setError]) + + if (loading || error !== null) { + return ( + <div id="content"> + <Header/> + <Layout> + {loading ? ( + <Loading/> + ) : ( + <div className="alert alert-danger">{error}</div> + )} + </Layout> + </div> + ) + } + + const elections = data.elections; + const open = elections.filter((election) => election.start_date !== null && new Date(election.start_date) <= new Date() && (election.end_date === null || new Date(election.end_date) > new Date())); + const upcoming = elections.filter((election) => election.start_date === null || new Date(election.start_date) > new Date()); + const closed = elections.filter((election) => election.end_date && new Date(election.end_date) <= new Date()); + const unpublished = closed.filter((election) => election.result_published === '0'); + const published = closed.filter((election) => election.result_published === '1'); + + return ( + <div id="content"> + <Header/> + <Layout> + <div className="card bg-dark text-light shadow mb-2 py-2"> + <div className="card-body"> + <h4 className="text-center d-inline">{data.title}</h4> + {data.meeting_link !== null ? ( + <div><small>Meeting at <a + href={data.meeting_link}>{data.meeting_link}</a></small> + </div> + ) : ""} + </div> + </div> + <div id="electionCard"> + {open.length > 0 ? ( + <div className="card shadow mb-2"> + <div className="card-header"> + <h4>Open Elections</h4> + </div> + <div className="card-body"> + {open.map(election => ( + <SpectatorElection key={election.id} election={election}/> + ))} + </div> + </div> + ) : ""} + {upcoming.length > 0 ? ( + <div className="card shadow mb-2"> + <div className="card-header"> + <h4>Upcoming Elections</h4> + </div> + <div className="card-body"> + {upcoming.map(election => ( + <SpectatorElection key={election.id} election={election}/> + ))} + </div> + </div> + ) : ""} + {unpublished.length > 0 ? ( + <div className="card shadow mb-2"> + <div className="card-header"> + <h4>Closed Elections</h4> + </div> + <div className="card-body"> + {unpublished.map(election => ( + <SpectatorElection key={election.id} election={election}/> + ))} + </div> + </div> + ) : ""} + {published.length > 0 ? ( + <div className="card shadow mb-2"> + <div className="card-header"> + <h4>Published Results</h4> + </div> + <div className="card-body"> + {published.map(election => ( + <SpectatorElection key={election.id} election={election}/> + ))} + </div> + </div> + ) : ""} + </div> + </Layout> + </div> + ) } \ No newline at end of file diff --git a/wahlfang_web/src/pages/VoteApp.js b/wahlfang_web/src/pages/VoteApp.js index 3e08d955c5e28a4d5db0318016cca005a40e1ebe..aee0e6fded2c8ea552027e1b1199071847a502f5 100644 --- a/wahlfang_web/src/pages/VoteApp.js +++ b/wahlfang_web/src/pages/VoteApp.js @@ -21,31 +21,27 @@ export default function VoteApp() { const [loading, setLoading] = useState(!authenticated); useEffect(() => { - if (loading && !authenticated) { - const authToken = loadVoterToken(); - if (authToken && isTokenValid(authToken.access)) { - setAuthenticated(true); - setLoading(false); - voterWS.initWs(); - console.log("found valid access token"); - } else if (authToken && isTokenValid(authToken.refresh)) { - console.log("found valid refresh token"); - refreshVoterToken() - .then(() => { - setAuthenticated(true); - setLoading(false); - voterWS.initWs(); - }) - .catch(() => { - setLoading(false); - }) - } else { - setLoading(false); - } + const authToken = loadVoterToken(); + if (authToken && isTokenValid(authToken.access)) { + setAuthenticated(true); + setLoading(false); + voterWS.initWs(); + console.log("found valid access token"); + } else if (authToken && isTokenValid(authToken.refresh)) { + console.log("found valid refresh token"); + refreshVoterToken() + .then(() => { + setAuthenticated(true); + setLoading(false); + voterWS.initWs(); + }) + .catch(() => { + setLoading(false); + }) + } else { + setLoading(false); } - }, [loading, setLoading, authenticated, setAuthenticated]) - - console.log("vote app loading:", loading); + }, [setLoading, setAuthenticated]) return ( <>