From c5d8ac7243ba2343cf2fac02d818d9fa8cdaf807 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 2 Jan 2025 11:30:53 +0100 Subject: [PATCH] move some vote logic to backend Signed-off-by: dartcafe --- lib/Controller/PublicController.php | 16 +++++++-- lib/Controller/VoteController.php | 9 ++--- lib/Db/Vote.php | 51 ++++++++++++++++++++++++++- src/components/Export/ExportPoll.vue | 8 ++--- src/components/VoteTable/VoteItem.vue | 2 +- src/helpers/index.ts | 1 + src/helpers/modules/StoreHelper.ts | 28 +++++++++++++++ src/stores/votes.ts | 48 ++++++++++--------------- 8 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 src/helpers/modules/StoreHelper.ts diff --git a/lib/Controller/PublicController.php b/lib/Controller/PublicController.php index 8404e459f..225d1bfff 100644 --- a/lib/Controller/PublicController.php +++ b/lib/Controller/PublicController.php @@ -162,8 +162,12 @@ public function getVotes(): JSONResponse { #[OpenAPI(OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'DELETE', url: '/s/{token}/user')] public function deleteUser(): JSONResponse { + $pollId = $this->userSession->getShare()->getPollId(); + $this->voteService->deleteUserFromPoll($pollId); return $this->response(fn () => [ - 'deleted' => $this->voteService->deleteUserFromPoll($this->userSession->getShare()->getPollId()) + 'poll' => $this->pollService->get($pollId), + 'options' => $this->optionService->list($pollId), + 'votes' => $this->voteService->list($pollId) ]); } @@ -175,8 +179,12 @@ public function deleteUser(): JSONResponse { #[OpenAPI(OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'DELETE', url: '/s/{token}/votes/orphaned')] public function deleteOrphanedVotes(): JSONResponse { + $pollId = $this->userSession->getShare()->getPollId(); + $this->voteService->deleteUserFromPoll($pollId, deleteOnlyOrphaned: true); return $this->response(fn () => [ - 'deleted' => $this->voteService->deleteUserFromPoll($this->userSession->getShare()->getPollId(), deleteOnlyOrphaned: true) + 'poll' => $this->pollService->get($pollId), + 'options' => $this->optionService->list($pollId), + 'votes' => $this->voteService->list($pollId) ]); } @@ -253,10 +261,12 @@ public function restoreOption(int $optionId): JSONResponse { #[FrontpageRoute(verb: 'PUT', url: '/s/{token}/vote')] public function setVote(int $optionId, string $setTo): JSONResponse { $option = $this->optionService->get($optionId); + $vote = $this->voteService->set($optionId, $setTo); return $this->response(fn () => [ - 'vote' => $this->voteService->set($optionId, $setTo), + 'vote' => $vote, 'poll' => $this->pollService->get($option->getPollId()), 'options' => $this->optionService->list($option->getPollId()), + 'votes' => $this->voteService->list($option->getPollId()) ]); } diff --git a/lib/Controller/VoteController.php b/lib/Controller/VoteController.php index e9a7575b8..f4a4dd2b1 100644 --- a/lib/Controller/VoteController.php +++ b/lib/Controller/VoteController.php @@ -55,11 +55,12 @@ public function list(int $pollId): JSONResponse { // #[FrontpageRoute(verb: 'PUT', url: '/vote/{optionId}/set/{setTo}')] public function set(int $optionId, string $setTo): JSONResponse { $option = $this->optionService->get($optionId); - + $vote = $this->voteService->set($optionId, $setTo); return $this->response(fn () => [ - 'vote' => $this->voteService->set($optionId, $setTo), + 'vote' => $vote, 'poll' => $this->pollService->get($option->getPollId()), 'options' => $this->optionService->list($option->getPollId()), + 'votes' => $this->voteService->list($option->getPollId()) ]); } @@ -73,8 +74,8 @@ public function set(int $optionId, string $setTo): JSONResponse { #[FrontpageRoute(verb: 'DELETE', url: '/poll/{pollId}/user/{userId}', postfix: 'named')] #[FrontpageRoute(verb: 'DELETE', url: '/poll/{pollId}/user', postfix: 'self')] public function delete(int $pollId, string $userId = ''): JSONResponse { + $this->voteService->deleteUserFromPoll($pollId, $userId); return $this->response(fn () => [ - 'deleted' => $this->voteService->deleteUserFromPoll($pollId, $userId), 'poll' => $this->pollService->get($pollId), 'options' => $this->optionService->list($pollId), 'votes' => $this->voteService->list($pollId) @@ -90,8 +91,8 @@ public function delete(int $pollId, string $userId = ''): JSONResponse { #[OpenAPI(OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'DELETE', url: '/poll/{pollId}/votes/orphaned')] public function deleteOrphaned(int $pollId, string $userId = ''): JSONResponse { + $this->voteService->deleteUserFromPoll($pollId, $userId, deleteOnlyOrphaned: true); return $this->response(fn () => [ - 'deleted' => $this->voteService->deleteUserFromPoll($pollId, $userId, deleteOnlyOrphaned: true), 'poll' => $this->pollService->get($pollId), 'options' => $this->optionService->list($pollId), 'votes' => $this->voteService->list($pollId) diff --git a/lib/Db/Vote.php b/lib/Db/Vote.php index f0ea1a8dc..70629b02e 100644 --- a/lib/Db/Vote.php +++ b/lib/Db/Vote.php @@ -9,6 +9,11 @@ namespace OCA\Polls\Db; use JsonSerializable; +use OCA\Polls\AppConstants; +use OCA\Polls\Helper\Container; +use OCA\Polls\UserSession; +use OCP\IL10N; +use OCP\L10N\IFactory; /** * @psalm-suppress UnusedProperty @@ -38,6 +43,10 @@ class Vote extends EntityWithUser implements JsonSerializable { public const VOTE_NO = 'no'; public const VOTE_EVENTUALLY = 'maybe'; + protected IL10N $l10n; + protected UserSession $userSession; + protected IFactory $transFactory; + // schema columns public $id = null; protected int $pollId = 0; @@ -51,13 +60,51 @@ class Vote extends EntityWithUser implements JsonSerializable { // joined columns protected ?int $optionId = null; - public function __construct() { + public function __construct( + ) { + $this->userSession = Container::queryClass(UserSession::class); + $this->transFactory = Container::queryClass(IFactory::class); + $this->userSession->getUser()->getLocaleCode(); + + $languageCode = $this->userSession->getUser()->getLanguageCode() !== '' ? $this->userSession->getUser()->getLanguageCode() : $this->transFactory->findGenericLanguage(); + + $this->l10n = $this->transFactory->get( + AppConstants::APP_ID, + $languageCode, + $this->userSession->getUser()->getLocaleCode() + ); + $this->addType('id', 'integer'); $this->addType('pollId', 'integer'); $this->addType('voteOptionId', 'integer'); $this->addType('deleted', 'integer'); } + private function getAnswerSymbol(): string { + switch ($this->getVoteAnswer()) { + case self::VOTE_YES: + return '✔'; + case self::VOTE_NO: + return '❌'; + case self::VOTE_EVENTUALLY: + return '❔'; + default: + return ''; + } + } + + private function getAnswerTranslated(): string { + switch ($this->getVoteAnswer()) { + case self::VOTE_YES: + return $this->l10n->t('Yes'); + case self::VOTE_NO: + return $this->l10n->t('No'); + case self::VOTE_EVENTUALLY: + return $this->l10n->t('Maybe'); + default: + return ''; + } + } /** * @return array * @@ -72,6 +119,8 @@ public function jsonSerialize(): array { 'deleted' => $this->getDeleted(), 'optionId' => $this->getOptionId(), 'user' => $this->getUser(), + 'answerSymbol' => $this->getAnswerSymbol(), + 'answerTranslated' => $this->getAnswerTranslated(), ]; } } diff --git a/src/components/Export/ExportPoll.vue b/src/components/Export/ExportPoll.vue index dd3c03bf6..cb54acff7 100644 --- a/src/components/Export/ExportPoll.vue +++ b/src/components/Export/ExportPoll.vue @@ -11,7 +11,7 @@ import { saveAs } from 'file-saver' import { t } from '@nextcloud/l10n' import { showError } from '@nextcloud/dialogs' - + import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' @@ -147,11 +147,11 @@ optionsStore.list.forEach((option) => { if (style === 'symbols') { - votesLine.push(votesStore.getVote({ userId: participant.id, option }).answerSymbol ?? '❌') + votesLine.push(votesStore.getVote({ user: participant, option }).answerSymbol ?? '❌') } else if (style === 'raw') { - votesLine.push(votesStore.getVote({ userId: participant.id, option }).answer) + votesLine.push(votesStore.getVote({ user: participant, option }).answer) } else { - votesLine.push(votesStore.getVote({ userId: participant.id, option }).answerTranslated ?? t('polls', 'No')) + votesLine.push(votesStore.getVote({ user: participant, option }).answerTranslated ?? t('polls', 'No')) } }) diff --git a/src/components/VoteTable/VoteItem.vue b/src/components/VoteTable/VoteItem.vue index b2279d3e6..966710ef6 100644 --- a/src/components/VoteTable/VoteItem.vue +++ b/src/components/VoteTable/VoteItem.vue @@ -41,7 +41,7 @@ const answer = computed(() => votesStore.getVote({ option: props.option, - userId: props.user.id, + user: props.user, }).answer) const iconAnswer = computed(() => { diff --git a/src/helpers/index.ts b/src/helpers/index.ts index eed888a72..585d00af6 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -8,3 +8,4 @@ export { uniqueArrayOfObjects, uniqueOptions, uniqueParticipants } from './modul export { groupComments } from './modules/comments.ts' export { SimpleLink } from './modules/SimpleLink.ts' export { GuestBubble } from './modules/GuestBubble.ts' +export { StoreHelper } from './modules/StoreHelper.ts' diff --git a/src/helpers/modules/StoreHelper.ts b/src/helpers/modules/StoreHelper.ts new file mode 100644 index 000000000..17a49b073 --- /dev/null +++ b/src/helpers/modules/StoreHelper.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { useVotesStore, Vote } from "../../stores/votes"; +import { Poll, usePollStore } from "../../stores/poll"; +import { Option, useOptionsStore } from "../../stores/options"; + +const StoreHelper = { + updateStores(data: { poll?: Poll, votes?: Vote[], options?: Option[] }) { + const pollStore = usePollStore() + const votesStore = useVotesStore() + const optionsStore = useOptionsStore() + + if (Object.prototype.hasOwnProperty.call(data, 'polls')) { + pollStore.$patch(data.poll) + } + if (Object.prototype.hasOwnProperty.call(data, 'votes')) { + votesStore.list = data.votes + } + if (Object.prototype.hasOwnProperty.call(data, 'options')) { + optionsStore.list = data.options + } + } +} + +export { StoreHelper } \ No newline at end of file diff --git a/src/stores/votes.ts b/src/stores/votes.ts index 8a05908c5..92547aaad 100644 --- a/src/stores/votes.ts +++ b/src/stores/votes.ts @@ -6,9 +6,8 @@ import { defineStore } from 'pinia' import { PublicAPI, VotesAPI } from '../Api/index.js' import { User } from '../Types/index.ts' -import { Logger } from '../helpers/index.ts' -import { t } from '@nextcloud/l10n' -import { Option, useOptionsStore } from './options.ts' +import { Logger, StoreHelper } from '../helpers/index.ts' +import { Option } from './options.ts' import { usePollStore } from './poll.ts' import { useSessionStore } from './session.ts' @@ -55,14 +54,20 @@ export const useVotesStore = defineStore('votes', { return this.list.filter((vote) => vote.answer === answer).length }, - getVote(payload: { userId: string; option: { text: string } }) { - const found = this.list.find((vote: Vote) => (vote.user.id === payload.userId + getVote(payload: { user: User; option: Option }): Vote { + const found = this.list.find((vote: Vote) => (vote.user.id === payload.user.id && vote.optionText === payload.option.text)) if (found === undefined) { return { - answer: '', + answer: Answer.None, optionText: payload.option.text, - userId: payload.userId, + user: payload.user, + answerSymbol: AnswerSymbol.None, + answerTranslated: '', + deleted: 0, + id: 0, + optionId: payload.option.id, + pollId: payload.option.pollId, } } return found @@ -82,22 +87,7 @@ export const useVotesStore = defineStore('votes', { return } - const votes: Vote[] = [] - response.data.votes.forEach((vote: Vote) => { - if (vote.answer === Answer.Yes) { - vote.answerTranslated = t('polls', 'Yes') - vote.answerSymbol = AnswerSymbol.Yes - } else if (vote.answer === Answer.Maybe) { - vote.answerTranslated = t('polls', 'Maybe') - vote.answerSymbol = AnswerSymbol.Maybe - } else { - vote.answerTranslated = t('polls', 'No') - vote.answerSymbol = AnswerSymbol.No - } - votes.push(vote) - }) - - this.list = votes + this.list = response.data.votes } catch (error) { if (error?.code === 'ERR_CANCELED') return this.$reset() @@ -119,7 +109,6 @@ export const useVotesStore = defineStore('votes', { async set(payload: { option: Option; setTo: Answer }) { const sessionStore = useSessionStore() - const optionsStore = useOptionsStore() const pollStore = usePollStore() try { let response = null @@ -128,9 +117,10 @@ export const useVotesStore = defineStore('votes', { } else { response = await VotesAPI.setVote(payload.option.id, payload.setTo) } + this.setItem({ option: payload.option, vote: response.data.vote }) - optionsStore.list = response.data.options - pollStore.$patch(response.data.poll) + StoreHelper.updateStores(response.data) + return response } catch (error) { if (error?.code === 'ERR_CANCELED') return @@ -154,7 +144,7 @@ export const useVotesStore = defineStore('votes', { } else { response = await VotesAPI.removeUser(sessionStore.route.params.id) } - this.list = this.list.filter((vote: Vote) => vote.user.id !== response.data.deleted) + StoreHelper.updateStores(response.data) } catch (error) { if (error?.code === 'ERR_CANCELED') return @@ -166,8 +156,8 @@ export const useVotesStore = defineStore('votes', { async deleteUser(payload) { const sessionStore = useSessionStore() try { - await VotesAPI.removeUser(sessionStore.route.params.id, payload.userId) - this.list = this.list.filter((vote: Vote) => vote.user.id !== payload.userId) + const response = await VotesAPI.removeUser(sessionStore.route.params.id, payload.userId) + StoreHelper.updateStores(response.data) } catch (error) { if (error?.code === 'ERR_CANCELED') return Logger.error('Error deleting votes', { error, payload })