Skip to content

Commit

Permalink
feat(polls): allow editing of draft polls
Browse files Browse the repository at this point in the history
Signed-off-by: Anna Larch <[email protected]>

# The commit message #2 will be skipped:

# fixup! feat(polls): allow editing of draft polls
  • Loading branch information
miaulalala committed Dec 9, 2024
1 parent 2af30d1 commit 0ca00dd
Show file tree
Hide file tree
Showing 16 changed files with 1,269 additions and 111 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes/routesPollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
'ocs' => [
/** @see \OCA\Talk\Controller\PollController::createPoll() */
['name' => 'Poll#createPoll', 'url' => '/api/{apiVersion}/poll/{token}', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PollController::updateDraftPoll() */
['name' => 'Poll#updateDraftPoll', 'url' => '/api/{apiVersion}/poll/{token}/draft/{pollId}', 'verb' => 'POST', 'requirements' => $requirementsWithPollId],
/** @see \OCA\Talk\Controller\PollController::getAllDraftPolls() */
['name' => 'Poll#getAllDraftPolls', 'url' => '/api/{apiVersion}/poll/{token}/drafts', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PollController::showPoll() */
Expand Down
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
* `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation
* `config => call => max-duration` - Integer, maximum call duration in seconds. Please note that this should only be used with system cron and with a reasonable high value, due to the expended duration until the background job ran.
* `config => call => blur-virtual-background` (local) - Boolean, whether blur background is set by default when joining a conversation
+ `edit-draft-poll` - Whether moderators can edit draft polls

## 21
* `config => conversations => force-passwords` - Whether passwords are enforced for public rooms
Expand Down
25 changes: 25 additions & 0 deletions docs/poll.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`

See [Poll data](#poll-data)

# Edit a draft poll in a conversation

* Required capability: `edit-draft-poll`
* Method: `POST`
* Endpoint: `/poll/{token}/draft/{pollId}`
* 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:
+ `200 OK`
+ `400 Bad Request` Modifying poll is not possible
+ `403 Forbidden` No permission to modify this poll
+ `404 Not Found` When the draft poll could not be found
- Data:

See [Poll data](#poll-data)

## Get state or result of a poll

* Federation capability: `federation-v1`
Expand Down
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class Capabilities implements IPublicCapability {
'email-csv-import',
'conversation-creation-password',
'call-notification-state-api',
'edit-draft-poll',
];

public const CONDITIONAL_FEATURES = [
Expand Down
91 changes: 81 additions & 10 deletions lib/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function __construct(
* @psalm-param Poll::MODE_* $resultMode Mode how the results will be shown
* @param int $maxVotes Number of maximum votes per voter
* @param bool $draft Whether the poll should be saved as a draft (only allowed for moderators and with `talk-polls-drafts` capability)
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'question'|'room'}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
*
* 200: Draft created successfully
* 201: Poll created successfully
Expand Down Expand Up @@ -104,7 +104,7 @@ public function createPoll(string $question, array $options, int $resultMode, in
);
} catch (PollPropertyException $e) {
$this->logger->error('Error creating poll', ['exception' => $e]);
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST);
}

if ($draft) {
Expand Down Expand Up @@ -133,6 +133,79 @@ public function createPoll(string $question, array $options, int $resultMode, in
return new DataResponse($this->renderPoll($poll), Http::STATUS_CREATED);
}

/**
* Modify a draft poll
*
* Required capability: `edit-draft-poll`
*
* @param int $pollId The poll id
* @param string $question Question of the poll
* @param string[] $options Options of the poll
* @psalm-param list<string> $options
* @param 0|1 $resultMode Mode how the results will be shown
* @psalm-param Poll::MODE_* $resultMode Mode how the results will be shown
* @param int $maxVotes Number of maximum votes per voter
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN, array{error: 'draft'|'options'|'question'|'room'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: Draft modified successfully
* 400: Modifying poll is not possible
* 403: No permission to modify this poll
* 404: No draft poll exists
*/
#[FederationSupported]
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequireReadWriteConversation]
public function updateDraftPoll(int $pollId, string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
if ($this->room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class);
return $proxy->updateDraftPoll($pollId, $this->room, $this->participant, $question, $options, $resultMode, $maxVotes);
}

if ($this->room->getType() !== Room::TYPE_GROUP
&& $this->room->getType() !== Room::TYPE_PUBLIC) {
return new DataResponse(['error' => PollPropertyException::REASON_ROOM], Http::STATUS_BAD_REQUEST);
}

try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (DoesNotExistException $e) {
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}

if (!$poll->isDraft()) {
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST);
}

if (!$this->participant->hasModeratorPermissions()
&& ($poll->getActorType() !== $this->participant->getAttendee()->getActorType()
|| $poll->getActorId() !== $this->participant->getAttendee()->getActorId())) {
return new DataResponse(['error' => PollPropertyException::REASON_DRAFT], Http::STATUS_BAD_REQUEST);
}

try {
$poll->setQuestion($question);
$poll->setOptions($options);
$poll->setResultMode($resultMode);
$poll->setMaxVotes($maxVotes);
} catch (PollPropertyException $e) {
$this->logger->error('Error modifying poll', ['exception' => $e]);
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
}

try {
$this->pollService->updatePoll($this->participant, $poll);
} catch (WrongPermissionsException $e) {
$this->logger->error('Error modifying poll', ['exception' => $e]);
return new DataResponse(['error' => 'poll'], Http::STATUS_FORBIDDEN);
}

return new DataResponse($poll->renderAsDraft());
}

/**
* Get all drafted polls
*
Expand Down Expand Up @@ -273,7 +346,7 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse {
*
* @param int $pollId ID of the poll
* @psalm-param non-negative-int $pollId
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'poll'}|array{error: string}, array{}>
*
* 200: Poll closed successfully
* 202: Poll draft was deleted successfully
Expand All @@ -295,7 +368,7 @@ public function closePoll(int $pollId): DataResponse {
try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (DoesNotExistException) {
return new DataResponse(['error' => 'poll'], Http::STATUS_NOT_FOUND);
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_NOT_FOUND);
}

if ($poll->getStatus() === Poll::STATUS_DRAFT) {
Expand All @@ -304,15 +377,13 @@ public function closePoll(int $pollId): DataResponse {
}

if ($poll->getStatus() === Poll::STATUS_CLOSED) {
return new DataResponse(['error' => 'poll'], Http::STATUS_BAD_REQUEST);
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST);
}

$poll->setStatus(Poll::STATUS_CLOSED);

try {
$this->pollService->updatePoll($this->participant, $poll);
} catch (WrongPermissionsException) {
return new DataResponse(['error' => 'poll'], Http::STATUS_FORBIDDEN);
$this->pollService->closePoll($this->participant, $poll);
} catch (WrongPermissionsException $e) {
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_FORBIDDEN);
}

$attendee = $this->participant->getAttendee();
Expand Down
1 change: 1 addition & 0 deletions lib/Exceptions/PollPropertyException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

class PollPropertyException extends \InvalidArgumentException {
public const REASON_DRAFT = 'draft';
public const REASON_POLL = 'poll';
public const REASON_QUESTION = 'question';
public const REASON_OPTIONS = 'options';
public const REASON_ROOM = 'room';
Expand Down
44 changes: 42 additions & 2 deletions lib/Federation/Proxy/TalkV1/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;

use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\PollPropertyException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Participant;
Expand Down Expand Up @@ -171,7 +172,46 @@ public function createPoll(Room $room, Participant $participant, string $questio
}

/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'question'|'room'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Draft created successfully
* 201: Poll created successfully
* 400: Creating poll is not possible
*
* @see \OCA\Talk\Controller\PollController::createPoll()
*/
public function updateDraftPoll(int $pollId, Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/draft/' . $pollId,
[
'question' => $question,
'options' => $options,
'resultMode' => $resultMode,
'maxVotes' => $maxVotes
],
);

$status = $proxy->getStatusCode();
if ($status === Http::STATUS_BAD_REQUEST) {
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
return new DataResponse($data, Http::STATUS_BAD_REQUEST);
}

/** @var TalkPollDraft $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_CREATED]);
$data = $this->userConverter->convertPoll($room, $data);

if ($status === Http::STATUS_OK) {
return new DataResponse($data);
}
return new DataResponse($data);
}

/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'poll'}|array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Poll closed successfully
Expand Down Expand Up @@ -199,7 +239,7 @@ public function closePoll(Room $room, Participant $participant, int $pollId): Da
}
/** @var array{error?: string} $data */
$data = $this->proxy->getOCSData($proxy);
return new DataResponse(['error' => $data['error'] ?? 'poll'], $statusCode);
return new DataResponse(['error' => $data['error'] ?? PollPropertyException::REASON_POLL], $statusCode);
}

/** @var TalkPoll $data */
Expand Down
56 changes: 54 additions & 2 deletions lib/Model/Poll.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Talk\Model;

use OCA\Talk\Exceptions\PollPropertyException;
use OCA\Talk\ResponseDefinitions;
use OCP\AppFramework\Db\Entity;
use OCP\DB\Types;
Expand All @@ -18,10 +19,8 @@
* @method void setRoomId(int $roomId)
* @method int getRoomId()
* @psalm-method int<1, max> getRoomId()
* @method void setQuestion(string $question)
* @method string getQuestion()
* @psalm-method non-empty-string getQuestion()
* @method void setOptions(string $options)
* @method string getOptions()
* @method void setVotes(string $votes)
* @method string getVotes()
Expand Down Expand Up @@ -121,4 +120,57 @@ public function renderAsDraft(): array {
'maxVotes' => $this->getMaxVotes(),
];
}

