From 60ea2876257b38d0b314e401baeb9731d590a9cb Mon Sep 17 00:00:00 2001 From: philippe lhardy Date: Sun, 3 Aug 2025 12:21:29 +0200 Subject: [PATCH 01/16] feat: add rank voting feature merge chosenrank with 8.1.4 refactorings reworked from git@github.com:vinimoz/polls.git rank-vote-feature-vue3 TRAPPE Vincent voting variant generic a configuration list of answers for a vote. merged lib/Db/Pool.php with main manually Signed-off-by: philippe lhardy --- lib/Db/Poll.php | 18 ++- .../InvalidVotingVariantException.php | 19 +++ lib/Migration/V2/TableSchema.php | 1 + lib/ResponseDefinitions.php | 1 + lib/Service/PollService.php | 21 +++- src/Api/modules/polls.ts | 5 +- .../Configuration/ConfigRankOptions.vue | 111 ++++++++++++++++++ src/components/Create/PollCreateDlg.vue | 18 ++- .../Navigation/PollNavigationItems.vue | 2 +- .../SideBar/SideBarTabConfiguration.vue | 11 +- src/components/VoteTable/VoteButton.vue | 19 ++- src/components/VoteTable/VoteIndicator.vue | 95 +++++++++++---- src/components/VoteTable/VoteItem.vue | 24 +++- src/components/VoteTable/VoteTable.vue | 10 +- src/stores/poll.ts | 23 +++- src/stores/votes.ts | 2 +- 16 files changed, 346 insertions(+), 34 deletions(-) create mode 100644 lib/Exceptions/InvalidVotingVariantException.php create mode 100755 src/components/Configuration/ConfigRankOptions.vue diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index dbc51b3e56..1328041f7e 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -41,6 +41,8 @@ * @method void setAllowComment(int $value) * @method int getAllowMaybe() * @method void setAllowMaybe(int $value) + * @method string getChosenRank() + * @method void setChosenRank(string $value) * @method string getAllowProposals() * @method void setAllowProposals(string $value) * @method int getProposalsExpire() @@ -81,7 +83,7 @@ class Poll extends EntityWithUser implements JsonSerializable { public const TYPE_DATE = 'datePoll'; public const TYPE_TEXT = 'textPoll'; public const VARIANT_SIMPLE = 'simple'; - /** @deprecated use ACCESS_PRIVATE instead */ + public const VARIANT_GENERIC = 'generic'; public const ACCESS_HIDDEN = 'hidden'; /** @deprecated use ACCESS_OPEN instead */ public const ACCESS_PUBLIC = 'public'; @@ -148,6 +150,7 @@ class Poll extends EntityWithUser implements JsonSerializable { protected string $access = ''; protected int $anonymous = 0; protected int $allowMaybe = 0; + protected string $chosenRank = ''; protected string $allowProposals = ''; protected int $proposalsExpire = 0; protected int $voteLimit = 0; @@ -183,6 +186,7 @@ public function __construct() { $this->addType('anonymous', 'integer'); $this->addType('allowComment', 'integer'); $this->addType('allowMaybe', 'integer'); + $this->addType('chosenRank', 'string'); $this->addType('proposalsExpire', 'integer'); $this->addType('voteLimit', 'integer'); $this->addType('optionLimit', 'integer'); @@ -250,6 +254,7 @@ public function getConfigurationArray(): array { 'access' => $this->getAccess(), 'allowComment' => boolval($this->getAllowComment()), 'allowMaybe' => boolval($this->getAllowMaybe()), + 'chosenRank' => $this->getChosenRank(), 'allowProposals' => $this->getAllowProposals(), 'anonymous' => boolval($this->getAnonymous()), 'autoReminder' => $this->getAutoReminder(), @@ -319,6 +324,17 @@ public function deserializeArray(array $pollConfiguration): self { $this->setAccess($pollConfiguration['access'] ?? $this->getAccess()); $this->setAllowComment($pollConfiguration['allowComment'] ?? $this->getAllowComment()); $this->setAllowMaybe($pollConfiguration['allowMaybe'] ?? $this->getAllowMaybe()); + $chosenRank = $pollConfiguration['chosenRank'] ?? $this->getChosenRank(); + if (is_array($chosenRank)) { + $chosenRank = json_encode($chosenRank); // explicit serialisation + } elseif (is_string($chosenRank)) { + if (!json_decode($chosenRank)) { + $chosenRank = '[]'; + } + } else { + $chosenRank = '[]'; + } + $this->setChosenRank($chosenRank); $this->setAllowProposals($pollConfiguration['allowProposals'] ?? $this->getAllowProposals()); $this->setAnonymousSafe($pollConfiguration['anonymous'] ?? $this->getAnonymous()); $this->setAutoReminder($pollConfiguration['autoReminder'] ?? $this->getAutoReminder()); diff --git a/lib/Exceptions/InvalidVotingVariantException.php b/lib/Exceptions/InvalidVotingVariantException.php new file mode 100644 index 0000000000..37a874f118 --- /dev/null +++ b/lib/Exceptions/InvalidVotingVariantException.php @@ -0,0 +1,19 @@ + ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => 'private', 'length' => 1024]], 'anonymous' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'allow_maybe' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 1, 'length' => 20]], + 'chosen_rank' => ['type' => Types::TEXT, 'options' => ['notnull' => true, 'default' => 1, 'length' => 200]], 'allow_proposals' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => 'disallow', 'length' => 64]], 'proposals_expire' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'vote_limit' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index c01231b8d6..9825e41e7b 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -79,6 +79,7 @@ * access: string, * allowComment: boolean, * allowMaybe: boolean, + * chosenRank: String, * allowProposals: string, * anonymous: boolean, * autoReminder: boolean, diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index d3975b2880..66f00ba516 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -27,6 +27,7 @@ use OCA\Polls\Exceptions\InvalidPollTypeException; use OCA\Polls\Exceptions\InvalidShowResultsException; use OCA\Polls\Exceptions\InvalidUsernameException; +use OCA\Polls\Exceptions\InvalidVotingVariantException; use OCA\Polls\Exceptions\NotFoundException; use OCA\Polls\Exceptions\UserNotFoundException; use OCA\Polls\Model\Settings\AppSettings; @@ -194,11 +195,16 @@ public function add(string $type, string $title, string $votingVariant = Poll::V throw new ForbiddenException('Poll creation is disabled'); } - // Validate valuess + // Validate values if (!in_array($type, $this->getValidPollType())) { throw new InvalidPollTypeException('Invalid poll type'); } + if (!in_array($votingVariant, $this->getValidVotingVariant())) { + throw new InvalidVotingVariantException('Invalid voting variant'); + } + + if (!$title) { throw new EmptyTitleException('Title must not be empty'); } @@ -222,6 +228,7 @@ public function add(string $type, string $title, string $votingVariant = Poll::V $this->poll->setExpire(0); $this->poll->setAnonymousSafe(0); $this->poll->setAllowMaybe(0); + $this->poll->setChosenRank(''); $this->poll->setVoteLimit(0); $this->poll->setShowResults(Poll::SHOW_RESULTS_ALWAYS); $this->poll->setDeleted(0); @@ -428,6 +435,7 @@ public function clone(int $pollId): Poll { // deanonymize cloned polls by default, to avoid locked anonymous polls $this->poll->setAnonymous(0); $this->poll->setAllowMaybe($origin->getAllowMaybe()); + $this->poll->setChosenRank($origin->getChosenRank()); $this->poll->setVoteLimit($origin->getVoteLimit()); $this->poll->setShowResults($origin->getShowResults()); $this->poll->setAdminAccess($origin->getAdminAccess()); @@ -484,6 +492,17 @@ private function getValidPollType(): array { return [Poll::TYPE_DATE, Poll::TYPE_TEXT]; } + /** + * Get valid values for votingVariant + * + * @return string[] + * + * @psalm-return array{0: string, 1: string} + */ + private function getValidVotingVariant(): array { + return [Poll::VARIANT_SIMPLE, Poll::VARIANT_GENERIC]; + } + /** * Get valid values for access * diff --git a/src/Api/modules/polls.ts b/src/Api/modules/polls.ts index 6bb8e09275..7cd7c1d753 100644 --- a/src/Api/modules/polls.ts +++ b/src/Api/modules/polls.ts @@ -7,7 +7,7 @@ import { httpInstance, createCancelTokenHandler } from './HttpApi' import type { AxiosResponse } from '@nextcloud/axios' import type { ApiEmailAdressList, FullPollResponse } from './api.types' import type { PollGroup } from '../../stores/pollGroups.types' -import type { Poll, PollConfiguration, PollType } from '../../stores/poll.types' +import type { Poll, PollConfiguration, PollType, VotingVariant } from '../../stores/poll.types' export type Confirmations = { sentMails: { emailAddress: string; displayName: string }[] @@ -87,13 +87,14 @@ const polls = { }) }, - addPoll(type: PollType, title: string): Promise> { + addPoll(type: PollType, title: string, votingVariant: VotingVariant): Promise> { return httpInstance.request({ method: 'POST', url: 'poll/add', data: { type, title, + votingVariant, }, cancelToken: cancelTokenHandlerObject[ diff --git a/src/components/Configuration/ConfigRankOptions.vue b/src/components/Configuration/ConfigRankOptions.vue new file mode 100755 index 0000000000..1dbe2c4966 --- /dev/null +++ b/src/components/Configuration/ConfigRankOptions.vue @@ -0,0 +1,111 @@ + + + + + + diff --git a/src/components/Create/PollCreateDlg.vue b/src/components/Create/PollCreateDlg.vue index d4e875f1d2..447c568bac 100644 --- a/src/components/Create/PollCreateDlg.vue +++ b/src/components/Create/PollCreateDlg.vue @@ -16,10 +16,10 @@ import InputDiv from '../Base/modules/InputDiv.vue' import RadioGroupDiv from '../Base/modules/RadioGroupDiv.vue' import ConfigBox from '../Base/modules/ConfigBox.vue' -import { pollTypes, usePollStore } from '../../stores/poll' +import { pollTypes, usePollStore, votingVariants } from '../../stores/poll' import { showError, showSuccess } from '@nextcloud/dialogs' -import type { PollType } from '../../stores/poll.types' +import type { PollType, VotingVariant } from '../../stores/poll.types' const pollStore = usePollStore() @@ -36,6 +36,7 @@ const emit = defineEmits<{ const pollTitle = ref('') const pollType = ref('datePoll') +const votingVariant = ref('simple') const pollId = ref(null) const adding = ref(false) @@ -44,6 +45,11 @@ const pollTypeOptions = Object.entries(pollTypes).map(([key, value]) => ({ label: value.name, })) +const votingVariantOptions = Object.entries(votingVariants).map(([key, value]) => ({ + value: key, + label: value.name, +})) + const titleIsEmpty = computed(() => pollTitle.value === '') const disableAddButton = computed(() => titleIsEmpty.value || adding.value) @@ -55,6 +61,7 @@ async function addPoll() { const poll = await pollStore.add({ type: pollType.value, title: pollTitle.value, + votingVariant: votingVariant.value }) if (poll) { @@ -111,6 +118,13 @@ function resetPoll() { + + + + +