Skip to content

Commit 2879b2e

Browse files
committed
Initial poll implementation
- add the ability to add a poll to Entry, EntryComment, Post and PostComment - Polls: - can be multiple choice - can contain an unlimited number of choices - can expire - Add a hidden form for the poll to all create forms, the form appears when a checkbox is checked - The ActivityPub implementation is the same as on Mastodon: https://docs.joinmastodon.org/spec/activitypub/#Question - Add tests for the ActivityPub implementation and the Api controller - The api had to be shifted a bit -> the `serialize...` functions in the base api take only the entity classes now instead of their dto variant -> touched a lot of api classes because of this
1 parent e2f7c84 commit 2879b2e

170 files changed

Lines changed: 4359 additions & 193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎assets/styles/app.scss‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
@use 'components/inline_md';
5252
@use 'components/emoji_picker';
5353
@use 'components/filter_list';
54+
@use 'components/poll';
5455
@use 'pages/post_single';
5556
@use 'pages/post_front';
5657
@use 'pages/page_bookmarks';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.poll {
2+
margin: auto;
3+
4+
form {
5+
margin: auto;
6+
max-width: 400px;
7+
}
8+
9+
.poll-result {
10+
margin: .5rem 0;
11+
}
12+
13+
.choice-result {
14+
margin-bottom: .5rem;
15+
16+
.choice-bar {
17+
background-color: var(--kbin-button-primary-bg);
18+
color: var(--kbin-button-primary-text-color);
19+
height: .5rem;
20+
border: var(--kbin-button-primary-border);
21+
border-radius: var(--kbin-rounded-edges-radius);
22+
}
23+
}
24+
}

‎assets/styles/components/_post.scss‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
div {
2424
margin-bottom: 0;
2525
}
26+
27+
.poll-area div {
28+
margin-bottom: 1rem;
29+
}
2630
}
2731

2832
.post-container {

‎assets/styles/layout/_forms.scss‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ form {
258258
flex-direction: row-reverse;
259259
justify-content: flex-end;
260260

261-
input[type=checkbox] {
261+
input[type=checkbox],
262+
input[type=radio] {
262263
margin-right: .5rem;
263264
}
264265
}
@@ -574,3 +575,18 @@ div.input-box {
574575
width: 100%;
575576

576577
}
578+
579+
.poll-area {
580+
.sub-form {
581+
display: none;
582+
}
583+
&:has(input:checked) .sub-form {
584+
display: block;
585+
}
586+
}
587+
588+
form {
589+
.collection-container.hide-index .existing-collection-items > div > label {
590+
display: none;
591+
}
592+
}

‎config/mbin_routes/activity_pub.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,9 @@ ap_contexts:
162162
path: /contexts.{_format}
163163
methods: [GET]
164164
format: jsonld
165+
166+
ap_poll_vote:
167+
controller: App\Controller\ActivityPub\PollVoteController
168+
path: /u/{username}/votes/{uuid}
169+
methods: [GET]
170+
condition: '%kbin_ap_route_condition%'

‎config/mbin_routes/poll.yaml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
poll_vote:
2+
controller: App\Controller\PollVoteController::vote
3+
path: /poll/{id}/vote
4+
methods: [GET]

‎config/mbin_routes/poll_api.yaml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
api_poll_vote_entry:
2+
controller: App\Controller\Api\Poll\PollVoteController::voteOnEntry
3+
path: /api/entry/{entryId}/poll/vote
4+
methods: [ PUT ]
5+
6+
api_poll_vote_entry_comment:
7+
controller: App\Controller\Api\Poll\PollVoteController::voteOnEntryComment
8+
path: /api/entry/{entryId}/comments/{commentId}/poll/vote
9+
methods: [ PUT ]
10+
11+
api_poll_vote_post:
12+
controller: App\Controller\Api\Poll\PollVoteController::voteOnPost
13+
path: /api/post/{postId}/poll/vote
14+
methods: [ PUT ]
15+
16+
api_poll_vote_post_comment:
17+
controller: App\Controller\Api\Poll\PollVoteController::voteOnPostComment
18+
path: /api/post/{postId}/comments/{commentId}/poll/vote
19+
methods: [ PUT ]

‎docs/05-fediverse_developers/README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,30 @@ Each magazine has an AP actor at `https://instance.tld/m/name`:
8484
%object_message%
8585
```
8686

87+
### Polls
88+
89+
Polls can be part of all of the above, except private messages. If that is the case, their `type` becomes `Question`.
90+
The deciding factor if an object is a thread or microblog in that case is the existence of the `title` property.
91+
92+
Mbin's representation of polls are mostly the same as Mastodons (see [Mastodon's documentation](https://docs.joinmastodon.org/spec/activitypub/#Question)):
93+
94+
- Poll options are part of the `oneOf` (for single choice polls) or the `anyOf` property (for multiple choice polls)
95+
- Each option has a `name` property, which is both the value and the label of this option
96+
- Each option has a `replies` property containing the amount of votes in their `totalItems` property
97+
98+
```json
99+
%object_poll%
100+
```
101+
102+
#### Poll Votes
103+
104+
A vote in a poll is represented as a `Note` object with a `name` property containing the exact value of the choice that is voted for.
105+
For multiple-choice-polls, multiple activities will be sent, each containing one vote for one choice.
106+
107+
```json
108+
%object_poll_vote%
109+
```
110+
87111
## Collections
88112

89113
### User Outbox
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20260408134939 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'Create initial poll table and relations';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
$this->addSql('CREATE SEQUENCE poll_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
20+
$this->addSql('CREATE SEQUENCE poll_choice_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
21+
$this->addSql('CREATE TABLE poll (id INT NOT NULL, multiple_choice BOOLEAN NOT NULL, voter_count INT DEFAULT 0 NOT NULL, end_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_remote BOOLEAN NOT NULL, created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL, sent_notifications BOOLEAN NOT NULL, PRIMARY KEY (id))');
22+
$this->addSql('CREATE TABLE poll_choice (id INT NOT NULL, name VARCHAR(255) NOT NULL, vote_count INT NOT NULL, poll_id INT NOT NULL, PRIMARY KEY (id))');
23+
$this->addSql('CREATE INDEX IDX_2DAE19C93C947C0F ON poll_choice (poll_id)');
24+
$this->addSql('CREATE TABLE poll_vote (uuid UUID NOT NULL, created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL, ap_id VARCHAR(255) DEFAULT NULL, voter_id INT NOT NULL, choice_id INT NOT NULL, poll_id INT NOT NULL, PRIMARY KEY (uuid))');
25+
$this->addSql('CREATE UNIQUE INDEX UNIQ_ED568EBE904F155E ON poll_vote (ap_id)');
26+
$this->addSql('CREATE INDEX IDX_ED568EBEEBB4B8AD ON poll_vote (voter_id)');
27+
$this->addSql('CREATE INDEX IDX_ED568EBE998666D1 ON poll_vote (choice_id)');
28+
$this->addSql('CREATE INDEX IDX_ED568EBE3C947C0F ON poll_vote (poll_id)');
29+
$this->addSql('ALTER TABLE poll_choice ADD CONSTRAINT FK_2DAE19C93C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id) ON DELETE CASCADE NOT DEFERRABLE');
30+
$this->addSql('ALTER TABLE poll_vote ADD CONSTRAINT FK_ED568EBEEBB4B8AD FOREIGN KEY (voter_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE');
31+
$this->addSql('ALTER TABLE poll_vote ADD CONSTRAINT FK_ED568EBE998666D1 FOREIGN KEY (choice_id) REFERENCES poll_choice (id) ON DELETE CASCADE NOT DEFERRABLE');
32+
$this->addSql('ALTER TABLE poll_vote ADD CONSTRAINT FK_ED568EBE3C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id) ON DELETE CASCADE NOT DEFERRABLE');
33+
$this->addSql('ALTER TABLE activity ADD object_poll_vote_id UUID DEFAULT NULL');
34+
$this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A69F3DEA4 FOREIGN KEY (object_poll_vote_id) REFERENCES poll_vote (uuid) ON DELETE CASCADE NOT DEFERRABLE');
35+
$this->addSql('CREATE INDEX IDX_AC74095A69F3DEA4 ON activity (object_poll_vote_id)');
36+
$this->addSql('ALTER TABLE entry ADD poll_id INT DEFAULT NULL');
37+
$this->addSql('ALTER TABLE entry ADD CONSTRAINT FK_2B219D703C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id)');
38+
$this->addSql('CREATE UNIQUE INDEX UNIQ_2B219D703C947C0F ON entry (poll_id)');
39+
$this->addSql('ALTER TABLE entry_comment ADD poll_id INT DEFAULT NULL');
40+
$this->addSql('ALTER TABLE entry_comment ADD CONSTRAINT FK_B892FDFB3C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id)');
41+
$this->addSql('CREATE UNIQUE INDEX UNIQ_B892FDFB3C947C0F ON entry_comment (poll_id)');
42+
$this->addSql('ALTER TABLE post ADD poll_id INT DEFAULT NULL');
43+
$this->addSql('ALTER TABLE post ADD CONSTRAINT FK_5A8A6C8D3C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id)');
44+
$this->addSql('CREATE UNIQUE INDEX UNIQ_5A8A6C8D3C947C0F ON post (poll_id)');
45+
$this->addSql('ALTER TABLE post_comment ADD poll_id INT DEFAULT NULL');
46+
$this->addSql('ALTER TABLE post_comment ADD CONSTRAINT FK_A99CE55F3C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id)');
47+
$this->addSql('CREATE UNIQUE INDEX UNIQ_A99CE55F3C947C0F ON post_comment (poll_id)');
48+
$this->addSql('ALTER TABLE notification ADD poll_id INT DEFAULT NULL');
49+
$this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA3C947C0F FOREIGN KEY (poll_id) REFERENCES poll (id) ON DELETE CASCADE NOT DEFERRABLE');
50+
$this->addSql('CREATE INDEX IDX_BF5476CA3C947C0F ON notification (poll_id)');
51+
}
52+
53+
public function down(Schema $schema): void
54+
{
55+
$this->addSql('DROP SEQUENCE poll_id_seq CASCADE');
56+
$this->addSql('DROP SEQUENCE poll_choice_id_seq CASCADE');
57+
$this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A69F3DEA4');
58+
$this->addSql('DROP INDEX IDX_AC74095A69F3DEA4');
59+
$this->addSql('ALTER TABLE activity DROP object_poll_vote_id');
60+
$this->addSql('ALTER TABLE entry DROP CONSTRAINT FK_2B219D703C947C0F');
61+
$this->addSql('DROP INDEX UNIQ_2B219D703C947C0F');
62+
$this->addSql('ALTER TABLE entry DROP poll_id');
63+
$this->addSql('ALTER TABLE entry_comment DROP CONSTRAINT FK_B892FDFB3C947C0F');
64+
$this->addSql('DROP INDEX UNIQ_B892FDFB3C947C0F');
65+
$this->addSql('ALTER TABLE entry_comment DROP poll_id');
66+
$this->addSql('ALTER TABLE post DROP CONSTRAINT FK_5A8A6C8D3C947C0F');
67+
$this->addSql('DROP INDEX UNIQ_5A8A6C8D3C947C0F');
68+
$this->addSql('ALTER TABLE post DROP poll_id');
69+
$this->addSql('ALTER TABLE post_comment DROP CONSTRAINT FK_A99CE55F3C947C0F');
70+
$this->addSql('ALTER TABLE notification DROP CONSTRAINT FK_BF5476CA3C947C0F');
71+
$this->addSql('DROP INDEX IDX_BF5476CA3C947C0F');
72+
$this->addSql('ALTER TABLE notification DROP poll_id');
73+
$this->addSql('DROP INDEX UNIQ_A99CE55F3C947C0F');
74+
$this->addSql('ALTER TABLE post_comment DROP poll_id');
75+
$this->addSql('ALTER TABLE poll_choice DROP CONSTRAINT FK_2DAE19C93C947C0F');
76+
$this->addSql('ALTER TABLE poll_vote DROP CONSTRAINT FK_ED568EBEEBB4B8AD');
77+
$this->addSql('ALTER TABLE poll_vote DROP CONSTRAINT FK_ED568EBE998666D1');
78+
$this->addSql('ALTER TABLE poll_vote DROP CONSTRAINT FK_ED568EBE3C947C0F');
79+
$this->addSql('DROP TABLE poll');
80+
$this->addSql('DROP TABLE poll_choice');
81+
$this->addSql('DROP TABLE poll_vote');
82+
}
83+
}

‎src/Command/DebugCommand.php‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Command;
6+
7+
use App\Repository\PollRepository;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
13+
#[AsCommand('mbin:debug')]
14+
class DebugCommand extends Command
15+
{
16+
public function __construct(
17+
private readonly PollRepository $pollRepository,
18+
) {
19+
parent::__construct();
20+
}
21+
22+
protected function execute(InputInterface $input, OutputInterface $output): int
23+
{
24+
foreach ($this->pollRepository->getAllEndedPollsToSentNotifications() as $poll) {
25+
$output->writeln("poll {$poll->getId()}");
26+
foreach ($this->pollRepository->getAllLocalVotersOfPoll($poll) as $voter) {
27+
$output->writeln("Voter $voter->username in poll {$poll->getId()}");
28+
}
29+
}
30+
31+
return 0;
32+
}
33+
}

0 commit comments

Comments
 (0)