public function isDraft(): bool {
return $this->getStatus() === self::STATUS_DRAFT;
}

/**
* @param array $options
* @return void
* @throws PollPropertyException
*/
public function setOptions(array $options): void {
try {
$jsonOptions = json_encode($options, JSON_THROW_ON_ERROR, 1);
} catch (\Exception) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}

$validOptions = [];
foreach ($options as $option) {
if (!is_string($option)) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}

$option = trim($option);
if ($option !== '') {
$validOptions[] = $option;
}
}

if (count($validOptions) < 2) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}

if (strlen($jsonOptions) > 60_000) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}

$this->setter('options', [$jsonOptions]);
}

/**
* @param string $question
* @return void
* @throws PollPropertyException
*/
public function setQuestion(string $question): void {
$question = trim($question);
if ($question === '' || strlen($question) > 32_000) {
throw new PollPropertyException(PollPropertyException::REASON_QUESTION);
}

$this->setter('question', [$question]);
}
}
5 changes: 3 additions & 2 deletions lib/Model/PollMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ public function getDraftsByRoomId(int $roomId): array {
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function getByPollId(int $pollId): Poll {
public function getPollByRoomIdAndPollId(int $roomId, int $pollId): Poll {
$query = $this->db->getQueryBuilder();

$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)));
->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)));

return $this->findEntity($query);
}
Expand Down
Loading

0 comments on commit 0ca00dd

Please sign in to comment.