- {((!this.poll.hasEnded() && this.poll.canVote()) || !app.session.user) && (
+ {((!poll.hasEnded() && poll.canVote()) || !app.session.user) && (
)}
@@ -111,25 +110,21 @@ export default class PostPoll extends Component {
);
return (
-
-
- {poll}
-
+
+ {canSeeVoteCount ? (
+
+ {bar}
+
+ ) : (
+ bar
+ )}
);
}
- updateData() {
- this.options = this.poll.options() || [];
- this.myVotes = this.poll.myVotes() || [];
- }
-
- onError(evt, error) {
- evt.target.checked = false;
-
- throw error;
- }
-
changeVote(option, evt) {
if (!app.session.user) {
app.modal.show(LogInModal);
@@ -137,12 +132,9 @@ export default class PostPoll extends Component {
return;
}
- // // if we click on our current vote, we want to "un-vote"
- // if (this.myVotes.some((vote) => vote.option() === option)) option = null;
-
- const optionIds = new Set(this.poll.myVotes().map((v) => v.option().id()));
+ const optionIds = new Set(this.attrs.poll.myVotes().map?.((v) => v.option().id()));
const isUnvoting = optionIds.delete(option.id());
- const allowsMultiple = this.poll.allowMultipleVotes();
+ const allowsMultiple = this.attrs.poll.allowMultipleVotes();
if (!allowsMultiple) {
optionIds.clear();
@@ -152,10 +144,13 @@ export default class PostPoll extends Component {
optionIds.add(option.id());
}
+ this.loadingOptions = true;
+ m.redraw();
+
return app
.request({
method: 'PATCH',
- url: `${app.forum.attribute('apiUrl')}/fof/polls/${this.poll.id()}/votes`,
+ url: `${app.forum.attribute('apiUrl')}/fof/polls/${this.attrs.poll.id()}/votes`,
body: {
data: {
optionIds: Array.from(optionIds),
@@ -165,28 +160,41 @@ export default class PostPoll extends Component {
.then((res) => {
app.store.pushPayload(res);
- this.updateData();
-
- m.redraw();
+ // m.redraw();
})
.catch(() => {
evt.target.checked = isUnvoting;
+ })
+ .finally(() => {
+ this.loadingOptions = false;
+
+ m.redraw();
});
}
showVoters() {
// Load all the votes only when opening the votes list
app.modal.show(ListVotersModal, {
- poll: this.poll,
+ poll: this.attrs.poll,
post: this.attrs.post,
});
}
deletePoll() {
if (confirm(app.translator.trans('fof-polls.forum.moderation.delete_confirm'))) {
- this.poll.delete().then(() => {
+ this.attrs.poll.delete().then(() => {
m.redraw.sync();
});
}
}
+
+ /**
+ * Attempting to use the `tooltipVisible` attr on the Tooltip component set to 'false' when no vote count
+ * caused the tooltip to break on click. This is a workaround to hide the tooltip when no vote count is available,
+ * called on 'onremove' of the Tooltip component. It doesn't always work as intended either, but it does the job.
+ */
+ hideOptionTooltip(vnode) {
+ vnode.attrs.tooltipVisible = false;
+ vnode.state.updateVisibility();
+ }
}
diff --git a/js/src/forum/extend.js b/js/src/forum/extend.js
index 097394c1..17a40979 100644
--- a/js/src/forum/extend.js
+++ b/js/src/forum/extend.js
@@ -1,6 +1,6 @@
import Extend from 'flarum/common/extenders';
import Post from 'flarum/common/models/Post';
-import User from 'flarum/common/models/User';
+import Forum from 'flarum/common/models/Forum';
import Discussion from 'flarum/common/models/Discussion';
import Poll from './models/Poll';
import PollOption from './models/PollOption';
@@ -9,9 +9,9 @@ import PollVote from './models/PollVote';
export default [
new Extend.Store().add('polls', Poll).add('poll_options', PollOption).add('poll_votes', PollVote),
- new Extend.Model(Post).hasMany('polls'),
+ new Extend.Model(Post).hasMany('polls').attribute('canStartPoll'),
- new Extend.Model(User).attribute('canStartPolls'),
+ new Extend.Model(Forum).attribute('canStartPolls'),
new Extend.Model(Discussion).attribute('hasPoll').attribute('canStartPoll'),
];
diff --git a/js/src/forum/index.js b/js/src/forum/index.js
index e4732b1c..bf0b4400 100755
--- a/js/src/forum/index.js
+++ b/js/src/forum/index.js
@@ -3,6 +3,7 @@ import app from 'flarum/forum/app';
import addDiscussionBadge from './addDiscussionBadge';
import addComposerItems from './addComposerItems';
import addPollsToPost from './addPollsToPost';
+import addPostControls from './addPostControls';
export * from './components';
export * from './models';
@@ -11,6 +12,7 @@ app.initializers.add('fof/polls', () => {
addDiscussionBadge();
addComposerItems();
addPollsToPost();
+ addPostControls();
});
export { default as extend } from './extend';
diff --git a/js/src/forum/models/Poll.js b/js/src/forum/models/Poll.js
index 23cf0421..4e6009ab 100755
--- a/js/src/forum/models/Poll.js
+++ b/js/src/forum/models/Poll.js
@@ -13,7 +13,7 @@ export default class Poll extends Model {
canVote = Model.attribute('canVote');
canEdit = Model.attribute('canEdit');
canDelete = Model.attribute('canDelete');
- canSeeVotes = Model.attribute('canSeeVotes');
+ canSeeVoters = Model.attribute('canSeeVoters');
canChangeVote = Model.attribute('canChangeVote');
options = Model.hasMany('options');
diff --git a/migrations/2019_07_01_000000_add_polls_table.php b/migrations/2019_07_01_000000_add_polls_table.php
index a9c90409..46769f6d 100755
--- a/migrations/2019_07_01_000000_add_polls_table.php
+++ b/migrations/2019_07_01_000000_add_polls_table.php
@@ -9,63 +9,22 @@
* file that was distributed with this source code.
*/
-use FoF\Polls\Migrations\AbstractMigration;
-use FoF\Polls\Poll;
+use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Database\Schema\Builder;
-return AbstractMigration::make(
- function (Builder $schema) {
- $schema->create('polls', function (Blueprint $table) {
- $table->increments('id');
+return Migration::createTable('polls', function (Blueprint $table) {
+ $table->increments('id');
- $table->string('question');
+ $table->string('question');
- $table->integer('discussion_id')->unsigned();
- $table->integer('user_id')->unsigned()->nullable();
+ $table->integer('discussion_id')->unsigned();
+ $table->integer('user_id')->unsigned()->nullable();
- $table->boolean('public_poll');
+ $table->boolean('public_poll');
- $table->timestamp('end_date')->nullable();
- $table->timestamps();
+ $table->timestamp('end_date')->nullable();
+ $table->timestamps();
- $table->foreign('discussion_id')->references('id')->on('discussions')->onDelete('cascade');
- $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
- });
- },
- function (Builder $schema) {
- // delete polls whose discussion was deleted
- $polls = Poll::doesntHave('discussion')->get();
-
- if ($polls->count() !== 0) {
- resolve('log')->info("[fof/polls] deleting {$polls->count()} polls");
-
- foreach ($polls as $poll) {
- /*
- * @var Poll $poll
- */
- resolve('log')->info("[fof/polls] |> deleting #{$poll->id}");
-
- $poll->votes()->delete();
- $poll->options()->delete();
- $poll->delete();
- }
- }
-
- // set user to null if user was deleted
- Poll::query()->whereNotNull('user_id')->doesntHave('user')->update([
- 'user_id' => null,
- ]);
-
- $schema->table('polls', function (Blueprint $table) {
- $table->integer('discussion_id')->unsigned()->change();
- $table->integer('user_id')->unsigned()->nullable()->change();
-
- $table->foreign('discussion_id')->references('id')->on('discussions')->onDelete('cascade');
- $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
- });
- },
- function (Builder $schema) {
- $schema->dropIfExists('polls');
- }
-);
+ $table->foreign('discussion_id')->references('id')->on('discussions')->onDelete('cascade');
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
+});
diff --git a/migrations/2019_07_01_000001_add_poll_options_table.php b/migrations/2019_07_01_000001_add_poll_options_table.php
index a2c31c6d..439f2813 100755
--- a/migrations/2019_07_01_000001_add_poll_options_table.php
+++ b/migrations/2019_07_01_000001_add_poll_options_table.php
@@ -9,38 +9,17 @@
* file that was distributed with this source code.
*/
-use FoF\Polls\Migrations\AbstractMigration;
-use FoF\Polls\PollOption;
+use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Database\Schema\Builder;
-return AbstractMigration::make(
- function (Builder $schema) {
- $schema->create('poll_options', function (Blueprint $table) {
- $table->increments('id');
+return Migration::createTable('poll_options', function (Blueprint $table) {
+ $table->increments('id');
- $table->string('answer');
+ $table->string('answer');
- $table->integer('poll_id')->unsigned();
+ $table->integer('poll_id')->unsigned();
- $table->timestamps();
+ $table->timestamps();
- $table->foreign('poll_id')->references('id')->on('polls')->onDelete('cascade');
- });
- },
- function (Builder $schema) {
- // delete poll options that don't have a poll
- PollOption::query()->doesntHave('poll')->delete();
-
- $schema->table('poll_options', function (Blueprint $table) {
- $table->dropForeign(['poll_id']);
- });
-
- $schema->table('poll_options', function (Blueprint $table) {
- $table->foreign('poll_id')->references('id')->on('polls')->onDelete('cascade');
- });
- },
- function (Builder $schema) {
- $schema->dropIfExists('poll_options');
- }
-);
+ $table->foreign('poll_id')->references('id')->on('polls')->onDelete('cascade');
+});
diff --git a/migrations/2019_07_01_000002_add_poll_votes_table.php b/migrations/2019_07_01_000002_add_poll_votes_table.php
index aa194f4f..dd129652 100755
--- a/migrations/2019_07_01_000002_add_poll_votes_table.php
+++ b/migrations/2019_07_01_000002_add_poll_votes_table.php
@@ -9,41 +9,19 @@
* file that was distributed with this source code.
*/
-use FoF\Polls\Migrations\AbstractMigration;
-use FoF\Polls\PollVote;
+use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Database\Schema\Builder;
-return AbstractMigration::make(
- function (Builder $schema) {
- $schema->create('poll_votes', function (Blueprint $table) {
- $table->increments('id');
+return Migration::createTable('poll_votes', function (Blueprint $table) {
+ $table->increments('id');
- $table->integer('poll_id')->unsigned();
- $table->integer('option_id')->unsigned();
- $table->integer('user_id')->unsigned()->nullable();
+ $table->integer('poll_id')->unsigned();
+ $table->integer('option_id')->unsigned();
+ $table->integer('user_id')->unsigned()->nullable();
- $table->timestamps();
+ $table->timestamps();
- $table->foreign('poll_id')->references('id')->on('polls')->onDelete('cascade');
- $table->foreign('option_id')->references('id')->on('poll_options')->onDelete('cascade');
- $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
- });
- },
- function (Builder $schema) {
- // delete votes that don't have a poll or option
- PollVote::query()->doesntHave('poll')->orDoesntHave('option')->delete();
-
- $schema->table('poll_votes', function (Blueprint $table) {
- $table->dropForeign(['option_id']);
- });
-
- $schema->table('poll_votes', function (Blueprint $table) {
- $table->foreign('poll_id')->references('id')->on('polls')->onDelete('cascade');
- $table->foreign('option_id')->references('id')->on('poll_options')->onDelete('cascade');
- });
- },
- function (Builder $schema) {
- $schema->dropIfExists('poll_votes');
- }
-);
+ $table->foreign('poll_id')->references('id')->on('polls')->onDelete('cascade');
+ $table->foreign('option_id')->references('id')->on('poll_options')->onDelete('cascade');
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
+});
diff --git a/resources/less/forum.less b/resources/less/forum.less
index 723836a0..0cafa149 100755
--- a/resources/less/forum.less
+++ b/resources/less/forum.less
@@ -81,14 +81,6 @@
}
}
- .Checkbox {
- &.off {
- .Checkbox-display {
- background: darken(@control-bg, 5%);
- }
- }
- }
-
.PollModal-SubmitButton {
margin-top: 10px;
}
@@ -99,6 +91,10 @@
}
.Post-poll {
+ .Post--hidden & {
+ opacity: 0.5;
+ }
+
.PollOptions {
display: grid;
grid: auto-flow / repeat(auto-fit, minmax(~"min(250px, 100%)", 1fr));
@@ -121,8 +117,13 @@
flex-grow: 1;
}
+ &-voters {
+ .Button--color-auto('button-primary');
+ }
+
.Button {
flex-shrink: 1;
+ padding: 6px 10px;
.icon {
margin-right: 0;
@@ -141,8 +142,11 @@
width: var(--width, 0);
background-color: @muted-color;
- animation: slideIn 1s ease-in-out;
- transition: width 0.75s ease-in-out;
+ // Don't animate if the post is hidden
+ &:not(.Post--hidden &) {
+ animation: slideIn 1s ease-in-out;
+ transition: width 0.75s ease-in-out;
+ }
.checkmark {
background-color: #fabc67;
@@ -254,6 +258,8 @@
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
+
+
}
}
}
diff --git a/resources/locale/en.yml b/resources/locale/en.yml
index 71907a02..9abb2de2 100755
--- a/resources/locale/en.yml
+++ b/resources/locale/en.yml
@@ -5,7 +5,8 @@ fof-polls:
permissions:
view_results_without_voting: View results without voting
start: Start a poll
- self_edit: Allow users to edit their own polls
+ self_edit: Edit created polls (requires post edit permission)
+ self_post_edit: Edit *all* polls on own posts (requires post edit permission)
vote: Vote on polls
change_vote: Change vote
moderate: Edit & remove polls
@@ -18,7 +19,7 @@ fof-polls:
max_votes_allowed: "Poll allows voting for {max, plural, one {# option} other {# options}}."
composer_discussion:
- add_poll: Add Poll
+ add_poll: => fof-polls.forum.moderation.add
edit_poll: => fof-polls.forum.moderation.edit
no_permission_alert: You do not have permission to start a poll.
@@ -42,6 +43,7 @@ fof-polls:
submit: Submit
moderation:
+ add: Add Poll
delete: Remove Poll
delete_confirm: Are you sure you want to delete this poll?
edit: Edit Poll
diff --git a/src/Access/PollPolicy.php b/src/Access/PollPolicy.php
index 85ec07a2..e51bc44b 100644
--- a/src/Access/PollPolicy.php
+++ b/src/Access/PollPolicy.php
@@ -24,13 +24,20 @@ public function seeVoteCount(User $actor, Poll $poll)
}
}
- public function seeVotes(User $actor, Poll $poll)
+ public function seeVoters(User $actor, Poll $poll)
{
if (($poll->myVotes($actor)->count() || $actor->can('polls.viewResultsWithoutVoting', $poll->post->discussion)) && $poll->public_poll) {
return $this->allow();
}
}
+ public function view(User $actor, Poll $poll)
+ {
+ if ($actor->can('view', $poll->post)) {
+ return $this->allow();
+ }
+ }
+
public function vote(User $actor, Poll $poll)
{
if ($actor->can('polls.vote', $poll->post->discussion) && !$poll->hasEnded()) {
@@ -51,10 +58,10 @@ public function edit(User $actor, Poll $poll)
return $this->allow();
}
- if ($actor->hasPermission('polls.selfEdit') && !$poll->hasEnded()) {
- $ownerId = $poll->post->user_id;
-
- if ($ownerId && $ownerId === $actor->id) {
+ if (!$poll->hasEnded() && $actor->can('edit', $poll->post)) {
+ // User either created poll & can edit own poll or can edit all polls in post
+ if (($actor->id === $poll->user_id && $actor->hasPermission('polls.selfEdit'))
+ || ($actor->id == $poll->post->user_id && $actor->hasPermission('polls.selfPostEdit'))) {
return $this->allow();
}
}
diff --git a/src/Access/PostPolicy.php b/src/Access/PostPolicy.php
new file mode 100644
index 00000000..1a9ad07d
--- /dev/null
+++ b/src/Access/PostPolicy.php
@@ -0,0 +1,35 @@
+type, static::$ALLOWED_POST_TYPES)) {
+ return $this->deny();
+ }
+
+ // Only allow polls to be started if the user can start polls in the discussion
+ // and if the user can either edit the post or is currently creating a new post.
+ // For example, actors cannot 'edit' a post they're currently creating if post editing is allowed until next reply.
+ if ($actor->can('polls.start', $post->discussion) && (!$post->exists || $actor->can('edit', $post))) {
+ return $this->allow();
+ }
+ }
+}
diff --git a/src/Access/ScopePollVisibility.php b/src/Access/ScopePollVisibility.php
new file mode 100644
index 00000000..ea9f2442
--- /dev/null
+++ b/src/Access/ScopePollVisibility.php
@@ -0,0 +1,29 @@
+whereExists(function ($query) use ($actor) {
+ $query->selectRaw('1')
+ ->from('posts')
+ ->whereColumn('posts.id', 'polls.post_id');
+ Post::query()->setQuery($query)->whereVisibleTo($actor);
+ });
+ }
+}
diff --git a/src/Api/Controllers/VotePollController.php b/src/Api/Controllers/CreatePollController.php
old mode 100755
new mode 100644
similarity index 50%
rename from src/Api/Controllers/VotePollController.php
rename to src/Api/Controllers/CreatePollController.php
index b72cd20d..d313b435
--- a/src/Api/Controllers/VotePollController.php
+++ b/src/Api/Controllers/CreatePollController.php
@@ -11,54 +11,47 @@
namespace FoF\Polls\Api\Controllers;
-use Flarum\Api\Controller\AbstractShowController;
+use Flarum\Api\Controller\AbstractCreateController;
+use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
+use Flarum\Post\PostRepository;
use FoF\Polls\Api\Serializers\PollSerializer;
-use FoF\Polls\Commands\VotePoll;
-use Illuminate\Contracts\Bus\Dispatcher;
+use FoF\Polls\Commands\CreatePoll;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
-class VotePollController extends AbstractShowController
+class CreatePollController extends AbstractCreateController
{
- /**
- * @var string
- */
public $serializer = PollSerializer::class;
- public $include = ['options', 'myVotes', 'myVotes.option'];
+ public $include = ['options'];
- public $optionalInclude = ['votes', 'votes.option', 'votes.user'];
+ /**
+ * @var PostRepository
+ */
+ protected $posts;
/**
* @var Dispatcher
*/
protected $bus;
- /**
- * @param Dispatcher $bus
- */
- public function __construct(Dispatcher $bus)
+ public function __construct(PostRepository $posts, Dispatcher $bus)
{
+ $this->posts = $posts;
$this->bus = $bus;
}
- /**
- * Get the data to be serialized and assigned to the response document.
- *
- * @param ServerRequestInterface $request
- * @param Document $document
- *
- * @return mixed
- */
protected function data(ServerRequestInterface $request, Document $document)
{
+ $postId = Arr::get($request->getParsedBody(), 'data.relationships.post.data.id');
+
return $this->bus->dispatch(
- new VotePoll(
+ new CreatePoll(
RequestUtil::getActor($request),
- Arr::get($request->getQueryParams(), 'id'),
- Arr::get($request->getParsedBody(), 'data', [])
+ $this->posts->findOrFail($postId),
+ Arr::get($request->getParsedBody(), 'data.attributes')
)
);
}
diff --git a/src/Api/Controllers/ShowPollController.php b/src/Api/Controllers/ShowPollController.php
index 39ed0775..0001b8e9 100644
--- a/src/Api/Controllers/ShowPollController.php
+++ b/src/Api/Controllers/ShowPollController.php
@@ -12,8 +12,9 @@
namespace FoF\Polls\Api\Controllers;
use Flarum\Api\Controller\AbstractShowController;
+use Flarum\Http\RequestUtil;
use FoF\Polls\Api\Serializers\PollSerializer;
-use FoF\Polls\Poll;
+use FoF\Polls\PollRepository;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -29,11 +30,24 @@ class ShowPollController extends AbstractShowController
public $optionalInclude = ['votes', 'votes.option', 'votes.user'];
+ /**
+ * @var PollRepository
+ */
+ protected $polls;
+
+ public function __construct(PollRepository $polls)
+ {
+ $this->polls = $polls;
+ }
+
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
- return Poll::findOrFail(Arr::get($request->getQueryParams(), 'id'));
+ return $this->polls->findOrFail(
+ Arr::get($request->getQueryParams(), 'id'),
+ RequestUtil::getActor($request)
+ );
}
}
diff --git a/src/Api/Serializers/PollSerializer.php b/src/Api/Serializers/PollSerializer.php
index 6665e10f..95207fe5 100755
--- a/src/Api/Serializers/PollSerializer.php
+++ b/src/Api/Serializers/PollSerializer.php
@@ -42,7 +42,7 @@ protected function getDefaultAttributes($poll)
'canVote' => $this->actor->can('vote', $poll),
'canEdit' => $this->actor->can('edit', $poll),
'canDelete' => $this->actor->can('delete', $poll),
- 'canSeeVotes' => $this->actor->can('seeVotes', $poll),
+ 'canSeeVoters' => $this->actor->can('seeVoters', $poll),
'canChangeVote' => $this->actor->can('changeVote', $poll),
];
@@ -63,7 +63,7 @@ public function options($model)
public function votes($model)
{
- if ($this->actor->cannot('seeVotes', $model)) {
+ if ($this->actor->cannot('seeVoters', $model)) {
return null;
}
diff --git a/src/Commands/CreatePoll.php b/src/Commands/CreatePoll.php
new file mode 100644
index 00000000..b56e2ee3
--- /dev/null
+++ b/src/Commands/CreatePoll.php
@@ -0,0 +1,54 @@
+actor = $actor;
+ $this->post = $post;
+ $this->data = $data;
+ $this->savePollOn = $savePollOn ?: function (callable $callback) {
+ return $callback();
+ };
+ }
+}
diff --git a/src/Commands/CreatePollHandler.php b/src/Commands/CreatePollHandler.php
new file mode 100644
index 00000000..9f4a51f5
--- /dev/null
+++ b/src/Commands/CreatePollHandler.php
@@ -0,0 +1,130 @@
+validator = $validator;
+ $this->optionValidator = $optionValidator;
+ $this->events = $events;
+ $this->settings = $settings;
+ $this->posts = $posts;
+ }
+
+ public function handle(CreatePoll $command)
+ {
+ $command->actor->assertCan('startPoll', $command->post);
+
+ $attributes = $command->data;
+
+ // Ideally we would use some JSON:API relationship syntax, but it's just too complicated with Flarum to generate the correct JSON payload
+ // Instead we just pass an array of option objects that are each a set of key-value pairs for the option attributes
+ // This is also the same syntax that always used by EditPollHandler
+ $rawOptionsData = Arr::get($attributes, 'options');
+ $optionsData = [];
+
+ if (is_array($rawOptionsData)) {
+ foreach ($rawOptionsData as $rawOptionData) {
+ $optionsData[] = [
+ 'answer' => Arr::get($rawOptionData, 'answer'),
+ 'imageUrl' => Arr::get($rawOptionData, 'imageUrl') ?: null,
+ ];
+ }
+ }
+
+ $this->validator->assertValid($attributes);
+
+ foreach ($optionsData as $optionData) {
+ // It is guaranteed all keys exist in the array because $optionData is manually created above
+ // This ensures every attribute will be validated (Flarum doesn't validate missing keys)
+ $this->optionValidator->assertValid($optionData);
+ }
+
+ return ($command->savePollOn)(function () use ($optionsData, $attributes, $command) {
+ $endDate = Arr::get($attributes, 'endDate');
+ $carbonDate = Carbon::parse($endDate);
+
+ if (!$carbonDate->isFuture()) {
+ $carbonDate = null;
+ }
+
+ $poll = Poll::build(
+ Arr::get($attributes, 'question'),
+ $command->post->id,
+ $command->actor->id,
+ $carbonDate != null ? $carbonDate->utc() : null,
+ Arr::get($attributes, 'publicPoll'),
+ Arr::get($attributes, 'allowMultipleVotes'),
+ Arr::get($attributes, 'maxVotes')
+ );
+
+ $this->events->dispatch(new SavingPollAttributes($command->actor, $poll, $attributes, $attributes));
+
+ $poll->save();
+
+ $this->events->dispatch(new PollWasCreated($command->actor, $poll));
+
+ foreach ($optionsData as $optionData) {
+ $imageUrl = Arr::get($optionData, 'imageUrl');
+
+ if (!$this->settings->get('fof-polls.allowOptionImage')) {
+ $imageUrl = null;
+ }
+
+ $option = PollOption::build(Arr::get($optionData, 'answer'), $imageUrl);
+
+ $poll->options()->save($option);
+ }
+
+ return $poll;
+ });
+ }
+}
diff --git a/src/Commands/DeletePollHandler.php b/src/Commands/DeletePollHandler.php
index a01fdd88..2a13be33 100755
--- a/src/Commands/DeletePollHandler.php
+++ b/src/Commands/DeletePollHandler.php
@@ -11,16 +11,23 @@
namespace FoF\Polls\Commands;
-use FoF\Polls\Poll;
+use FoF\Polls\PollRepository;
class DeletePollHandler
{
+ /**
+ * @var PollRepository
+ */
+ protected $polls;
+
+ public function __construct(PollRepository $polls)
+ {
+ $this->polls = $polls;
+ }
+
public function handle(DeletePoll $command)
{
- /**
- * @var $poll Poll
- */
- $poll = Poll::findOrFail($command->pollId);
+ $poll = $this->polls->findOrFail($command->pollId, $command->actor);
$command->actor->assertCan('delete', $poll);
diff --git a/src/Commands/EditPollHandler.php b/src/Commands/EditPollHandler.php
index 42441c57..4bf815ba 100755
--- a/src/Commands/EditPollHandler.php
+++ b/src/Commands/EditPollHandler.php
@@ -14,7 +14,7 @@
use Carbon\Carbon;
use Flarum\Settings\SettingsRepositoryInterface;
use FoF\Polls\Events\SavingPollAttributes;
-use FoF\Polls\Poll;
+use FoF\Polls\PollRepository;
use FoF\Polls\Validators\PollOptionValidator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
@@ -36,19 +36,22 @@ class EditPollHandler
*/
protected $settings;
- public function __construct(PollOptionValidator $optionValidator, Dispatcher $events, SettingsRepositoryInterface $settings)
+ /**
+ * @var PollRepository
+ */
+ protected $polls;
+
+ public function __construct(PollRepository $polls, PollOptionValidator $optionValidator, Dispatcher $events, SettingsRepositoryInterface $settings)
{
$this->optionValidator = $optionValidator;
$this->events = $events;
$this->settings = $settings;
+ $this->polls = $polls;
}
public function handle(EditPoll $command)
{
- /**
- * @var $poll Poll
- */
- $poll = Poll::findOrFail($command->pollId);
+ $poll = $this->polls->findOrFail($command->pollId, $command->actor);
$command->actor->assertCan('edit', $poll);
diff --git a/src/Commands/MultipleVotesPollHandler.php b/src/Commands/MultipleVotesPollHandler.php
index 026121f2..a16d6c54 100644
--- a/src/Commands/MultipleVotesPollHandler.php
+++ b/src/Commands/MultipleVotesPollHandler.php
@@ -18,7 +18,7 @@
use FoF\Polls\Events\PollVotesChanged;
use FoF\Polls\Events\PollWasVoted;
use FoF\Polls\Poll;
-use FoF\Polls\PollVote;
+use FoF\Polls\PollRepository;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface;
@@ -53,13 +53,19 @@ class MultipleVotesPollHandler
*/
private $db;
+ /**
+ * @var PollRepository
+ */
+ private $polls;
+
/**
* @param Dispatcher $events
* @param SettingsRepositoryInterface $settings
* @param Container $container
*/
- public function __construct(Dispatcher $events, SettingsRepositoryInterface $settings, Container $container, Factory $validation, ConnectionResolverInterface $db)
+ public function __construct(PollRepository $polls, Dispatcher $events, SettingsRepositoryInterface $settings, Container $container, Factory $validation, ConnectionResolverInterface $db)
{
+ $this->polls = $polls;
$this->events = $events;
$this->settings = $settings;
$this->container = $container;
@@ -74,12 +80,10 @@ public function __construct(Dispatcher $events, SettingsRepositoryInterface $set
public function handle(MultipleVotesPoll $command)
{
$actor = $command->actor;
- /**
- * @var $poll Poll
- */
- $poll = Poll::findOrFail($command->pollId);
$data = $command->data;
+ $poll = $this->polls->findOrFail($command->pollId, $actor);
+
$actor->assertCan('vote', $poll);
$optionIds = Arr::get($data, 'optionIds');
@@ -137,8 +141,6 @@ function ($attribute, $value, $fail) use ($options) {
]);
$myVotes->push($vote);
-
- $this->pushNewVote($vote);
});
// Update vote counts of options & poll
@@ -179,20 +181,6 @@ function ($attribute, $value, $fail) use ($options) {
return $poll;
}
- /**
- * Pushes a new vote through websocket. Kept for backward compatibility, but we are no longer using it.
- *
- * @param PollVote $vote
- *
- * @throws \Pusher\PusherException
- */
- public function pushNewVote($vote)
- {
- if ($pusher = $this->getPusher()) {
- $pusher->trigger('public', 'newPollVote', $vote);
- }
- }
-
/**
* Pushes an updated option through websocket.
*
diff --git a/src/Commands/VotePoll.php b/src/Commands/VotePoll.php
deleted file mode 100755
index ae2740b2..00000000
--- a/src/Commands/VotePoll.php
+++ /dev/null
@@ -1,44 +0,0 @@
-actor = $actor;
- $this->pollId = $pollId;
- $this->data = $data;
- }
-}
diff --git a/src/Commands/VotePollHandler.php b/src/Commands/VotePollHandler.php
deleted file mode 100755
index 1dc53d6d..00000000
--- a/src/Commands/VotePollHandler.php
+++ /dev/null
@@ -1,168 +0,0 @@
-events = $events;
- $this->settings = $settings;
- $this->container = $container;
- }
-
- public function handle(VotePoll $command)
- {
- $actor = $command->actor;
- /**
- * @var $poll Poll
- */
- $poll = Poll::findOrFail($command->pollId);
- $data = $command->data;
-
- $optionId = Arr::get($data, 'optionId');
-
- $actor->assertCan('vote', $poll);
-
- /**
- * @var $vote PollVote|null
- */
- $vote = $poll->votes()->where('user_id', $actor->id)->first();
-
- if ($vote) {
- $actor->assertCan('changeVote', $poll);
-
- if ($poll->allow_multiple_votes) {
- throw new PermissionDeniedException(); // TODO change to proper error
- }
- }
-
- $previousOption = null;
-
- if ($optionId === null && $vote !== null) {
- $previousOption = $vote->option;
-
- $vote->delete();
- $vote = null;
- } elseif ($optionId !== null) {
- if ($vote) {
- $previousOption = $vote->option;
-
- $vote->option_id = $optionId;
- $vote->save();
- } else {
- $vote = $poll->votes()->create([
- 'user_id' => $actor->id,
- 'option_id' => $optionId,
- ]);
- }
-
- // Forget the relation in case it was loaded for $previousOption
- $vote->unsetRelation('option');
-
- $vote->option->refreshVoteCount()->save();
-
- $this->events->dispatch(new PollWasVoted($actor, $poll, $vote, $vote !== null));
-
- $this->pushNewVote($vote);
- }
-
- $poll->refreshVoteCount()->save();
-
- if ($previousOption) {
- $previousOption->refreshVoteCount()->save();
-
- $this->pushUpdatedOption($previousOption);
- }
-
- if ($vote) {
- $this->pushUpdatedOption($vote->option);
- }
-
- return $poll;
- }
-
- /**
- * Pushes a new vote through websocket. Kept for backward compatibility, but we are no longer using it.
- *
- * @param PollVote $vote
- *
- * @throws \Pusher\PusherException
- */
- public function pushNewVote($vote)
- {
- if ($pusher = $this->getPusher()) {
- $pusher->trigger('public', 'newPollVote', $vote);
- }
- }
-
- /**
- * Pushes an updated option through websocket.
- *
- * @param PollOption $option
- *
- * @throws \Pusher\PusherException
- */
- public function pushUpdatedOption(PollOption $option)
- {
- if ($pusher = $this->getPusher()) {
- $pusher->trigger('public', 'updatedPollOptions', [
- 'pollId' => $option->poll->id,
- 'pollVoteCount' => $option->poll->vote_count,
- 'options' => [
- [
- 'id' => $option->id,
- 'voteCount' => $option->vote_count,
- ],
- ],
- ]);
- }
- }
-
- private function getPusher()
- {
- return MultipleVotesPollHandler::pusher($this->container, $this->settings);
- }
-}
diff --git a/src/Listeners/SavePollsToDatabase.php b/src/Listeners/SavePollsToDatabase.php
index 9a5feea4..18df2a96 100755
--- a/src/Listeners/SavePollsToDatabase.php
+++ b/src/Listeners/SavePollsToDatabase.php
@@ -11,18 +11,12 @@
namespace FoF\Polls\Listeners;
-use Carbon\Carbon;
use Flarum\Foundation\ValidationException;
use Flarum\Post\Event\Saving;
-use Flarum\Settings\SettingsRepositoryInterface;
-use FoF\Polls\Events\PollWasCreated;
-use FoF\Polls\Events\SavingPollAttributes;
-use FoF\Polls\Poll;
-use FoF\Polls\PollOption;
+use FoF\Polls\Commands\CreatePoll;
use FoF\Polls\Validators\PollOptionValidator;
use FoF\Polls\Validators\PollValidator;
use Illuminate\Contracts\Events\Dispatcher;
-use Illuminate\Support\Arr;
use Symfony\Contracts\Translation\TranslatorInterface;
class SavePollsToDatabase
@@ -43,16 +37,16 @@ class SavePollsToDatabase
protected $events;
/**
- * @var SettingsRepositoryInterface
+ * @var \Flarum\Bus\Dispatcher
*/
- protected $settings;
+ protected $bus;
- public function __construct(PollValidator $validator, PollOptionValidator $optionValidator, Dispatcher $events, SettingsRepositoryInterface $settings)
+ public function __construct(PollValidator $validator, PollOptionValidator $optionValidator, Dispatcher $events, \Flarum\Bus\Dispatcher $bus)
{
$this->validator = $validator;
$this->optionValidator = $optionValidator;
$this->events = $events;
- $this->settings = $settings;
+ $this->bus = $bus;
}
public function handle(Saving $event)
@@ -63,7 +57,7 @@ public function handle(Saving $event)
// 'assertCan' throws a generic no permission error, but we want to be more specific.
// There are a lot of different reasons why a user might not be able to post a discussion.
- if ($event->actor->cannot('polls.start', $event->post->discussion)) {
+ if ($event->actor->cannot('startPoll', $event->post)) {
$translator = resolve(TranslatorInterface::class);
throw new ValidationException([
@@ -73,73 +67,15 @@ public function handle(Saving $event)
$attributes = (array) $event->data['attributes']['poll'];
- // Ideally we would use some JSON:API relationship syntax, but it's just too complicated with Flarum to generate the correct JSON payload
- // Instead we just pass an array of option objects that are each a set of key-value pairs for the option attributes
- // This is also the same syntax that always used by EditPollHandler
- $rawOptionsData = Arr::get($attributes, 'options');
-
- $optionsData = [];
-
- if (is_array($rawOptionsData)) {
- foreach ($rawOptionsData as $rawOptionData) {
- $optionsData[] = [
- 'answer' => Arr::get($rawOptionData, 'answer'),
- 'imageUrl' => Arr::get($rawOptionData, 'imageUrl') ?: null,
- ];
- }
- } else {
- // Backward-compatibility with old syntax that only passed an array of strings
- // We are no longer using this syntax in the extension itself
- foreach ((array) Arr::get($attributes, 'relationships.options') as $answerText) {
- $optionsData[] = [
- 'answer' => (string) $answerText,
- ];
- }
- }
-
- $this->validator->assertValid($attributes);
-
- foreach ($optionsData as $optionData) {
- // It is guaranteed all keys exist in the array because $optionData is manually created above
- // This ensures every attribute will be validated (Flarum doesn't validate missing keys)
- $this->optionValidator->assertValid($optionData);
- }
-
- $event->post->afterSave(function ($post) use ($optionsData, $attributes, $event) {
- $endDate = Arr::get($attributes, 'endDate');
- $carbonDate = Carbon::parse($endDate);
-
- if (!$carbonDate->isFuture()) {
- $carbonDate = null;
- }
-
- $poll = Poll::build(
- Arr::get($attributes, 'question'),
- $post->id,
- $event->actor->id,
- $carbonDate != null ? $carbonDate->utc() : null,
- Arr::get($attributes, 'publicPoll'),
- Arr::get($attributes, 'allowMultipleVotes'),
- Arr::get($attributes, 'maxVotes')
- );
-
- $this->events->dispatch(new SavingPollAttributes($event->actor, $poll, $attributes, $event->data));
-
- $poll->save();
-
- $this->events->dispatch(new PollWasCreated($event->actor, $poll));
-
- foreach ($optionsData as $optionData) {
- $imageUrl = Arr::get($optionData, 'imageUrl');
-
- if (!$this->settings->get('fof-polls.allowOptionImage')) {
- $imageUrl = null;
+ $this->bus->dispatch(
+ new CreatePoll(
+ $event->actor,
+ $event->post,
+ $attributes,
+ function (callable $callback) use ($event) {
+ $event->post->afterSave($callback);
}
-
- $option = PollOption::build(Arr::get($optionData, 'answer'), $imageUrl);
-
- $poll->options()->save($option);
- }
- });
+ )
+ );
}
}
diff --git a/src/Migrations/AbstractMigration.php b/src/Migrations/AbstractMigration.php
deleted file mode 100755
index 5f5115a7..00000000
--- a/src/Migrations/AbstractMigration.php
+++ /dev/null
@@ -1,51 +0,0 @@
- function (Builder $schema) use ($existing, $up) {
- if ($schema->getConnection()->table('migrations')->where('extension', 'reflar-polls')->exists()) {
- $migrated = $schema->getConnection()->table('migrations')
- ->where('migration', '2019_06_29_000000_add_support_for_deleted_users')
- ->where('extension', 'reflar-polls')
- ->exists();
-
- if (!$migrated) {
- throw new \UnexpectedValueException('[fof/polls] Please run the latest migration(s) of reflar/polls before enabling this extension.');
- }
-
- if ($existing != null) {
- $existing($schema);
- }
-
- return;
- }
-
- $up($schema);
- },
- 'down' => $down,
- ];
- }
-}
diff --git a/src/Poll.php b/src/Poll.php
index 088bc113..17e158bb 100755
--- a/src/Poll.php
+++ b/src/Poll.php
@@ -12,6 +12,7 @@
namespace FoF\Polls;
use Flarum\Database\AbstractModel;
+use Flarum\Database\ScopeVisibilityTrait;
use Flarum\Post\Post;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Collection;
@@ -35,6 +36,8 @@
*/
class Poll extends AbstractModel
{
+ use ScopeVisibilityTrait;
+
/**
* {@inheritdoc}
*/
diff --git a/src/PollRepository.php b/src/PollRepository.php
new file mode 100644
index 00000000..2118e506
--- /dev/null
+++ b/src/PollRepository.php
@@ -0,0 +1,50 @@
+
+ */
+ public function queryVisibleTo(?User $user = null): Builder
+ {
+ $query = $this->query();
+
+ if ($user !== null) {
+ $query->whereVisibleTo($user);
+ }
+
+ return $query;
+ }
+
+ /**
+ * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
+ */
+ public function findOrFail($id, User $actor = null): Poll
+ {
+ return $this->queryVisibleTo($actor)->findOrFail($id);
+ }
+}