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