Browse Source
Merge pull request #7306 from nextcloud/feature/noid/simple-polls
Merge pull request #7306 from nextcloud/feature/noid/simple-polls
📊 Simple polls API
pull/7484/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1963 additions and 13 deletions
-
2appinfo/info.xml
-
1appinfo/routes.php
-
48appinfo/routes/routesPollController.php
-
10docs/constants.md
-
114docs/poll.md
-
6lib/Chat/ChatManager.php
-
12lib/Chat/Parser/SystemMessage.php
-
260lib/Controller/PollController.php
-
19lib/GuestManager.php
-
12lib/Listener/UserDisplayNameListener.php
-
145lib/Migration/Version15000Date20220503121308.php
-
108lib/Model/Poll.php
-
70lib/Model/PollMapper.php
-
77lib/Model/Vote.php
-
91lib/Model/VoteMapper.php
-
281lib/Service/PollService.php
-
1mkdocs.yml
-
181tests/integration/features/bootstrap/FeatureContext.php
-
525tests/integration/features/chat/poll.feature
-
6tests/integration/spreedcheats/lib/Controller/ApiController.php
-
7tests/php/Chat/ChatManagerTest.php
@ -0,0 +1,48 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
$requirements = [ |
|||
'apiVersion' => 'v1', |
|||
'token' => '^[a-z0-9]{4,30}$', |
|||
]; |
|||
|
|||
$requirementsWithPollId = [ |
|||
'apiVersion' => 'v1', |
|||
'token' => '^[a-z0-9]{4,30}$', |
|||
'pollId' => '^[0-9]+$', |
|||
]; |
|||
|
|||
return [ |
|||
'ocs' => [ |
|||
/** @see \OCA\Talk\Controller\PollController::createPoll() */ |
|||
['name' => 'Poll#createPoll', 'url' => '/api/{apiVersion}/poll/{token}', 'verb' => 'POST', 'requirements' => $requirements], |
|||
/** @see \OCA\Talk\Controller\PollController::showPoll() */ |
|||
['name' => 'Poll#showPoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'GET', 'requirements' => $requirementsWithPollId], |
|||
/** @see \OCA\Talk\Controller\PollController::votePoll() */ |
|||
['name' => 'Poll#votePoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'POST', 'requirements' => $requirementsWithPollId], |
|||
/** @see \OCA\Talk\Controller\PollController::closePoll() */ |
|||
['name' => 'Poll#closePoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'DELETE', 'requirements' => $requirementsWithPollId], |
|||
], |
|||
]; |
|||
@ -0,0 +1,114 @@ |
|||
# Poll API |
|||
|
|||
Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` |
|||
|
|||
## Create a poll in a conversation |
|||
|
|||
* Method: `POST` |
|||
* Endpoint: `/poll/{token}` |
|||
* Data: |
|||
|
|||
| field | type | Description | |
|||
|--------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
|||
| `question` | string | The question of the poll | |
|||
| `options` | string[] | Array of strings with the voting options | |
|||
| `resultMode` | int | The result and voting mode of the poll, `0` means participants can immediatelly see the result and who voted for which option. `1` means the result is hidden until the poll is closed and then only the summary is published. | |
|||
| `maxVotes` | int | Maximum amount of options a participant can vote for | |
|||
|
|||
* Response: |
|||
- Status code: |
|||
+ `201 Created` |
|||
+ `400 Bad Request` When the question or the options were too long |
|||
+ `403 Forbidden` When the conversation is read-only |
|||
+ `403 Forbidden` When the actor does not have chat permissions |
|||
+ `404 Not Found` When the conversation could not be found for the participant |
|||
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator |
|||
|
|||
- Data: |
|||
|
|||
See [Poll data](#poll-data) |
|||
|
|||
## Get state or result of a poll |
|||
|
|||
* Method: `GET` |
|||
* Endpoint: `/poll/{token}/{pollId}` |
|||
|
|||
* Response: |
|||
- Status code: |
|||
+ `201 Created` |
|||
+ `400 Bad Request` In case of any other error |
|||
+ `404 Not Found` When the conversation could not be found for the participant |
|||
+ `404 Not Found` When the poll id could not be found in the conversation |
|||
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator |
|||
|
|||
- Data: |
|||
|
|||
See [Poll data](#poll-data) |
|||
|
|||
## Vote on a poll |
|||
|
|||
* Method: `POST` |
|||
* Endpoint: `/poll/{token}/{pollId}` |
|||
* Data: |
|||
|
|||
| field | type | Description | |
|||
|--------------|-------|--------------------------------------------------| |
|||
| `optionIds` | int[] | The option IDs the participant wants to vote for | |
|||
|
|||
|
|||
* Response: |
|||
- Status code: |
|||
+ `201 Created` |
|||
+ `400 Bad Request` When an option id is invalid |
|||
+ `400 Bad Request` When too many options were voted |
|||
+ `404 Not Found` When the conversation could not be found for the participant |
|||
+ `404 Not Found` When the poll id could not be found in the conversation |
|||
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator |
|||
|
|||
- Data: |
|||
|
|||
See [Poll data](#poll-data) |
|||
|
|||
## Close a poll |
|||
|
|||
* Method: `DELETE` |
|||
* Endpoint: `/poll/{token}/{pollId}` |
|||
|
|||
* Response: |
|||
- Status code: |
|||
+ `200 OK` |
|||
+ `403 Forbidden` When the participant is not the author of the poll or a moderator |
|||
+ `404 Not Found` When the conversation could not be found for the participant |
|||
+ `404 Not Found` When the poll id could not be found in the conversation |
|||
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator |
|||
|
|||
- Data: |
|||
|
|||
See [Poll data](#poll-data) |
|||
|
|||
## Poll data |
|||
|
|||
| field | type | Description | |
|||
|--------------------|----------|------------------------------------------------------------------------------------------------------------------| |
|||
| `id` | int | ID of the poll | |
|||
| `question` | string | The question of the poll | |
|||
| `options` | string[] | The options participants can vote for | |
|||
| `votes` | int[] | Map with optionId => number of votes (only available for when the actor voted on public poll or the poll is closed) | |
|||
| `actorType` | string | Actor type of the poll author (see [Constants - Attendee types](constants.md#attendee-types)) | |
|||
| `actorId` | string | Actor ID identifying the poll author | |
|||
| `actorDisplayName` | string | Display name of the poll author | |
|||
| `status` | int | Status of the poll (see [Constants - Poll status](constants.md#poll-status)) | |
|||
| `resultMode` | int | Result mode of the poll (see [Constants - Poll mode](constants.md#poll-mode)) | |
|||
| `maxVotes` | int | Maximum amount of options a user can vote for, `0` means unlimited | |
|||
| `votedSelf` | int[] | Array of option ids the participant voted for | |
|||
| `numVoters` | int | The number of unique voters that (only available for when the actor voted on public poll or the poll is closed) | |
|||
| `details` | array[] | Detailed list who voted for which option (only available for public closed polls), see [Details](#details) below | |
|||
|
|||
### Details |
|||
|
|||
| field | type | Description | |
|||
|------------------|--------|--------------------------------------------------------------------------------------------------------------| |
|||
| actorType | string | The actor type of the participant that voted (see [Constants - Attendee types](constants.md#attendee-types)) | |
|||
| actorId | string | The actor id of the participant that voted | |
|||
| actorDisplayName | string | The display name of the participant that voted | |
|||
| optionId | int | The option that was voted for | |
|||
@ -0,0 +1,260 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Controller; |
|||
|
|||
use OCA\Talk\Chat\ChatManager; |
|||
use OCA\Talk\Exceptions\WrongPermissionsException; |
|||
use OCA\Talk\Model\Poll; |
|||
use OCA\Talk\Model\Vote; |
|||
use OCA\Talk\Service\AttachmentService; |
|||
use OCA\Talk\Service\PollService; |
|||
use OCP\AppFramework\Db\DoesNotExistException; |
|||
use OCP\AppFramework\Http; |
|||
use OCP\AppFramework\Http\DataResponse; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\DB\Exception; |
|||
use OCP\IRequest; |
|||
use Psr\Log\LoggerInterface; |
|||
|
|||
class PollController extends AEnvironmentAwareController { |
|||
protected ChatManager $chatManager; |
|||
protected PollService $pollService; |
|||
protected AttachmentService $attachmentService; |
|||
protected ITimeFactory $timeFactory; |
|||
protected LoggerInterface $logger; |
|||
|
|||
public function __construct(string $appName, |
|||
IRequest $request, |
|||
ChatManager $chatManager, |
|||
PollService $pollService, |
|||
AttachmentService $attachmentService, |
|||
ITimeFactory $timeFactory, |
|||
LoggerInterface $logger) { |
|||
parent::__construct($appName, $request); |
|||
$this->pollService = $pollService; |
|||
$this->attachmentService = $attachmentService; |
|||
$this->chatManager = $chatManager; |
|||
$this->timeFactory = $timeFactory; |
|||
$this->logger = $logger; |
|||
} |
|||
|
|||
/** |
|||
* @PublicPage |
|||
* @RequireParticipant |
|||
* @RequireReadWriteConversation |
|||
* @RequirePermissions(permissions=chat) |
|||
* @RequireModeratorOrNoLobby |
|||
* |
|||
* @param string $question |
|||
* @param array $options |
|||
* @param int $resultMode |
|||
* @param int $maxVotes |
|||
* @return DataResponse |
|||
*/ |
|||
public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse { |
|||
$attendee = $this->participant->getAttendee(); |
|||
try { |
|||
$poll = $this->pollService->createPoll( |
|||
$this->room->getId(), |
|||
$attendee->getActorType(), |
|||
$attendee->getActorId(), |
|||
$attendee->getDisplayName(), |
|||
$question, |
|||
$options, |
|||
$resultMode, |
|||
$maxVotes |
|||
); |
|||
} catch (\Exception $e) { |
|||
$this->logger->error('Error creating poll', ['exception' => $e]); |
|||
return new DataResponse([], Http::STATUS_BAD_REQUEST); |
|||
} |
|||
|
|||
$message = json_encode([ |
|||
'message' => 'object_shared', |
|||
'parameters' => [ |
|||
'objectType' => 'talk-poll', |
|||
'objectId' => $poll->getId(), |
|||
'metaData' => [ |
|||
'type' => 'talk-poll', |
|||
'id' => $poll->getId(), |
|||
'name' => $question, |
|||
] |
|||
], |
|||
], JSON_THROW_ON_ERROR); |
|||
|
|||
try { |
|||
$this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true); |
|||
} catch (\Exception $e) { |
|||
$this->logger->error($e->getMessage(), ['exception' => $e]); |
|||
} |
|||
|
|||
return new DataResponse($this->renderPoll($poll, []), Http::STATUS_CREATED); |
|||
} |
|||
|
|||
/** |
|||
* @PublicPage |
|||
* @RequireParticipant |
|||
* @RequireModeratorOrNoLobby |
|||
* |
|||
* @param int $pollId |
|||
* @return DataResponse |
|||
*/ |
|||
public function showPoll(int $pollId): DataResponse { |
|||
try { |
|||
$poll = $this->pollService->getPoll($this->room->getId(), $pollId); |
|||
} catch (DoesNotExistException $e) { |
|||
return new DataResponse([], Http::STATUS_NOT_FOUND); |
|||
} |
|||
|
|||
$votedSelf = $this->pollService->getVotesForActor($this->participant, $poll); |
|||
$detailedVotes = []; |
|||
if ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) { |
|||
$detailedVotes = $this->pollService->getVotes($poll); |
|||
} |
|||
|
|||
return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes)); |
|||
} |
|||
|
|||
/** |
|||
* @PublicPage |
|||
* @RequireParticipant |
|||
* @RequireModeratorOrNoLobby |
|||
* |
|||
* @param int $pollId |
|||
* @param int[] $optionIds |
|||
* @return DataResponse |
|||
*/ |
|||
public function votePoll(int $pollId, array $optionIds): DataResponse { |
|||
try { |
|||
$poll = $this->pollService->getPoll($this->room->getId(), $pollId); |
|||
} catch (\Exception $e) { |
|||
return new DataResponse([], Http::STATUS_NOT_FOUND); |
|||
} |
|||
|
|||
if ($poll->getStatus() === Poll::STATUS_CLOSED) { |
|||
return new DataResponse([], Http::STATUS_BAD_REQUEST); |
|||
} |
|||
|
|||
try { |
|||
$votedSelf = $this->pollService->votePoll($this->participant, $poll, $optionIds); |
|||
} catch (\RuntimeException $e) { |
|||
return new DataResponse([], Http::STATUS_BAD_REQUEST); |
|||
} |
|||
|
|||
if ($poll->getResultMode() === Poll::MODE_PUBLIC) { |
|||
$attendee = $this->participant->getAttendee(); |
|||
try { |
|||
$message = json_encode([ |
|||
'message' => 'poll_voted', |
|||
'parameters' => [ |
|||
'poll' => [ |
|||
'type' => 'talk-poll', |
|||
'id' => $poll->getId(), |
|||
'name' => $poll->getQuestion(), |
|||
], |
|||
], |
|||
], JSON_THROW_ON_ERROR); |
|||
$this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true); |
|||
} catch (\Exception $e) { |
|||
$this->logger->error($e->getMessage(), ['exception' => $e]); |
|||
} |
|||
} |
|||
|
|||
return new DataResponse($this->renderPoll($poll, $votedSelf)); |
|||
} |
|||
|
|||
/** |
|||
* @PublicPage |
|||
* @RequireParticipant |
|||
* @RequireModeratorOrNoLobby |
|||
* |
|||
* @param int $pollId |
|||
* @return DataResponse |
|||
*/ |
|||
public function closePoll(int $pollId): DataResponse { |
|||
try { |
|||
$poll = $this->pollService->getPoll($this->room->getId(), $pollId); |
|||
} catch (\Exception $e) { |
|||
return new DataResponse([], Http::STATUS_NOT_FOUND); |
|||
} |
|||
|
|||
$poll->setStatus(Poll::STATUS_CLOSED); |
|||
|
|||
try { |
|||
$this->pollService->updatePoll($this->participant, $poll); |
|||
} catch (WrongPermissionsException $e) { |
|||
return new DataResponse([], Http::STATUS_FORBIDDEN); |
|||
} catch (Exception $e) { |
|||
$this->logger->error($e->getMessage(), ['exception' => $e]); |
|||
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); |
|||
} |
|||
|
|||
$attendee = $this->participant->getAttendee(); |
|||
try { |
|||
$message = json_encode([ |
|||
'message' => 'poll_closed', |
|||
'parameters' => [ |
|||
'poll' => [ |
|||
'type' => 'talk-poll', |
|||
'id' => $poll->getId(), |
|||
'name' => $poll->getQuestion(), |
|||
], |
|||
], |
|||
], JSON_THROW_ON_ERROR); |
|||
$this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true); |
|||
} catch (\Exception $e) { |
|||
$this->logger->error($e->getMessage(), ['exception' => $e]); |
|||
} |
|||
|
|||
$detailedVotes = []; |
|||
if ($poll->getResultMode() === Poll::MODE_PUBLIC) { |
|||
$detailedVotes = $this->pollService->getVotes($poll); |
|||
} |
|||
|
|||
$votedSelf = $this->pollService->getVotesForActor($this->participant, $poll); |
|||
|
|||
return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes)); |
|||
} |
|||
|
|||
protected function renderPoll(Poll $poll, array $votedSelf = [], array $detailedVotes = []): array { |
|||
$data = $poll->asArray(); |
|||
unset($data['roomId']); |
|||
|
|||
$canSeeSummary = !empty($votedSelf) && $poll->getResultMode() === Poll::MODE_PUBLIC; |
|||
|
|||
if (!$canSeeSummary && $poll->getStatus() === Poll::STATUS_OPEN) { |
|||
$data['votes'] = []; |
|||
$data['numVoters'] = 0; |
|||
} elseif ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) { |
|||
$data['details'] = array_map(static fn (Vote $vote) => $vote->asArray(), $detailedVotes); |
|||
} |
|||
|
|||
$data['votedSelf'] = array_map(static fn (Vote $vote) => $vote->getOptionId(), $votedSelf); |
|||
|
|||
return $data; |
|||
} |
|||
} |
|||
@ -0,0 +1,145 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Migration; |
|||
|
|||
use Closure; |
|||
use OCP\DB\ISchemaWrapper; |
|||
use OCP\DB\Types; |
|||
use OCP\Migration\IOutput; |
|||
use OCP\Migration\SimpleMigrationStep; |
|||
|
|||
class Version15000Date20220503121308 extends SimpleMigrationStep { |
|||
/** |
|||
* @param IOutput $output |
|||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` |
|||
* @param array $options |
|||
* @return null|ISchemaWrapper |
|||
*/ |
|||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { |
|||
/** @var ISchemaWrapper $schema */ |
|||
$schema = $schemaClosure(); |
|||
|
|||
if (!$schema->hasTable('talk_polls')) { |
|||
$table = $schema->createTable('talk_polls'); |
|||
|
|||
$table->addColumn('id', Types::BIGINT, [ |
|||
'autoincrement' => true, |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
]); |
|||
$table->addColumn('room_id', Types::BIGINT, [ |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
]); |
|||
$table->addColumn('question', Types::TEXT, [ |
|||
'notnull' => false, |
|||
'length' => null, |
|||
]); |
|||
$table->addColumn('options', Types::TEXT, [ |
|||
'notnull' => false, |
|||
'length' => null, |
|||
]); |
|||
$table->addColumn('votes', Types::TEXT, [ |
|||
'notnull' => false, |
|||
'length' => null, |
|||
]); |
|||
$table->addColumn('num_voters', Types::BIGINT, [ |
|||
'notnull' => false, |
|||
'length' => 20, |
|||
'default' => 0, |
|||
]); |
|||
$table->addColumn('actor_type', Types::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 64, |
|||
]); |
|||
$table->addColumn('actor_id', Types::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 64, |
|||
]); |
|||
$table->addColumn('display_name', Types::STRING, [ |
|||
'notnull' => false, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('status', Types::SMALLINT, [ |
|||
'notnull' => false, |
|||
'default' => 0, |
|||
]); |
|||
$table->addColumn('result_mode', Types::SMALLINT, [ |
|||
'notnull' => false, |
|||
'default' => 0, |
|||
]); |
|||
$table->addColumn('max_votes', Types::INTEGER, [ |
|||
'notnull' => true, |
|||
'default' => 0, |
|||
]); |
|||
|
|||
$table->setPrimaryKey(['id']); |
|||
$table->addIndex(['room_id'], 'talk_poll_room'); |
|||
} |
|||
|
|||
if (!$schema->hasTable('talk_poll_votes')) { |
|||
$table = $schema->createTable('talk_poll_votes'); |
|||
|
|||
$table->addColumn('id', Types::BIGINT, [ |
|||
'autoincrement' => true, |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
]); |
|||
$table->addColumn('poll_id', Types::BIGINT, [ |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
]); |
|||
$table->addColumn('room_id', Types::BIGINT, [ |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
]); |
|||
$table->addColumn('actor_type', Types::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 64, |
|||
]); |
|||
$table->addColumn('actor_id', Types::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 64, |
|||
]); |
|||
$table->addColumn('display_name', Types::STRING, [ |
|||
'notnull' => false, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('option_id', Types::INTEGER, [ |
|||
'notnull' => true, |
|||
'length' => 6, |
|||
'default' => 1, |
|||
]); |
|||
|
|||
$table->setPrimaryKey(['id']); |
|||
$table->addIndex(['poll_id', 'actor_type', 'actor_id'], 'talk_poll_vote'); |
|||
$table->addIndex(['room_id'], 'talk_vote_room'); |
|||
} |
|||
|
|||
return $schema; |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Model; |
|||
|
|||
use OCP\AppFramework\Db\Entity; |
|||
|
|||
/** |
|||
* @method void setRoomId(int $roomId) |
|||
* @method int getRoomId() |
|||
* @method void setQuestion(string $question) |
|||
* @method string getQuestion() |
|||
* @method void setOptions(string $options) |
|||
* @method string getOptions() |
|||
* @method void setVotes(string $votes) |
|||
* @method string getVotes() |
|||
* @method void setNumVoters(int $numVoters) |
|||
* @method int getNumVoters() |
|||
* @method void setActorType(string $actorType) |
|||
* @method string getActorType() |
|||
* @method void setActorId(string $actorId) |
|||
* @method string getActorId() |
|||
* @method void setDisplayName(string $displayName) |
|||
* @method string getDisplayName() |
|||
* @method void setStatus(int $status) |
|||
* @method int getStatus() |
|||
* @method void setResultMode(int $resultMode) |
|||
* @method int getResultMode() |
|||
* @method void setMaxVotes(int $maxVotes) |
|||
* @method int getMaxVotes() |
|||
*/ |
|||
class Poll extends Entity { |
|||
public const STATUS_OPEN = 0; |
|||
public const STATUS_CLOSED = 1; |
|||
public const MODE_PUBLIC = 0; |
|||
public const MODE_HIDDEN = 1; |
|||
public const MAX_VOTES_UNLIMITED = 0; |
|||
|
|||
protected int $roomId = 0; |
|||
protected string $question = ''; |
|||
protected string $options = ''; |
|||
protected string $votes = ''; |
|||
protected int $numVoters = 0; |
|||
protected string $actorType = ''; |
|||
protected string $actorId = ''; |
|||
protected ?string $displayName = null; |
|||
protected int $status = self::STATUS_OPEN; |
|||
protected int $resultMode = self::MODE_PUBLIC; |
|||
protected int $maxVotes = self::MAX_VOTES_UNLIMITED; |
|||
|
|||
public function __construct() { |
|||
$this->addType('roomId', 'int'); |
|||
$this->addType('question', 'string'); |
|||
$this->addType('options', 'string'); |
|||
$this->addType('votes', 'string'); |
|||
$this->addType('numVoters', 'int'); |
|||
$this->addType('actorType', 'string'); |
|||
$this->addType('actorId', 'string'); |
|||
$this->addType('displayName', 'string'); |
|||
$this->addType('status', 'int'); |
|||
$this->addType('resultMode', 'int'); |
|||
$this->addType('maxVotes', 'int'); |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function asArray(): array { |
|||
return [ |
|||
'id' => $this->getId(), |
|||
// The room id is not needed on the API level but only internally for optimising database queries
|
|||
// 'roomId' => $this->getRoomId(),
|
|||
'question' => $this->getQuestion(), |
|||
'options' => json_decode($this->getOptions(), true, 512, JSON_THROW_ON_ERROR), |
|||
'votes' => json_decode($this->getVotes(), true, 512, JSON_THROW_ON_ERROR), |
|||
'numVoters' => $this->getNumVoters(), |
|||
'actorType' => $this->getActorType(), |
|||
'actorId' => $this->getActorId(), |
|||
'actorDisplayName' => $this->getDisplayName(), |
|||
'status' => $this->getStatus(), |
|||
'resultMode' => $this->getResultMode(), |
|||
'maxVotes' => $this->getMaxVotes(), |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Model; |
|||
|
|||
use OCP\AppFramework\Db\DoesNotExistException; |
|||
use OCP\AppFramework\Db\MultipleObjectsReturnedException; |
|||
use OCP\AppFramework\Db\QBMapper; |
|||
use OCP\DB\Exception; |
|||
use OCP\DB\QueryBuilder\IQueryBuilder; |
|||
use OCP\IDBConnection; |
|||
|
|||
class PollMapper extends QBMapper { |
|||
|
|||
/** |
|||
* @param IDBConnection $db |
|||
*/ |
|||
public function __construct(IDBConnection $db) { |
|||
parent::__construct($db, 'talk_polls', Poll::class); |
|||
} |
|||
|
|||
/** |
|||
* @param int $pollId |
|||
* @return Poll |
|||
* @throws DoesNotExistException |
|||
* @throws MultipleObjectsReturnedException |
|||
* @throws Exception |
|||
*/ |
|||
public function getByPollId(int $pollId): Poll { |
|||
$query = $this->db->getQueryBuilder(); |
|||
|
|||
$query->select('*') |
|||
->from($this->getTableName()) |
|||
->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))); |
|||
|
|||
return $this->findEntity($query); |
|||
} |
|||
|
|||
public function deleteByRoomId(int $roomId): void { |
|||
$query = $this->db->getQueryBuilder(); |
|||
|
|||
$query->delete($this->getTableName()) |
|||
->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); |
|||
|
|||
$query->executeStatement(); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Model; |
|||
|
|||
use OCP\AppFramework\Db\Entity; |
|||
|
|||
/** |
|||
* @method void setPollId(int $pollId) |
|||
* @method int getPollId() |
|||
* @method void setRoomId(int $roomId) |
|||
* @method int getRoomId() |
|||
* @method void setActorType(string $actorType) |
|||
* @method string getActorType() |
|||
* @method void setActorId(string $actorId) |
|||
* @method string getActorId() |
|||
* @method void setDisplayName(string $displayName) |
|||
* @method string getDisplayName() |
|||
* @method void setOptionId(int $optionId) |
|||
* @method int getOptionId() |
|||
*/ |
|||
class Vote extends Entity { |
|||
protected int $pollId = 0; |
|||
protected int $roomId = 0; |
|||
protected string $actorType = ''; |
|||
protected string $actorId = ''; |
|||
protected ?string $displayName = null; |
|||
protected int $optionId = 0; |
|||
|
|||
public function __construct() { |
|||
$this->addType('pollId', 'int'); |
|||
$this->addType('roomId', 'int'); |
|||
$this->addType('actorType', 'string'); |
|||
$this->addType('actorId', 'string'); |
|||
$this->addType('displayName', 'string'); |
|||
$this->addType('optionId', 'int'); |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function asArray(): array { |
|||
return [ |
|||
// The ids are not needed on the API level but only internally for optimising database queries
|
|||
// 'id' => $this->getId(),
|
|||
// 'pollId' => $this->getPollId(),
|
|||
// 'roomId' => $this->getRoomId(),
|
|||
'actorType' => $this->getActorType(), |
|||
'actorId' => $this->getActorId(), |
|||
'actorDisplayName' => $this->getDisplayName(), |
|||
'optionId' => $this->getOptionId(), |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Model; |
|||
|
|||
use OCP\AppFramework\Db\QBMapper; |
|||
use OCP\DB\QueryBuilder\IQueryBuilder; |
|||
use OCP\IDBConnection; |
|||
|
|||
class VoteMapper extends QBMapper { |
|||
|
|||
/** |
|||
* @param IDBConnection $db |
|||
*/ |
|||
public function __construct(IDBConnection $db) { |
|||
parent::__construct($db, 'talk_poll_votes', Vote::class); |
|||
} |
|||
|
|||
/** |
|||
* @param int $pollId |
|||
* @return Vote[] |
|||
*/ |
|||
public function findByPollId(int $pollId): array { |
|||
$query = $this->db->getQueryBuilder(); |
|||
$query->select('*') |
|||
->from($this->getTableName()) |
|||
->where($query->expr()->eq('poll_id', $query->createNamedParameter($pollId))); |
|||
|
|||
return $this->findEntities($query); |
|||
} |
|||
|
|||
/** |
|||
* @param int $pollId |
|||
* @param string $actorType |
|||
* @param string $actorId |
|||
* @return Vote[] |
|||
*/ |
|||
public function findByPollIdForActor(int $pollId, string $actorType, string $actorId): array { |
|||
$query = $this->db->getQueryBuilder(); |
|||
$query->select('*') |
|||
->from($this->getTableName()) |
|||
->where($query->expr()->eq('poll_id', $query->createNamedParameter($pollId))) |
|||
->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType))) |
|||
->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId))); |
|||
|
|||
return $this->findEntities($query); |
|||
} |
|||
|
|||
public function deleteByRoomId(int $roomId): void { |
|||
$query = $this->db->getQueryBuilder(); |
|||
|
|||
$query->delete($this->getTableName()) |
|||
->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); |
|||
|
|||
$query->executeStatement(); |
|||
} |
|||
|
|||
public function deleteVotesByActor(int $pollId, string $actorType, string $actorId): void { |
|||
$query = $this->db->getQueryBuilder(); |
|||
|
|||
$query->delete($this->getTableName()) |
|||
->where($query->expr()->eq('poll_id', $query->createNamedParameter($pollId))) |
|||
->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType))) |
|||
->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId))); |
|||
|
|||
$query->executeStatement(); |
|||
} |
|||
} |
|||
@ -0,0 +1,281 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @author Joas Schilling <coding@schilljs.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\Talk\Service; |
|||
|
|||
use OCA\Talk\Exceptions\WrongPermissionsException; |
|||
use OCA\Talk\Model\Poll; |
|||
use OCA\Talk\Model\PollMapper; |
|||
use OCA\Talk\Model\Vote; |
|||
use OCA\Talk\Model\VoteMapper; |
|||
use OCA\Talk\Participant; |
|||
use OCP\AppFramework\Db\DoesNotExistException; |
|||
use OCP\DB\Exception; |
|||
use OCP\DB\QueryBuilder\IQueryBuilder; |
|||
use OCP\IDBConnection; |
|||
|
|||
class PollService { |
|||
protected IDBConnection $connection; |
|||
protected PollMapper $pollMapper; |
|||
protected VoteMapper $voteMapper; |
|||
|
|||
public function __construct(IDBConnection $connection, |
|||
PollMapper $pollMapper, |
|||
VoteMapper $voteMapper) { |
|||
$this->connection = $connection; |
|||
$this->pollMapper = $pollMapper; |
|||
$this->voteMapper = $voteMapper; |
|||
} |
|||
|
|||
public function createPoll(int $roomId, string $actorType, string $actorId, string $displayName, string $question, array $options, int $resultMode, int $maxVotes): Poll { |
|||
$poll = new Poll(); |
|||
$poll->setRoomId($roomId); |
|||
$poll->setActorType($actorType); |
|||
$poll->setActorId($actorId); |
|||
$poll->setDisplayName($displayName); |
|||
$poll->setQuestion($question); |
|||
$poll->setOptions(json_encode($options)); |
|||
$poll->setVotes(json_encode([])); |
|||
$poll->setResultMode($resultMode); |
|||
$poll->setMaxVotes($maxVotes); |
|||
|
|||
$this->pollMapper->insert($poll); |
|||
|
|||
return $poll; |
|||
} |
|||
|
|||
/** |
|||
* @param int $roomId |
|||
* @param int $pollId |
|||
* @return Poll |
|||
* @throws DoesNotExistException |
|||
*/ |
|||
public function getPoll(int $roomId, int $pollId): Poll { |
|||
$poll = $this->pollMapper->getByPollId($pollId); |
|||
|
|||
if ($poll->getRoomId() !== $roomId) { |
|||
throw new DoesNotExistException('Room id mismatch'); |
|||
} |
|||
|
|||
return $poll; |
|||
} |
|||
|
|||
/** |
|||
* @param Participant $participant |
|||
* @param Poll $poll |
|||
* @throws WrongPermissionsException |
|||
* @throws Exception |
|||
*/ |
|||
public function updatePoll(Participant $participant, Poll $poll): void { |
|||
if (!$participant->hasModeratorPermissions() |
|||
&& ($poll->getActorType() !== $participant->getAttendee()->getActorType() |
|||
|| $poll->getActorId() !== $participant->getAttendee()->getActorId())) { |
|||
// Only moderators and the author of the poll can update it
|
|||
throw new WrongPermissionsException(); |
|||
} |
|||
|
|||
$this->pollMapper->update($poll); |
|||
} |
|||
|
|||
/** |
|||
* @param Participant $participant |
|||
* @param Poll $poll |
|||
* @return Vote[] |
|||
*/ |
|||
public function getVotesForActor(Participant $participant, Poll $poll): array { |
|||
return $this->voteMapper->findByPollIdForActor( |
|||
$poll->getId(), |
|||
$participant->getAttendee()->getActorType(), |
|||
$participant->getAttendee()->getActorId() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @param Poll $poll |
|||
* @return Vote[] |
|||
*/ |
|||
public function getVotes(Poll $poll): array { |
|||
return $this->voteMapper->findByPollId($poll->getId()); |
|||
} |
|||
|
|||
/** |
|||
* @param Participant $participant |
|||
* @param Poll $poll |
|||
* @param int[] $optionIds Options the user voted for |
|||
* @return Vote[] |
|||
* @throws \RuntimeException |
|||
*/ |
|||
public function votePoll(Participant $participant, Poll $poll, array $optionIds): array { |
|||
$numVotes = count($optionIds); |
|||
if ($numVotes !== count(array_unique($optionIds))) { |
|||
throw new \UnexpectedValueException(); |
|||
} |
|||
|
|||
if ($poll->getMaxVotes() !== Poll::MAX_VOTES_UNLIMITED |
|||
&& $poll->getMaxVotes() < $numVotes) { |
|||
throw new \OverflowException(); |
|||
} |
|||
|
|||
$maxOptionId = max(array_keys(json_decode($poll->getOptions(), true, 512, JSON_THROW_ON_ERROR))); |
|||
$maxVotedId = max($optionIds); |
|||
$minVotedId = min($optionIds); |
|||
if ($minVotedId < 0 || $maxVotedId > $maxOptionId) { |
|||
throw new \RangeException(); |
|||
} |
|||
|
|||
$votes = []; |
|||
$result = json_decode($poll->getVotes(), true); |
|||
|
|||
$previousVotes = $this->voteMapper->findByPollIdForActor( |
|||
$poll->getId(), |
|||
$participant->getAttendee()->getActorType(), |
|||
$participant->getAttendee()->getActorId() |
|||
); |
|||
|
|||
$numVoters = $poll->getNumVoters(); |
|||
if ($previousVotes && $numVoters > 0) { |
|||
$numVoters--; |
|||
} |
|||
|
|||
foreach ($previousVotes as $vote) { |
|||
$result[$vote->getOptionId()] ??= 1; |
|||
$result[$vote->getOptionId()] -= 1; |
|||
} |
|||
|
|||
$this->connection->beginTransaction(); |
|||
try { |
|||
$this->voteMapper->deleteVotesByActor( |
|||
$poll->getId(), |
|||
$participant->getAttendee()->getActorType(), |
|||
$participant->getAttendee()->getActorId() |
|||
); |
|||
|
|||
if (!empty($optionIds)) { |
|||
$numVoters++; |
|||
} |
|||
|
|||
foreach ($optionIds as $optionId) { |
|||
$vote = new Vote(); |
|||
$vote->setPollId($poll->getId()); |
|||
$vote->setRoomId($poll->getRoomId()); |
|||
$vote->setActorType($participant->getAttendee()->getActorType()); |
|||
$vote->setActorId($participant->getAttendee()->getActorId()); |
|||
$vote->setDisplayName($participant->getAttendee()->getDisplayName()); |
|||
$vote->setOptionId($optionId); |
|||
$this->voteMapper->insert($vote); |
|||
|
|||
$result[$optionId] ??= 0; |
|||
$result[$optionId] += 1; |
|||
$votes[] = $vote; |
|||
} |
|||
} catch (\Exception $e) { |
|||
$this->connection->rollBack(); |
|||
throw $e; |
|||
} |
|||
$this->connection->commit(); |
|||
|
|||
$this->updateResultCache($poll->getId()); |
|||
$result = array_filter($result); |
|||
$poll->setVotes(json_encode($result)); |
|||
$poll->setNumVoters($numVoters); |
|||
|
|||
return $votes; |
|||
} |
|||
|
|||
public function updateResultCache(int $pollId): void { |
|||
$resultQuery = $this->connection->getQueryBuilder(); |
|||
$resultQuery->selectAlias( |
|||
$resultQuery->func()->concat( |
|||
$resultQuery->expr()->literal('"'), |
|||
'option_id', |
|||
$resultQuery->expr()->literal('":'), |
|||
$resultQuery->func()->count('id') |
|||
), |
|||
'colonseparatedvalue' |
|||
) |
|||
->from('talk_poll_votes') |
|||
->where($resultQuery->expr()->eq('poll_id', $resultQuery->createNamedParameter($pollId))) |
|||
->groupBy('option_id') |
|||
->orderBy('option_id', 'ASC'); |
|||
|
|||
$jsonQuery = $this->connection->getQueryBuilder(); |
|||
$jsonQuery |
|||
->selectAlias( |
|||
$jsonQuery->func()->concat( |
|||
$jsonQuery->expr()->literal('{'), |
|||
$jsonQuery->func()->groupConcat('colonseparatedvalue'), |
|||
$jsonQuery->expr()->literal('}') |
|||
), |
|||
'json' |
|||
) |
|||
->from($jsonQuery->createFunction('(' . $resultQuery->getSQL() . ')'), 'json'); |
|||
|
|||
$subQuery = $this->connection->getQueryBuilder(); |
|||
$subQuery->select('actor_type', 'actor_id') |
|||
->from('talk_poll_votes') |
|||
->where($subQuery->expr()->eq('poll_id', $subQuery->createNamedParameter($pollId))) |
|||
->groupBy('actor_type', 'actor_id'); |
|||
|
|||
$votersQuery = $this->connection->getQueryBuilder(); |
|||
$votersQuery->select($votersQuery->func()->count('*')) |
|||
->from($votersQuery->createFunction('(' . $subQuery->getSQL() . ')'), 'voters'); |
|||
|
|||
$update = $this->connection->getQueryBuilder(); |
|||
$update->update('talk_polls') |
|||
->set('votes', $jsonQuery->createFunction('(' . $jsonQuery->getSQL() . ')')) |
|||
->set('num_voters', $jsonQuery->createFunction('(' . $votersQuery->getSQL() . ')')) |
|||
->where($update->expr()->eq('id', $update->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))); |
|||
|
|||
$this->connection->beginTransaction(); |
|||
try { |
|||
$update->executeStatement(); |
|||
} catch (\Exception $e) { |
|||
$this->connection->rollBack(); |
|||
throw $e; |
|||
} |
|||
$this->connection->commit(); |
|||
} |
|||
|
|||
public function deleteByRoomId(int $roomId): void { |
|||
$this->voteMapper->deleteByRoomId($roomId); |
|||
$this->pollMapper->deleteByRoomId($roomId); |
|||
} |
|||
|
|||
public function updateDisplayNameForActor(string $actorType, string $actorId, string $displayName): void { |
|||
$update = $this->connection->getQueryBuilder(); |
|||
$update->update('talk_polls') |
|||
->set('display_name', $update->createNamedParameter($displayName)) |
|||
->where($update->expr()->eq('actor_type', $update->createNamedParameter($actorType))) |
|||
->andWhere($update->expr()->eq('actor_id', $update->createNamedParameter($actorId))); |
|||
$update->executeStatement(); |
|||
|
|||
$update = $this->connection->getQueryBuilder(); |
|||
$update->update('talk_poll_votes') |
|||
->set('display_name', $update->createNamedParameter($displayName)) |
|||
->where($update->expr()->eq('actor_type', $update->createNamedParameter($actorType))) |
|||
->andWhere($update->expr()->eq('actor_id', $update->createNamedParameter($actorId))); |
|||
$update->executeStatement(); |
|||
} |
|||
} |
|||
@ -0,0 +1,525 @@ |
|||
Feature: chat/poll |
|||
Background: |
|||
Given user "participant1" exists |
|||
Given user "participant2" exists |
|||
|
|||
Scenario: Create a public poll without max votes limit |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" sees the following messages in room "room" with 200 |
|||
| room | actorType | actorId | actorDisplayName | message | messageParameters | |
|||
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} | |
|||
Then user "participant1" sees poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | not voted | |
|||
Then user "participant1" votes for options "[1]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant1" sees poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant2" sees poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | not voted | |
|||
Then user "participant1" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | closed | |
|||
| votedSelf | [1] | |
|||
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":1}] | |
|||
Then user "participant2" sees poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | closed | |
|||
| votedSelf | not voted | |
|||
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":1}] | |
|||
|
|||
Scenario: Participants can update their votes but only while open |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [0] | |
|||
Then user "participant1" votes for options "[1]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant1" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | closed | |
|||
| votedSelf | [1] | |
|||
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":1}] | |
|||
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 400 |
|||
|
|||
Scenario: Participants can only vote for valid options |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[-1]" on poll "What is the question?" in room "room" with 400 |
|||
Then user "participant1" votes for options "[2]" on poll "What is the question?" in room "room" with 400 |
|||
|
|||
Scenario: Participants can not exceed the maxVotes |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | 1 | |
|||
Then user "participant1" votes for options "[0,1]" on poll "What is the question?" in room "room" with 400 |
|||
|
|||
Scenario: Participants can vote for multiple options |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[0,1]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1,"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [0,1] | |
|||
|
|||
Scenario: Participants can not vote for the same option multiple times |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[1,1]" on poll "What is the question?" in room "room" with 400 |
|||
|
|||
Scenario: Non-moderators can also create polls and close it themselves |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant2" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" sees the following messages in room "room" with 200 |
|||
| room | actorType | actorId | actorDisplayName | message | messageParameters | |
|||
| room | users | participant2 | participant2-displayname | {object} | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} | |
|||
Then user "participant2" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant2 | |
|||
| actorDisplayName | participant2-displayname | |
|||
| status | closed | |
|||
| votedSelf | not voted | |
|||
| details | {} | |
|||
|
|||
Scenario: Non-moderators can note create polls without chat permission |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
# Removing chat permission only |
|||
Then user "participant1" sets permissions for "participant2" in room "room" to "CSJLAVP" with 200 (v4) |
|||
When user "participant2" creates a poll in room "room" with 403 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
|
|||
Scenario: Moderators can close polls of others |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant2" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant2 | |
|||
| actorDisplayName | participant2-displayname | |
|||
| status | closed | |
|||
| votedSelf | not voted | |
|||
| details | {} | |
|||
|
|||
Scenario: There are system messages for opening, voting and closing on public polls |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [0] | |
|||
Then user "participant2" votes for options "[1]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1,"1":1} | |
|||
| numVoters | 2 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant1" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1,"1":1} | |
|||
| numVoters | 2 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | closed | |
|||
| votedSelf | [0] | |
|||
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":0},{"actorType":"users","actorId":"participant2","actorDisplayName":"participant2-displayname","optionId":1}] | |
|||
Then user "participant1" sees the following system messages in room "room" with 200 (v1) |
|||
| room | actorType | actorId | actorDisplayName | systemMessage | |
|||
| room | users | participant1 | participant1-displayname | poll_closed | |
|||
| room | users | participant2 | participant2-displayname | poll_voted | |
|||
| room | users | participant1 | participant1-displayname | poll_voted | |
|||
| room | users | participant1 | participant1-displayname | user_added | |
|||
| room | users | participant1 | participant1-displayname | conversation_created | |
|||
Then user "participant1" sees the following messages in room "room" with 200 (v1) |
|||
| room | actorType | actorId | actorDisplayName | message | messageParameters | |
|||
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} | |
|||
|
|||
|
|||
Scenario: There are only system messages for opening and closing on hidden polls |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [0] | |
|||
Then user "participant2" votes for options "[1]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant1" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1,"1":1} | |
|||
| numVoters | 2 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | closed | |
|||
| votedSelf | [0] | |
|||
Then user "participant1" sees the following system messages in room "room" with 200 (v1) |
|||
| room | actorType | actorId | actorDisplayName | systemMessage | |
|||
| room | users | participant1 | participant1-displayname | poll_closed | |
|||
| room | users | participant1 | participant1-displayname | user_added | |
|||
| room | users | participant1 | participant1-displayname | conversation_created | |
|||
Then user "participant1" sees the following messages in room "room" with 200 (v1) |
|||
| room | actorType | actorId | actorDisplayName | message | messageParameters | |
|||
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} | |
|||
|
|||
Scenario: Non-moderators can not close polls of others |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant2" closes poll "What is the question?" in room "room" with 403 |
|||
|
|||
Scenario: Votes and details are not accessible in hidden result mode |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" sees the following messages in room "room" with 200 |
|||
| room | actorType | actorId | actorDisplayName | message | messageParameters | |
|||
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} | |
|||
Then user "participant2" votes for options "[1]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant1" sees poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | not voted | |
|||
Then user "participant2" sees poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | [] | |
|||
| numVoters | 0 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant1" closes poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | hidden | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | closed | |
|||
| votedSelf | not voted | |
|||
|
|||
Scenario: Number of voters and votes are restricted to the very same poll |
|||
Given user "participant1" creates room "room" (v4) |
|||
| roomType | 2 | |
|||
| roomName | room | |
|||
When user "participant1" adds user "participant2" to room "room" with 200 (v4) |
|||
When user "participant1" creates a poll in room "room" with 201 |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200 |
|||
| id | POLL_ID(What is the question?) | |
|||
| question | What is the question? | |
|||
| options | ["Where are you?","How much is the fish?"] | |
|||
| votes | {"0":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant1 | |
|||
| actorDisplayName | participant1-displayname | |
|||
| status | open | |
|||
| votedSelf | [0] | |
|||
When user "participant2" creates a poll in room "room" with 201 |
|||
| question | Another one ... | |
|||
| options | ["... bites the dust!","... bites de_dust!"] | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
Then user "participant2" votes for options "[1]" on poll "Another one ..." in room "room" with 200 |
|||
| id | POLL_ID(Another one ...) | |
|||
| question | Another one ... | |
|||
| options | ["... bites the dust!","... bites de_dust!"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant2 | |
|||
| actorDisplayName | participant2-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Then user "participant2" sees poll "Another one ..." in room "room" with 200 |
|||
| id | POLL_ID(Another one ...) | |
|||
| question | Another one ... | |
|||
| options | ["... bites the dust!","... bites de_dust!"] | |
|||
| votes | {"1":1} | |
|||
| numVoters | 1 | |
|||
| resultMode | public | |
|||
| maxVotes | unlimited | |
|||
| actorType | users | |
|||
| actorId | participant2 | |
|||
| actorDisplayName | participant2-displayname | |
|||
| status | open | |
|||
| votedSelf | [1] | |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue