Browse Source

feat(polls): Split draft model from normal polls

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/13506/head
Joas Schilling 1 year ago
parent
commit
e27449d302
No known key found for this signature in database GPG Key ID: F72FA5B49FFA96B0
  1. 7
      lib/Controller/PollController.php
  2. 5
      lib/Federation/Proxy/TalkV1/Controller/PollController.php
  3. 8
      lib/Federation/Proxy/TalkV1/UserConverter.php
  4. 18
      lib/Model/Poll.php
  5. 3
      lib/Model/PollMapper.php
  6. 9
      lib/ResponseDefinitions.php
  7. 62
      openapi-full.json
  8. 62
      openapi.json
  9. 20
      src/types/openapi/openapi-full.ts
  10. 20
      src/types/openapi/openapi.ts
  11. 41
      tests/integration/features/bootstrap/FeatureContext.php
  12. 23
      tests/integration/features/chat-3/poll.feature

7
lib/Controller/PollController.php

@ -35,6 +35,7 @@ use Psr\Log\LoggerInterface;
/**
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
*/
class PollController extends AEnvironmentAwareController {
@ -134,7 +135,7 @@ class PollController extends AEnvironmentAwareController {
*
* Required capability: `talk-polls-drafts`
*
* @return DataResponse<Http::STATUS_OK, list<TalkPoll>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
* @return DataResponse<Http::STATUS_OK, list<TalkPollDraft>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
*
* 200: Poll returned
* 403: User is not a moderator
@ -153,7 +154,7 @@ class PollController extends AEnvironmentAwareController {
$polls = $this->pollService->getDraftsForRoom($this->room->getId());
$data = [];
foreach ($polls as $poll) {
$data[] = $this->renderPoll($poll);
$data[] = $poll->renderAsDraft();
}
return new DataResponse($data);
@ -346,7 +347,7 @@ class PollController extends AEnvironmentAwareController {
* @throws JsonException
*/
protected function renderPoll(Poll $poll, array $votedSelf = [], array $detailedVotes = []): array {
$data = $poll->asArray();
$data = $poll->renderAsPoll();
$canSeeSummary = !empty($votedSelf) && $poll->getResultMode() === Poll::MODE_PUBLIC;

5
lib/Federation/Proxy/TalkV1/Controller/PollController.php

@ -20,6 +20,7 @@ use OCP\AppFramework\Http\DataResponse;
/**
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
*/
class PollController {
public function __construct(
@ -29,7 +30,7 @@ class PollController {
}
/**
* @return DataResponse<Http::STATUS_OK, list<TalkPoll>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
* @return DataResponse<Http::STATUS_OK, list<TalkPollDraft>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
* @throws CannotReachRemoteException
*
* 200: Polls returned
@ -49,7 +50,7 @@ class PollController {
return new DataResponse([], $status);
}
/** @var list<TalkPoll> $list */
/** @var list<TalkPollDraft> $list */
$list = $this->proxy->getOCSData($proxy);
$data = [];

8
lib/Federation/Proxy/TalkV1/UserConverter.php

@ -18,6 +18,7 @@ use OCA\Talk\Service\ParticipantService;
/**
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
* @psalm-import-type TalkReaction from ResponseDefinitions
*/
class UserConverter {
@ -137,9 +138,12 @@ class UserConverter {
}
/**
* @template T of TalkPoll|TalkPollDraft
* @param Room $room
* @param TalkPoll $poll
* @return TalkPoll
* @param TalkPoll|TalkPollDraft $poll
* @psalm-param T $poll
* @return TalkPoll|TalkPollDraft
* @psalm-return T
*/
public function convertPoll(Room $room, array $poll): array {
$poll = $this->convertAttendee($room, $poll, 'actorType', 'actorId', 'actorDisplayName');

18
lib/Model/Poll.php

@ -37,6 +37,7 @@ use OCP\AppFramework\Db\Entity;
* @method int getMaxVotes()
*
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
*/
class Poll extends Entity {
public const STATUS_OPEN = 0;
@ -75,25 +76,32 @@ class Poll extends Entity {
/**
* @return TalkPoll
*/
public function asArray(): array {
public function renderAsPoll(): array {
$data = $this->renderAsDraft();
$votes = json_decode($this->getVotes(), true, 512, JSON_THROW_ON_ERROR);
// Because PHP is turning arrays with sequent numeric keys "{"0":x,"1":y,"2":z}" into "[x,y,z]"
// when json_encode() is used we have to prefix the keys with a string,
// to prevent breaking in the mobile apps.
$prefixedVotes = [];
$data['votes'] = [];
foreach ($votes as $option => $count) {
$prefixedVotes['option-' . $option] = $count;
$data['votes']['option-' . $option] = $count;
}
$data['numVoters'] = $this->getNumVoters();
return $data;
}
/**
* @return TalkPollDraft
*/
public function renderAsDraft(): 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' => $prefixedVotes,
'numVoters' => $this->getNumVoters(),
'actorType' => $this->getActorType(),
'actorId' => $this->getActorId(),
'actorDisplayName' => $this->getDisplayName(),

3
lib/Model/PollMapper.php

@ -36,7 +36,8 @@ class PollMapper extends QBMapper {
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('status', $query->createNamedParameter(Poll::STATUS_DRAFT, IQueryBuilder::PARAM_INT)));
->andWhere($query->expr()->eq('status', $query->createNamedParameter(Poll::STATUS_DRAFT, IQueryBuilder::PARAM_INT)))
->orderBy('id', 'ASC');
return $this->findEntities($query);
}

9
lib/ResponseDefinitions.php

@ -197,18 +197,21 @@ namespace OCA\Talk;
* optionId: int,
* }
*
* @psalm-type TalkPoll = array{
* @psalm-type TalkPollDraft = array{
* actorDisplayName: string,
* actorId: string,
* actorType: string,
* details?: TalkPollVote[],
* id: int,
* maxVotes: int,
* numVoters?: int,
* options: string[],
* question: string,
* resultMode: int,
* status: int,
* }
*
* @psalm-type TalkPoll = TalkPollDraft&array{
* details?: TalkPollVote[],
* numVoters?: int,
* votedSelf?: int[],
* votes?: array<string, int>,
* }

62
openapi-full.json

@ -842,6 +842,42 @@
}
},
"Poll": {
"allOf": [
{
"$ref": "#/components/schemas/PollDraft"
},
{
"type": "object",
"properties": {
"details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PollVote"
}
},
"numVoters": {
"type": "integer",
"format": "int64"
},
"votedSelf": {
"type": "array",
"items": {
"type": "integer",
"format": "int64"
}
},
"votes": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
}
}
}
]
},
"PollDraft": {
"type": "object",
"required": [
"actorDisplayName",
@ -864,12 +900,6 @@
"actorType": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PollVote"
}
},
"id": {
"type": "integer",
"format": "int64"
@ -878,10 +908,6 @@
"type": "integer",
"format": "int64"
},
"numVoters": {
"type": "integer",
"format": "int64"
},
"options": {
"type": "array",
"items": {
@ -898,20 +924,6 @@
"status": {
"type": "integer",
"format": "int64"
},
"votedSelf": {
"type": "array",
"items": {
"type": "integer",
"format": "int64"
}
},
"votes": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
}
}
},
@ -8904,7 +8916,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Poll"
"$ref": "#/components/schemas/PollDraft"
}
}
}

62
openapi.json

@ -729,6 +729,42 @@
}
},
"Poll": {
"allOf": [
{
"$ref": "#/components/schemas/PollDraft"
},
{
"type": "object",
"properties": {
"details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PollVote"
}
},
"numVoters": {
"type": "integer",
"format": "int64"
},
"votedSelf": {
"type": "array",
"items": {
"type": "integer",
"format": "int64"
}
},
"votes": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
}
}
}
]
},
"PollDraft": {
"type": "object",
"required": [
"actorDisplayName",
@ -751,12 +787,6 @@
"actorType": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PollVote"
}
},
"id": {
"type": "integer",
"format": "int64"
@ -765,10 +795,6 @@
"type": "integer",
"format": "int64"
},
"numVoters": {
"type": "integer",
"format": "int64"
},
"options": {
"type": "array",
"items": {
@ -785,20 +811,6 @@
"status": {
"type": "integer",
"format": "int64"
},
"votedSelf": {
"type": "array",
"items": {
"type": "integer",
"format": "int64"
}
},
"votes": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
}
}
},
@ -8791,7 +8803,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Poll"
"$ref": "#/components/schemas/PollDraft"
}
}
}

