Skip to content

Commit

Permalink
Add permissions to view count, view votes and change vote (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkwinkelmann authored Apr 8, 2021
1 parent 37976aa commit 2e3e4cf
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 25 deletions.
3 changes: 3 additions & 0 deletions extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,7 @@

(new Extend\Console())
->command(Console\RefreshVoteCountCommand::class),

(new Extend\Policy())
->modelPolicy(Poll::class, Access\PollPolicy::class),
];
26 changes: 21 additions & 5 deletions js/src/admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ app.initializers.add('fof/polls', () => {
.for('fof-polls')
.registerPermission(
{
icon: 'fa fa-pencil-alt',
label: app.translator.trans('fof-polls.admin.permissions.moderate'),
permission: 'discussion.polls',
icon: 'fa fa-signal',
label: app.translator.trans('fof-polls.admin.permissions.view_results_without_voting'),
permission: 'viewPollResultsWithoutVoting',
},
'moderate'
'view'
)
.registerPermission(
{
Expand All @@ -25,7 +25,7 @@ app.initializers.add('fof/polls', () => {
label: app.translator.trans('fof-polls.admin.permissions.self_edit'),
permission: 'selfEditPolls',
},
'reply'
'start'
)
.registerPermission(
{
Expand All @@ -34,5 +34,21 @@ app.initializers.add('fof/polls', () => {
permission: 'votePolls',
},
'reply'
)
.registerPermission(
{
icon: 'fa fa-signal',
label: app.translator.trans('fof-polls.admin.permissions.change_vote'),
permission: 'changeVotePolls',
},
'reply'
)
.registerPermission(
{
icon: 'fa fa-pencil-alt',
label: app.translator.trans('fof-polls.admin.permissions.moderate'),
permission: 'discussion.polls',
},
'moderate'
);
});
13 changes: 7 additions & 6 deletions js/src/forum/components/DiscussionPoll.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class DiscussionPoll extends Component {

view() {
const hasVoted = this.myVotes.length > 0;
const totalVotes = this.poll.voteCount();

return (
<div>
Expand All @@ -21,25 +22,25 @@ export default class DiscussionPoll extends Component {
{this.options.map((opt) => {
const voted = this.myVotes.some(vote => vote.option() === opt);
const votes = opt.voteCount();
const percent = Math.round((votes / this.poll.voteCount()) * 100);
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;

const title = hasVoted && app.translator.transChoice('fof-polls.forum.tooltip.votes', votes, {count: String(votes)}).join('');
const title = isNaN(votes) ? '' : app.translator.transChoice('fof-polls.forum.tooltip.votes', votes, {count: String(votes)}).join('');

return (
<div className={`PollOption ${hasVoted && 'PollVoted'} ${this.poll.hasEnded() && 'PollEnded'}`}>
<div title={title} className="PollBar" data-selected={voted}>
{((!this.poll.hasEnded() && app.session.user && app.session.user.canVotePolls()) || !app.session.user) && (
<label className="checkbox">
<input onchange={this.changeVote.bind(this, opt)} type="checkbox" checked={voted} />
<input onchange={this.changeVote.bind(this, opt)} type="checkbox" checked={voted} disabled={hasVoted && !this.poll.canChangeVote()} />
<span className="checkmark" />
</label>
)}

<div style={hasVoted && '--width: ' + percent + '%'} className="PollOption-active" />
<div style={!isNaN(votes) && '--width: ' + percent + '%'} className="PollOption-active" />
<label className="PollAnswer">
<span>{opt.answer()}</span>
</label>
{hasVoted && (
{!isNaN(votes) && (
<label>
<span className={percent !== 100 ? 'PollPercent PollPercent--option' : 'PollPercent'}>{percent}%</span>
</label>
Expand All @@ -51,7 +52,7 @@ export default class DiscussionPoll extends Component {

<div style="clear: both;" />

{this.poll.publicPoll()
{this.poll.canSeeVotes()
? Button.component(
{
className: 'Button Button--primary PublicPollButton',
Expand Down
3 changes: 1 addition & 2 deletions js/src/forum/components/ListVotersModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export default class ListVotersModal extends Modal {
<div className="Modal-body">
<ul className="VotesModal-list">
{this.attrs.poll.options().map((opt) => {
const votes = this.attrs.poll
.votes()
const votes = (this.attrs.poll.votes() || [])
.filter((v) => opt.id() === v.option().id())
.map((v) => v.user());

Expand Down
2 changes: 2 additions & 0 deletions js/src/forum/models/Poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export default class Poll extends mixin(Model, {
endDate: Model.attribute('endDate'),
publicPoll: Model.attribute('publicPoll'),
voteCount: Model.attribute('voteCount'),
canSeeVotes: Model.attribute('canSeeVotes'),
canChangeVote: Model.attribute('canChangeVote'),

options: Model.hasMany('options'),
votes: Model.hasMany('votes'),
Expand Down
4 changes: 4 additions & 0 deletions resources/less/forum.less
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@
&:checked ~ .checkmark:after {
display: block;
}

&[disabled] ~ .checkmark {
cursor: not-allowed;
}
}

.checkmark {
Expand Down
6 changes: 4 additions & 2 deletions resources/locale/en.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
fof-polls:
admin:
permissions:
moderate: Edit & remove polls
self_edit: Allow users to edit their own polls
view_results_without_voting: View results without voting
start: Start a poll
self_edit: Allow users to edit their own polls
vote: Vote on polls
change_vote: Change vote
moderate: Edit & remove polls

forum:
days_remaining: Poll ends {time}.
Expand Down
48 changes: 48 additions & 0 deletions src/Access/PollPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of fof/polls.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\Polls\Access;

use Flarum\User\Access\AbstractPolicy;
use Flarum\User\User;
use FoF\Polls\Poll;

class PollPolicy extends AbstractPolicy
{
public function seeVoteCount(User $actor, Poll $poll)
{
if ($actor->can('viewPollResultsWithoutVoting')) {
return $this->allow();
}

if ($poll->myVotes($actor)->count()) {
return $this->allow();
}
}

public function seeVotes(User $actor, Poll $poll)
{
if ($actor->can('viewPollResultsWithoutVoting')) {
return $this->allow();
}

if ($poll->myVotes($actor)->count() && $poll->public_poll) {
return $this->allow();
}
}

public function changeVote(User $actor, Poll $poll)
{
if ($actor->hasPermission('changeVotePolls')) {
return $this->allow();
}
}
}
9 changes: 7 additions & 2 deletions src/Api/Serializers/PollOptionSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ class PollOptionSerializer extends AbstractSerializer
*/
protected function getDefaultAttributes($option)
{
return [
$attributes = [
'answer' => $option->answer,
'voteCount' => (int) $option->vote_count,
'createdAt' => $this->formatDate($option->created_at),
'updatedAt' => $this->formatDate($option->updated_at),
];

if ($this->actor->can('seeVoteCount', $option->poll)) {
$attributes['voteCount'] = (int) $option->vote_count;
}

return $attributes;
}
}
27 changes: 19 additions & 8 deletions src/Api/Serializers/PollSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,22 @@ class PollSerializer extends AbstractSerializer
*/
protected function getDefaultAttributes($poll)
{
return [
'question' => $poll->question,
'hasEnded' => $poll->hasEnded(),
'publicPoll' => (bool) $poll->public_poll,
'voteCount' => (int) $poll->vote_count,
'endDate' => $this->formatDate($poll->end_date),
'createdAt' => $this->formatDate($poll->created_at),
'updatedAt' => $this->formatDate($poll->updated_at),
$attributes = [
'question' => $poll->question,
'hasEnded' => $poll->hasEnded(),
'publicPoll' => (bool) $poll->public_poll,
'endDate' => $this->formatDate($poll->end_date),
'createdAt' => $this->formatDate($poll->created_at),
'updatedAt' => $this->formatDate($poll->updated_at),
'canSeeVotes' => $this->actor->can('seeVotes', $poll),
'canChangeVote' => $this->actor->can('changeVote', $poll),
];

if ($this->actor->can('seeVoteCount', $poll)) {
$attributes['voteCount'] = (int) $poll->vote_count;
}

return $attributes;
}

public function options($model)
Expand All @@ -51,6 +58,10 @@ public function options($model)

public function votes($model)
{
if ($this->actor->cannot('seeVotes', $model)) {
return null;
}

return $this->hasMany(
$model,
PollVoteSerializer::class
Expand Down
4 changes: 4 additions & 0 deletions src/Commands/VotePollHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public function handle(VotePoll $command)
*/
$vote = $poll->votes()->where('user_id', $actor->id)->first();

if ($vote) {
$actor->assertCan('changeVote', $poll);
}

$previousOption = null;

if ($optionId === null && $vote !== null) {
Expand Down

0 comments on commit 2e3e4cf

Please sign in to comment.