From b2a7942db4314ceb76529d2be644aa5752af42a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Sun, 27 Sep 2020 20:45:51 +0200 Subject: [PATCH] Feature/fix limited result (#62) * fix(restrict-results): fix typo in variables name * fix(restrict-results): add error message if user try to create an election with restricted result but without voting time. --- public/locale/i18n/de/resource.json | 1 + public/locale/i18n/en/resource.json | 1 + public/locale/i18n/es/resource.json | 1 + public/locale/i18n/fr/resource.json | 1 + public/locale/i18n/ru/resource.json | 1 + src/components/views/CreateElection.jsx | 231 ++++++++++++------------ 6 files changed, 124 insertions(+), 112 deletions(-) diff --git a/public/locale/i18n/de/resource.json b/public/locale/i18n/de/resource.json index b9f0ce1..d386891 100644 --- a/public/locale/i18n/de/resource.json +++ b/public/locale/i18n/de/resource.json @@ -17,6 +17,7 @@ "Write here your question or introduce simple your election (250 characters max.)": "Schreiben Sie hier Ihre Frage oder erklären Sie kurz ihre Wahl (bis 250 Zeichen)", "Enter the name of your candidate or proposal here (250 characters max.)": "Geben Sie hier den Namen Ihres Kandidaten oder Antrags ein (max. 250 Zeichen)", "Please add at least 2 candidates.": "Bitte geben Sie mindestens zwei Kandidaten vor. ", + "You can't limit access results without specify a voting time.": "Sie können die Zugriffsergebnisse nicht einschränken, ohne eine Abstimmungszeit anzugeben.", "Question of the election": "Zur Wahl stehende Frage", "Write here the question of your election": "Schreiben Sie hier die zur Wahl stehenden Frage", "For example:": "Zum Beispiel", diff --git a/public/locale/i18n/en/resource.json b/public/locale/i18n/en/resource.json index 0d375c8..3e59f46 100644 --- a/public/locale/i18n/en/resource.json +++ b/public/locale/i18n/en/resource.json @@ -17,6 +17,7 @@ "Write here your question or introduce simple your election (250 characters max.)": "Write here your question or introduce simple your election (250 characters max.)", "Enter the name of your candidate or proposal here (250 characters max.)":"Enter the name of your candidate or proposal here (250 characters max.)", "Please add at least 2 candidates.": "Please add at least 2 candidates.", + "You can't limit access results without specify a voting time.": "You can't limit access results without specify a voting time.", "Question of the election": "Question of the election", "Write here the question of your election": "Write here the question of your election", "For example:": "For example:", diff --git a/public/locale/i18n/es/resource.json b/public/locale/i18n/es/resource.json index aa9d071..d25fbd3 100644 --- a/public/locale/i18n/es/resource.json +++ b/public/locale/i18n/es/resource.json @@ -17,6 +17,7 @@ "Write here your question or introduce simple your election (250 characters max.)": "Escriba aquí su pregunta o introduzca simplemente su elección (250 caracteres máx.)", "Enter the name of your candidate or proposal here (250 characters max.)": "Escriba aquí el nombre de su candidato o propuesta (250 caracteres como máximo)", "Please add at least 2 candidates.": "Por favor, añada al menos dos canidatos(as).", + "You can't limit access results without specify a voting time." : "No se puede limitar el acceso a los resultados sin especificar un tiempo de votación.", "Question of the election": "Pregunta de su elección", "Write here the question of your election": "Escriba aquí la pregunta de su elección", "For example:": "Por ejemplo:", diff --git a/public/locale/i18n/fr/resource.json b/public/locale/i18n/fr/resource.json index 0936da5..4a25222 100644 --- a/public/locale/i18n/fr/resource.json +++ b/public/locale/i18n/fr/resource.json @@ -17,6 +17,7 @@ "Write here your question or introduce simple your election (250 characters max.)": "Décrire ici votre question ou introduire simplement votre vote (250 caractères max.)", "Enter the name of your candidate or proposal here (250 characters max.)": "Saisissez ici le nom de votre candidat ou de votre proposition (250 caractères max.)", "Please add at least 2 candidates.": "Merci d'ajouter au moins 2 candidats.", + "You can't limit access results without specify a voting time." : "Vous ne pouvez pas limiter l'accès aux résultats sans spécifier une durée de vote.", "Question of the election": "Question de votre vote", "Write here the question of your election": "Ecrire ici la question de votre vote", "For example:": "Par exemple", diff --git a/public/locale/i18n/ru/resource.json b/public/locale/i18n/ru/resource.json index 9fba13e..24f924c 100644 --- a/public/locale/i18n/ru/resource.json +++ b/public/locale/i18n/ru/resource.json @@ -17,6 +17,7 @@ "Write here your question or introduce simple your election (250 characters max.)": "Напишите свой вопрос или опишите голосование (250 символов максимум.)", "Enter the name of your candidate or proposal here (250 characters max.)": "Введите имя вашего кандидата или предложение здесь (не более 250 символов).", "Please add at least 2 candidates.": "Пожалуйста добавьте как минимум 2 кандидатов", + "You can't limit access results without specify a voting time." : "Вы не можете ограничить результаты доступа без указания времени голосования.", "Question of the election": "Суть голосования", "Write here the question of your election": "Напишите вопрос вашего голосования", "For example:": "Например:", diff --git a/src/components/views/CreateElection.jsx b/src/components/views/CreateElection.jsx index b4940dc..a77f71e 100644 --- a/src/components/views/CreateElection.jsx +++ b/src/components/views/CreateElection.jsx @@ -12,7 +12,7 @@ import { InputGroupAddon, Button, Card, - CardBody + CardBody, } from "reactstrap"; import { withTranslation } from "react-i18next"; import { ReactMultiEmail, isEmail } from "react-multi-email"; @@ -25,7 +25,7 @@ import { arrayMove, sortableContainer, sortableElement, - sortableHandle + sortableHandle, } from "react-sortable-hoc"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -33,7 +33,7 @@ import { faTrashAlt, faCheck, faCogs, - faExclamationTriangle + faExclamationTriangle, } from "@fortawesome/free-solid-svg-icons"; import { i18nGrades } from "../../Util"; import { AppContext } from "../../AppContext"; @@ -45,32 +45,32 @@ import i18n from "../../i18n"; // Error messages const AT_LEAST_2_CANDIDATES_ERROR = "Please add at least 2 candidates."; const NO_TITLE_ERROR = "Please add a title."; +const RESTRICTED_RESULTS_NEED_LIMITED_TIME = + "You can't limit access results without specify a voting time."; -const isValidDate = date => date instanceof Date && !isNaN(date); -const getOnlyValidDate = date => (isValidDate(date) ? date : new Date()); +const isValidDate = (date) => date instanceof Date && !isNaN(date); +const getOnlyValidDate = (date) => (isValidDate(date) ? date : new Date()); // Convert a Date object into YYYY-MM-DD -const dateToISO = date => - getOnlyValidDate(date) - .toISOString() - .substring(0, 10); +const dateToISO = (date) => + getOnlyValidDate(date).toISOString().substring(0, 10); // Retrieve the current hour, minute, sec, ms, time into a timestamp -const hours = date => getOnlyValidDate(date).getHours() * 3600 * 1000; -const minutes = date => getOnlyValidDate(date).getMinutes() * 60 * 1000; -const seconds = date => getOnlyValidDate(date).getSeconds() * 1000; -const ms = date => getOnlyValidDate(date).getMilliseconds(); -const time = date => +const hours = (date) => getOnlyValidDate(date).getHours() * 3600 * 1000; +const minutes = (date) => getOnlyValidDate(date).getMinutes() * 60 * 1000; +const seconds = (date) => getOnlyValidDate(date).getSeconds() * 1000; +const ms = (date) => getOnlyValidDate(date).getMilliseconds(); +const time = (date) => hours(getOnlyValidDate(date)) + minutes(getOnlyValidDate(date)) + seconds(getOnlyValidDate(date)) + ms(getOnlyValidDate(date)); // Retrieve the time part from a timestamp and remove the day. Return a int. -const timeMinusDate = date => time(getOnlyValidDate(date)); +const timeMinusDate = (date) => time(getOnlyValidDate(date)); // Retrieve the day and remove the time. Return a Date -const dateMinusTime = date => +const dateMinusTime = (date) => new Date(getOnlyValidDate(date).getTime() - time(getOnlyValidDate(date))); const DragHandle = sortableHandle(({ children }) => ( @@ -100,13 +100,13 @@ const SortableCandidate = sortableElement( form.editCandidateLabel(event, sortIndex)} - onKeyPress={event => + onChange={(event) => form.editCandidateLabel(event, sortIndex)} + onKeyPress={(event) => form.handleKeypressOnCandidateLabel(event, sortIndex) } placeholder={t("Candidate/proposal name...")} tabIndex={sortIndex + 1} - innerRef={ref => (form.candidateInputs[sortIndex] = ref)} + innerRef={(ref) => (form.candidateInputs[sortIndex] = ref)} maxLength="250" /> @@ -184,33 +184,35 @@ class CreateElection extends Component { successCreate: false, redirectTo: null, isAdvancedOptionsOpen: false, - restrictResult: false, + restrictResults: false, isTimeLimited: false, start, // by default, the election ends in a week finish: new Date(start.getTime() + 7 * 24 * 3600 * 1000), - electorEmails: [] + electorEmails: [], }; this.candidateInputs = []; this.focusInput = React.createRef(); this.handleSubmit = this.handleSubmit.bind(this); - this.handleRestrictResultCheck = this.handleRestrictResultCheck.bind(this); + this.handleRestrictResultsCheck = this.handleRestrictResultsCheck.bind( + this + ); this.handleIsTimeLimited = this.handleIsTimeLimited.bind(this); } - handleChangeTitle = event => { + handleChangeTitle = (event) => { this.setState({ title: event.target.value }); }; - handleIsTimeLimited = event => { + handleIsTimeLimited = (event) => { this.setState({ isTimeLimited: event.target.value === "1" }); }; - handleRestrictResultCheck = event => { - this.setState({ restrictResult: event.target.value === "1" }); + handleRestrictResultsCheck = (event) => { + this.setState({ restrictResults: event.target.value === "1" }); }; - addCandidate = event => { + addCandidate = (event) => { let candidates = this.state.candidates; if (candidates.length < 100) { candidates.push({ label: "" }); @@ -223,7 +225,7 @@ class CreateElection extends Component { } }; - removeCandidate = index => { + removeCandidate = (index) => { let candidates = this.state.candidates; candidates.splice(index, 1); if (candidates.length === 0) { @@ -235,11 +237,11 @@ class CreateElection extends Component { editCandidateLabel = (event, index) => { let candidates = this.state.candidates; candidates[index].label = event.currentTarget.value; - candidates.map(candidate => { + candidates.map((candidate) => { return candidate.label; }); this.setState({ - candidates: candidates + candidates: candidates, }); }; @@ -260,7 +262,7 @@ class CreateElection extends Component { this.setState({ candidates: candidates }); }; - handleChangeNumGrades = event => { + handleChangeNumGrades = (event) => { this.setState({ numGrades: event.target.value }); }; @@ -269,13 +271,16 @@ class CreateElection extends Component { }; checkFields() { - const { candidates, title } = this.state; + const { candidates, title, restrictResults, isTimeLimited } = this.state; if (!candidates) { return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR }; } + if (restrictResults && !isTimeLimited) { + return { ok: false, msg: RESTRICTED_RESULTS_NEED_LIMITED_TIME }; + } let numCandidates = 0; - candidates.forEach(c => { + candidates.forEach((c) => { if (c.label !== "") numCandidates += 1; }); if (numCandidates < 2) { @@ -290,29 +295,19 @@ class CreateElection extends Component { } handleSubmit() { - const { - candidates, - title, - numGrades, - electorEmails - } = this.state; + const { candidates, title, numGrades, electorEmails } = this.state; - let { - start, - finish, - } = this.state; + let { start, finish } = this.state; const endpoint = resolve( this.context.urlServer, this.context.routesServer.setElection ); - if(!this.state.isTimeLimited){ + if (!this.state.isTimeLimited) { let now = new Date(); - start = new Date( - now.getTime() - minutes(now) - seconds(now) - ms(now) - ); - finish=new Date(start.getTime() + 10 * 365 * 24 * 3600 * 1000); + start = new Date(now.getTime() - minutes(now) - seconds(now) - ms(now)); + finish = new Date(start.getTime() + 10 * 365 * 24 * 3600 * 1000); } const { t } = this.props; @@ -322,7 +317,7 @@ class CreateElection extends Component { const check = this.checkFields(); if (!check.ok) { toast.error(t(check.msg), { - position: toast.POSITION.TOP_CENTER + position: toast.POSITION.TOP_CENTER, }); return; } @@ -332,11 +327,11 @@ class CreateElection extends Component { fetch(endpoint, { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify({ title: title, - candidates: candidates.map(c => c.label).filter(c => c !== ""), + candidates: candidates.map((c) => c.label).filter((c) => c !== ""), on_invitation_only: electorEmails.length > 0, num_grades: numGrades, elector_emails: electorEmails, @@ -344,11 +339,11 @@ class CreateElection extends Component { finish_at: finish.getTime() / 1000, select_language: locale, front_url: window.location.origin, - restrict_result: this.state.restrictResult - }) + restrict_results: this.state.restrictResults, + }), }) - .then(response => response.json()) - .then(result => { + .then((response) => response.json()) + .then((result) => { if (result.id) { const nextPage = electorEmails && electorEmails.length @@ -357,23 +352,26 @@ class CreateElection extends Component { this.setState(() => ({ redirectTo: nextPage, successCreate: true, - waiting: false + waiting: false, })); } else { toast.error(t("Unknown error. Try again please."), { - position: toast.POSITION.TOP_CENTER + position: toast.POSITION.TOP_CENTER, }); this.setState({ waiting: false }); } }) - .catch(error => error); + .catch((error) => error); } - handleSendNotReady = msg => { + handleSendNotReady = () => { const { t } = this.props; - toast.error(t(msg), { - position: toast.POSITION.TOP_CENTER - }); + const check = this.checkFields(); + if (!check.ok) { + toast.error(t(check.msg), { + position: toast.POSITION.TOP_CENTER, + }); + } }; render() { @@ -387,7 +385,7 @@ class CreateElection extends Component { candidates, numGrades, isAdvancedOptionsOpen, - electorEmails + electorEmails, } = this.state; const { t } = this.props; @@ -460,7 +458,7 @@ class CreateElection extends Component { className="btn-block mt-2" tabIndex={candidates.length + 2} type="button" - onClick={event => this.addCandidate(event)} + onClick={(event) => this.addCandidate(event)} > {t("Add a proposal")} @@ -490,24 +488,24 @@ class CreateElection extends Component { -