Skip to content

Commit

Permalink
Merge pull request #33 from c4dt/add_voters
Browse files Browse the repository at this point in the history
Adding voters to form
  • Loading branch information
PascalinDe authored Oct 6, 2023
2 parents 09e25f5 + 98f3945 commit 51df85c
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 38 deletions.
1 change: 0 additions & 1 deletion web/backend/src/authManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export async function getUserPermissions(userID: number) {
await authEnforcer.getFilteredPolicy(0, String(userID)).then((authRights) => {
permissions = authRights;
});
console.log(`[getUserPermissions] user has permissions: ${permissions}`);
return permissions;
}

Expand Down
9 changes: 5 additions & 4 deletions web/backend/src/controllers/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from 'express';

import { isAuthorized, PERMISSIONS, readSCIPER } from '../authManager';
import { assignUserPermissionToOwnElection, isAuthorized, PERMISSIONS } from '../authManager';

export const usersRouter = express.Router();

Expand All @@ -19,20 +19,21 @@ usersRouter.get('/user_rights', (req, res) => {
res.json(users);
});

// This call (only for admins) allow an admin to add a role to a voter.
// This call (only for admins) allows an admin to add a role to a voter.
usersRouter.post('/add_role', (req, res, next) => {
if (!isAuthorized(req.session.userId, PERMISSIONS.SUBJECTS.ROLES, PERMISSIONS.ACTIONS.ADD)) {
res.status(400).send('Unauthorized - only admins allowed');
return;
}

try {
readSCIPER(req.body.sciper);
assignUserPermissionToOwnElection(req.body.sciper, req.body.formId);
} catch (e) {
res.status(400).send('Sciper length is incorrect');
res.status(400).send(`Error while adding to roles: ${e}`);
return;
}

res.set(200).send();
next();
// Call https://search-api.epfl.ch/api/ldap?q=228271, if the answer is
// empty then sciper unknown, otherwise add it in userDB
Expand Down
9 changes: 8 additions & 1 deletion web/frontend/src/language/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"add": "Hinzufügen",
"exportJSON": "Exportieren als JSON",
"delete": "Löschen",
"addVoters": "Neue Wähler",
"combineShares": "Aktien zusammenlegen",
"createElec": "Formular erstellen",
"clearForm": "Das Formular löschen",
Expand Down Expand Up @@ -122,7 +123,8 @@
"open": "Offen",
"close": "Schließen",
"cancel": "Abbrechen",
"canceled": "Abgesagt",
"confirm": "OK",
"canceled": "Abgebrochen",
"action": "Aktion",
"login": "Login",
"loggedIn": "Sie sind eingeloggt. ",
Expand Down Expand Up @@ -226,6 +228,11 @@
"end": "Das Ende",
"save": "Speichern Sie",
"contributors": "Our contributors",
"addVotersDialog": "Wähler hinzufügen",
"addVotersDialogSuccess": "Wähler hinzugefügt",
"votersAdded": "Folgende Wähler wurden hinzugefügt:",
"inputAddVoters": "Für jeden Wähler, fügen Sie die SCIPER Nummer auf eine neue Linie hinzu",
"addVotersConfirm": "Hinzufügen",
"nodeSetup": "Einrichtung des Knotens",
"inputNodeSetup": "Wählen Sie den Knoten aus, auf dem die Einrichtung beginnen soll:",
"inputProxyAddressError": "Fehler: Die Adresse eines Proxys kann nicht leer sein.",
Expand Down
7 changes: 7 additions & 0 deletions web/frontend/src/language/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"add": "Add",
"exportJSON": "Export as JSON",
"delete": "Delete",
"addVoters": "Add voters",
"combineShares": "Combine shares",
"createElec": "Create form",
"clearForm": "Clear form",
Expand Down Expand Up @@ -121,6 +122,7 @@
"open": "Open",
"close": "Close",
"cancel": "Cancel",
"confirm": "OK",
"canceled": "Canceled",
"action": "Action",
"login": "Login",
Expand All @@ -139,6 +141,7 @@
"initializing": "Initializing...",
"settingUp": "Setting up...",
"statusSetup": "Setup",
"addVotersConfirm": "Add voters",
"setupNode": "Setup Node",
"statusOpen": "Open",
"failed": "Failed",
Expand Down Expand Up @@ -228,6 +231,10 @@
"de": "🇩🇪 German",
"save": "Save",
"contributors": "Our contributors",
"addVotersDialog": "Adding voters",
"addVotersDialogSuccess": "Voters added",
"votersAdded": "The following voters have been added:",
"inputAddVoters": "For every voter, add their SCIPER on a new line",
"nodeSetup": "Node setup",
"inputNodeSetup": "Choose which node to start the setup on:",
"inputProxyAddressError": "Error: the address of a proxy cannot be empty.",
Expand Down
7 changes: 7 additions & 0 deletions web/frontend/src/language/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"add": "Ajouter",
"exportJSON": "Exporter en JSON",
"delete": "Supprimer",
"addVoters": "Ajouter voteurs",
"combineShares": "Combiner les actions",
"createElec": "Créer un sondage",
"clearForm": "Effacer un sondage",
Expand Down Expand Up @@ -122,6 +123,7 @@
"open": "Ouvrir",
"close": "Fermer",
"cancel": "Annuler",
"confirm": "OK",
"canceled": "Annulé",
"action": "Action",
"login": "Connexion",
Expand Down Expand Up @@ -226,6 +228,11 @@
"end": "Fin",
"save": "Enregistrer",
"contributors": "Nos contributeurs",
"addVotersDialog": "Ajouter des électeurs",
"addVotersDialogSuccess": "Électeurs ajoutés",
"votersAdded": "Les électeurs suivants ont été ajouté:",
"inputAddVoters": "Saisissez le SCIPER de chaque électeur/électrice sur une ligne séparée",
"addVotersConfirm": "Ajout SCIPERs",
"nodeSetup": "Configuration du noeud",
"inputNodeSetup": "Choisi un noeud pour commencer la configuration dessus:",
"inputProxyAddressError": "Erreur: l'adresse d'un proxy ne peut pas être vide.",
Expand Down
12 changes: 11 additions & 1 deletion web/frontend/src/pages/form/components/Action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ const Action: FC<ActionProps> = ({
nodeToSetup,
setNodeToSetup,
}) => {
const { getAction, modalClose, modalCancel, modalDelete, modalSetup } = useChangeAction(
const {
getAction,
modalClose,
modalCancel,
modalDelete,
modalSetup,
modalAddVoters,
modalAddVotersSuccess,
} = useChangeAction(
status,
formID,
roster,
Expand All @@ -55,6 +63,8 @@ const Action: FC<ActionProps> = ({
{modalClose}
{modalCancel}
{modalDelete}
{modalAddVoters}
{modalAddVotersSuccess}
{modalSetup}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { TrashIcon } from '@heroicons/react/outline';
import { AuthContext } from 'index';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';

const SUBJECT_ELECTION = 'election';
const ACTION_CREATE = 'create';

const AddVotersButton = ({ handleAddVoters }) => {
const authCtx = useContext(AuthContext);
const { t } = useTranslation();

return (
authCtx.isAllowed(SUBJECT_ELECTION, ACTION_CREATE) && (
<button onClick={handleAddVoters}>
<div className="whitespace-nowrap inline-flex items-center justify-center px-4 py-1 mr-2 border border-gray-300 text-sm rounded-full font-medium text-gray-700 hover:text-red-500">
<TrashIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{t('addVoters')}
</div>
</button>
)
);
};
export default AddVotersButton;
190 changes: 190 additions & 0 deletions web/frontend/src/pages/form/components/AddVotersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { Dialog, Transition } from '@headlessui/react';
import { CogIcon } from '@heroicons/react/outline';
import { FC, Fragment, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

type AddVotersModalSuccessProps = {
showModal: boolean;
setShowModal: (show: boolean) => void;
newVoters: string;
};

export const AddVotersModalSuccess: FC<AddVotersModalSuccessProps> = ({
showModal,
setShowModal,
newVoters,
}) => {
const { t } = useTranslation();

function closeModal() {
setShowModal(false);
}

return (
<Transition.Root show={showModal} as={Fragment}>
<Dialog as="div" className="fixed z-10 inset-0 px-4 overflow-y-auto" onClose={closeModal}>
<div className="block items-end justify-center min-h-screen text-center">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />

<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

{/* This element is to trick the browser into centering the modal contents. */}
<span className="inline-block align-middle h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div className=" inline-block bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all my-8 align-middle max-w-lg w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-indigo-100 sm:mx-0 sm:h-10 sm:w-10">
<CogIcon className="h-6 w-6 text-indigo-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
{t('addVotersDialog')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">{t('votersAdded')}</p>
</div>
<pre>{newVoters}</pre>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={closeModal}>
{t('confirm')}
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};
type AddVotersModalProps = {
showModal: boolean;
setShowModal: (show: boolean) => void;
setUserConfirmedAction: (voters: string) => void;
};

export const AddVotersModal: FC<AddVotersModalProps> = ({
showModal,
setShowModal,
setUserConfirmedAction,
}) => {
const { t } = useTranslation();
const cancelButtonRef = useRef(null);
const [voters, setVoters] = useState('');

const closeModal = () => {
setShowModal(false);
};

const confirmChoice = () => {
setUserConfirmedAction(voters);
closeModal();
};

const votersBox = () => {
return (
<div>
<textarea
autoFocus={true}
onChange={(e) => setVoters(e.target.value)}
name="Voters"
placeholder="SCIPERs"
className="m-3 px-1 w-100 text-lg border rounded-md"
rows={10}
/>
</div>
);
};

return (
<Transition.Root show={showModal} as={Fragment}>
<Dialog as="div" className="fixed z-10 inset-0 px-4 overflow-y-auto" onClose={closeModal}>
<div className="block items-end justify-center min-h-screen text-center">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />

<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

{/* This element is to trick the browser into centering the modal contents. */}
<span className="inline-block align-middle h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div className=" inline-block bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all my-8 align-middle max-w-lg w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-indigo-100 sm:mx-0 sm:h-10 sm:w-10">
<CogIcon className="h-6 w-6 text-indigo-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
{t('addVotersDialog')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">{t('inputAddVoters')}</p>
</div>
{votersBox()}
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={confirmChoice}>
{t('addVotersConfirm')}
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={closeModal}
ref={cancelButtonRef}>
{t('cancel')}
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};
Loading

0 comments on commit 51df85c

Please sign in to comment.