20
src/types/openapi/openapi-full.ts

@ -2053,27 +2053,29 @@ export type components = {
phoneNumber?: string | null;
callId?: string | null;
};
Poll: {
Poll: components["schemas"]["PollDraft"] & {
details?: components["schemas"]["PollVote"][];
/** Format: int64 */
numVoters?: number;
votedSelf?: number[];
votes?: {
[key: string]: number;
};
};
PollDraft: {
actorDisplayName: string;
actorId: string;
actorType: string;
details?: components["schemas"]["PollVote"][];
/** Format: int64 */
id: number;
/** Format: int64 */
maxVotes: number;
/** Format: int64 */
numVoters?: number;
options: string[];
question: string;
/** Format: int64 */
resultMode: number;
/** Format: int64 */
status: number;
votedSelf?: number[];
votes?: {
[key: string]: number;
};
};
PollVote: {
actorDisplayName: string;
@ -5195,7 +5197,7 @@ export interface operations {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Poll"][];
data: components["schemas"]["PollDraft"][];
};
};
};

20
src/types/openapi/openapi.ts

@ -1534,27 +1534,29 @@ export type components = {
phoneNumber?: string | null;
callId?: string | null;
};
Poll: {
Poll: components["schemas"]["PollDraft"] & {
details?: components["schemas"]["PollVote"][];
/** Format: int64 */
numVoters?: number;
votedSelf?: number[];
votes?: {
[key: string]: number;
};
};
PollDraft: {
actorDisplayName: string;
actorId: string;
actorType: string;
details?: components["schemas"]["PollVote"][];
/** Format: int64 */
id: number;
/** Format: int64 */
maxVotes: number;
/** Format: int64 */
numVoters?: number;
options: string[];
question: string;
/** Format: int64 */
resultMode: number;
/** Format: int64 */
status: number;
votedSelf?: number[];
votes?: {
[key: string]: number;
};
};
PollVote: {
actorDisplayName: string;
@ -4676,7 +4678,7 @@ export interface operations {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Poll"][];
data: components["schemas"]["PollDraft"][];
};
};
};

41
tests/integration/features/bootstrap/FeatureContext.php

@ -2428,6 +2428,47 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
/**
* @Then /^user "([^"]*)" gets poll drafts for room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
* @param string $user
* @param string $identifier
* @param string $statusCode
* @param string $apiVersion
*/
public function getPollDrafts(string $user, string $identifier, string $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void {
$this->setCurrentUser($user);
$this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier] . '/drafts');
$this->assertStatusCode($this->response, $statusCode);
if ($statusCode !== '200') {
return;
}
$response = $this->getDataFromResponse($this->response);
$data = array_map(static function (array $poll): array {
$result = preg_match('/POLL_ID\(([^)]+)\)/', $poll['id'], $matches);
if ($result) {
$poll['id'] = self::$questionToPollId[$matches[1]];
}
$poll['resultMode'] = match($poll['resultMode']) {
'public' => 0,
'hidden' => 1,
};
$poll['status'] = match($poll['status']) {
'open' => 0,
'closed' => 1,
'draft' => 2,
};
$poll['maxVotes'] = (int)$poll['maxVotes'];
$poll['options'] = json_decode($poll['options'], true, flags: JSON_THROW_ON_ERROR);
return $poll;
}, $formData->getColumnsHash());
Assert::assertCount(count($data), $response);
Assert::assertSame($data, $response);
}
/**
* @Then /^user "([^"]*)" sees poll "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*

23
tests/integration/features/chat-3/poll.feature

@ -806,23 +806,40 @@ Feature: chat-2/poll
| room | actorType | actorId | systemMessage | message | silent | messageParameters |
| room | users | participant1 | history_cleared | You cleared the history of the conversation | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} |
Scenario: Create a public poll without max votes limit
Scenario: Drafts
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?"] |
| options | ["You","me"] |
| resultMode | public |
| maxVotes | unlimited |
| draft | 1 |
When user "participant1" creates a poll in room "room" with 201
| question | Shall we draft 2 questions? |
| options | ["Yes","No"] |
| resultMode | hidden |
| maxVotes | 1 |
| draft | 1 |
When user "participant1" creates a poll in room "room" with 201
| question | This is not a draft! |
| options | ["Yes!","Ok!"] |
| resultMode | public |
| maxVotes | 1 |
| draft | 0 |
When user "participant1" gets poll drafts for room "room" with 200
| id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes |
| POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 |
| POLL_ID(Shall we draft 2 questions?) | Shall we draft 2 questions? | ["Yes","No"] | users | participant1 | participant1-displayname | draft | hidden | 1 |
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(This is not a draft!),"name":"This is not a draft!"}} |
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?"] |
| options | ["You","me"] |
| votes | [] |
| numVoters | 0 |
| resultMode | public |

Loading…
Cancel
Save