Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
->type(Notification\AwardedBestAnswerBlueprint::class, Serializer\BasicDiscussionSerializer::class, ['alert'])
->type(Notification\BestAnswerSetInDiscussionBlueprint::class, Serializer\BasicDiscussionSerializer::class, []),

(new Extend\ApiSerializer(Serializer\DiscussionSerializer::class))
->attributes(Api\DiscussionAttributes::class),
(new Extend\ApiSerializer(Serializer\PostSerializer::class))
->attributes(Api\PostAttributes::class),

(new Extend\ApiSerializer(Serializer\BasicDiscussionSerializer::class))
->hasOne('bestAnswerPost', Serializer\BasicPostSerializer::class)
Expand Down
7 changes: 6 additions & 1 deletion js/src/@types/shims.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ declare module 'flarum/common/models/Discussion' {
hasBestAnswer(): boolean | undefined;
bestAnswerPost(): Post | null;
bestAnswerUser(): User | null;
canSelectBestAnswer(): boolean;
bestAnswerSetAt(): Date | null;
}
}
Expand All @@ -37,3 +36,9 @@ declare module 'flarum/common/models/User' {
bestAnswerCount(): number;
}
}

declare module 'flarum/common/models/Post' {
export default interface Post {
canSelectAsBestAnswer(): boolean;
}
}
6 changes: 0 additions & 6 deletions js/src/admin/components/BestAnswerSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ export default class BestAnswerSettingsPage extends ExtensionPage {
</div>
<h3>{app.translator.trans('fof-best-answer.admin.settings.label.general')}</h3>
<div className="Section">
{this.buildSettingComponent({
type: 'boolean',
setting: 'fof-best-answer.allow_select_own_post',
label: app.translator.trans('fof-best-answer.admin.settings.allow_select_own_post'),
help: app.translator.trans('fof-best-answer.admin.settings.allow_select_own_post_help'),
})}
{this.buildSettingComponent({
type: 'boolean',
setting: 'fof-best-answer.use_alternative_ui',
Expand Down
8 changes: 8 additions & 0 deletions js/src/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ app.initializers.add(
permission: 'discussion.selectBestAnswerNotOwnDiscussion',
},
'reply'
)
.registerPermission(
{
icon: 'fas fa-check',
label: app.translator.trans('fof-best-answer.admin.permissions.allow_select_own_post'),
permission: 'discussion.fof-best-answer.allow_select_own_post',
},
'reply'
);

addBestAnswerCountSort();
Expand Down
15 changes: 5 additions & 10 deletions js/src/forum/addBestAnswerAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@ import extractText from 'flarum/common/utils/extractText';

export default function addBestAnswerAction() {
const ineligible = (discussion: Discussion, post: Post) => {
return post.isHidden() || post.number() === 1 || !discussion.canSelectBestAnswer() || !app.session.user;
};

const blockSelectOwnPost = (post: Post): boolean => {
const user = post.user();
return !app.forum.attribute<boolean>('canSelectBestAnswerOwnPost') && user !== false && user.id() === app.session.user?.id();
return post.isHidden() || post.number() === 1 || !post.canSelectBestAnswer() || !app.session.user;
};

const isThisBestAnswer = (discussion: Discussion, post: Post): boolean => {
const bestAnswerPost = discussion.bestAnswerPost();
const bAPost = discussion.bestAnswerPost?.();
const hasBestAnswer = discussion.hasBestAnswer();
return hasBestAnswer !== undefined && hasBestAnswer && bestAnswerPost !== null && bestAnswerPost.id() === post.id();
return hasBestAnswer !== undefined && hasBestAnswer && bAPost !== null && bAPost.id?.() === post.id();
};

const actionLabel = (isBestAnswer: boolean): string => {
Expand Down Expand Up @@ -71,7 +66,7 @@ export default function addBestAnswerAction() {

if (post.contentType() !== 'comment') return;

if (ineligible(discussion, post) || blockSelectOwnPost(post) || !app.current.matches(DiscussionPage)) return;
if (ineligible(discussion, post) || !app.current.matches(DiscussionPage)) return;

items.add(
'bestAnswer',
Expand Down Expand Up @@ -100,7 +95,7 @@ export default function addBestAnswerAction() {

post.pushAttributes({ isBestAnswer });

if (ineligible(discussion, post) || blockSelectOwnPost(post) || !app.current.matches(DiscussionPage)) return;
if (ineligible(discussion, post) || !app.current.matches(DiscussionPage)) return;

items.add(
'bestAnswer',
Expand Down
6 changes: 4 additions & 2 deletions js/src/forum/extend.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Discussion from 'flarum/common/models/Discussion';
import commonExtend from '../common/extend';
import Extend from 'flarum/common/extenders';
import type Post from 'flarum/common/models/Post';
import Post from 'flarum/common/models/Post';
import User from 'flarum/common/models/User';
import Model from 'flarum/common/Model';

Expand All @@ -12,9 +12,11 @@ export default [
.hasOne<Post>('bestAnswerPost')
.hasOne<User>('bestAnswerUser')
.attribute<boolean | number>('hasBestAnswer')
.attribute<boolean>('canSelectBestAnswer')
.attribute('bestAnswerSetAt', Model.transformDate),

new Extend.Model(User) //
.attribute<number>('bestAnswerCount'),

new Extend.Model(Post) //
.attribute<boolean>('canSelectBestAnswer'),
];
3 changes: 1 addition & 2 deletions resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ fof-best-answer:
permissions:
best_answer: Select Best Answer (own Discussion)
best_answer_not_own_discussion: Select Best Answer (not own Discussion)
allow_select_own_post: Select own post as Best Answer
settings:
label:
tags: Best Answer Tags
Expand All @@ -11,8 +12,6 @@ fof-best-answer:
search: Search
advanced: Advanced
reminders_notice: For reminders to function, you must have set up the Flarum scheduler correctly.
allow_select_own_post: Select own post
allow_select_own_post_help: Allow a user to select their own post as a best answer to a discussion
show_max_lines_label: Max lines to show in post preview
show_max_lines_help: Set to 0 to disable. If a post is longer than the configured amount of lines, it will be truncated in the post preview with a fade out effect.
select_best_answer_reminder_days: Reminder frequency
Expand Down
36 changes: 0 additions & 36 deletions src/Api/DiscussionAttributes.php

This file was deleted.

1 change: 0 additions & 1 deletion src/Api/ForumAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public function __invoke(ForumSerializer $serializer, $model, array $attributes)
}

$attributes['solutionSearchEnabled'] = $value;
$attributes['canSelectBestAnswerOwnPost'] = $this->getBooleanSetting('fof-best-answer.allow_select_own_post');
$attributes['useAlternativeBestAnswerUi'] = $this->getBooleanSetting('fof-best-answer.use_alternative_ui');
$attributes['showBestAnswerFilterUi'] = $this->getBooleanSetting('fof-best-answer.show_filter_dropdown');
$attributes['bestAnswerDiscussionSidebarJumpButton'] = $this->getBooleanSetting('fof-best-answer.discussion_sidebar_jump_button');
Expand Down
31 changes: 31 additions & 0 deletions src/Api/PostAttributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

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

namespace FoF\BestAnswer\Api;

use Flarum\Api\Serializer\PostSerializer;
use Flarum\Post\Post;
use FoF\BestAnswer\Repository\BestAnswerRepository;

class PostAttributes
{
public function __construct(

Check failure on line 20 in src/Api/PostAttributes.php

View workflow job for this annotation

GitHub Actions / run / PHPStan PHP 7.4

Promoted properties are supported only on PHP 8.0 and later.

Check failure on line 20 in src/Api/PostAttributes.php

View workflow job for this annotation

GitHub Actions / run / PHPStan PHP 7.3

Promoted properties are supported only on PHP 8.0 and later.
protected BestAnswerRepository $bestAnswerRepository,
) {
}

public function __invoke(PostSerializer $serializer, Post $post, array $attributes): array
{
$attributes['canSelectBestAnswer'] = $this->bestAnswerRepository->canSelectBestAnswer($serializer->getActor(), $post->discussion);

return $attributes;
}
}
2 changes: 1 addition & 1 deletion src/Console/NotifyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function handle()
// - The user must have permission to select a best answer on their own discussion
// - The user must be able to select a post, whether they can select any post (including their own) or not.
$discussions = $discussions->filter(function ($d) use ($canSelectOwn) {
$hasPermission = $d->user->can('selectBestAnswerOwnDiscussion', $d);
$hasPermission = $d->user->can('discussion.selectBestAnswerOwnDiscussion', $d);
$canSelectPosts = $canSelectOwn || $d->posts()->where('user_id', '!=', $d->user_id)->count() != 0;

return $hasPermission && $canSelectPosts;
Expand Down
17 changes: 14 additions & 3 deletions src/Repository/BestAnswerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,19 @@ public function canSelectBestAnswer(User $user, Discussion $discussion): bool
}

return $this->tagEnabledForBestAnswer($discussion) && ($user->id === $discussion->user_id
? $user->can('selectBestAnswerOwnDiscussion', $discussion)
: $user->can('selectBestAnswerNotOwnDiscussion', $discussion));
? $user->can('discussion.selectBestAnswerOwnDiscussion', $discussion)
: $user->can('discussion.selectBestAnswerNotOwnDiscussion', $discussion));
}

public function canSelectBestAnswerOwnPost(User $user, Discussion $discussion): bool
{
// Prevent best answers being set in a private discussion (ie byobu, etc)
if ($discussion->is_private) {
return false;
}

return $this->tagEnabledForBestAnswer($discussion) && ($user->id === $discussion->user_id
&& $user->can('discussion.fof-best-answer.allow_select_own_post', $discussion));
}

public function canSelectPostAsBestAnswer(User $user, Post $post): bool
Expand All @@ -71,7 +82,7 @@ public function canSelectPostAsBestAnswer(User $user, Post $post): bool
}

if ($user->id === $post->user_id) {
return (bool) $this->settings->get('fof-best-answer.allow_select_own_post');
return $user->can('discussion.fof-best-answer.allow_select_own_post', $post->discussion);
}

return true;
Expand Down
77 changes: 71 additions & 6 deletions tests/integration/api/SetBestAnswerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class SetBestAnswerTest extends TestCase
public function setUp(): void
{
parent::setUp();

$this->extension('flarum-tags');
$this->extension('fof-best-answer');

Expand All @@ -43,35 +42,54 @@ public function setUp(): void
['id' => 1, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => 'post 1 - question', 'created_at' => Carbon::now()],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => 'post 2 - answer1', 'created_at' => Carbon::now()],
['id' => 3, 'discussion_id' => 1, 'user_id' => 3, 'type' => 'comment', 'content' => 'post 2 - answer2', 'created_at' => Carbon::now()],
['id' => 4, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => 'post 4 - answer by owner', 'created_at' => Carbon::now()],
['id' => 5, 'discussion_id' => 1, 'user_id' => 3, 'type' => 'comment', 'content' => 'post 5 - answer by normal2', 'created_at' => Carbon::now()],
['id' => 6, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => 'post 6 - answer by moderator', 'created_at' => Carbon::now()],
],
'discussion_tag' => [
['discussion_id' => 1, 'tag_id' => 2],
],
'group_permission' => [
['group_id' => 4, 'permission' => 'discussion.selectBestAnswerNotOwnDiscussion', 'created_at' => Carbon::now()],
['group_id' => 4, 'permission' => 'discussion.fof-best-answer.allow_select_own_post', 'created_at' => Carbon::now()],
],
'group_user' => [
['user_id' => 4, 'group_id' => 4],
],
]);

$this->database()->table('group_permission')
->where('permission', 'discussion.selectBestAnswerOwnDiscussion')
->delete();
}

public function allowedUsersProvider(): array
{
return [
[1],
[2],
[4],
];
}

public function notAllowedUsersProvider(): array
{
return [
[2],
[3],
];
}

private function getCanSelectBestAnswer(array $included, int $userId): bool
{
foreach ($included as $item) {
if (($item['type'] ?? null) === 'posts' && isset($item['attributes']['canSelectBestAnswer']) && $item['relationships']['user']['data']['id'] == $userId) {
return $item['attributes']['canSelectBestAnswer'];
}
}

return false;
}

public function getDiscussion(int $userId): ResponseInterface
{
return $this->send(
Expand Down Expand Up @@ -119,9 +137,8 @@ public function user_with_permission_can_set_best_answer(int $userId)
$data = json_decode($response->getBody()->getContents(), true);

$attributes = $data['data']['attributes'];

$this->assertFalse($attributes['hasBestAnswer'], 'Expected no best answer post ID');
$this->assertTrue($attributes['canSelectBestAnswer'], 'Expected user to be able to set best answer');
$this->assertTrue($this->getCanSelectBestAnswer($data['included'], $userId), 'Expected user to be able to set best answer');

$response = $this->setBestAnswer($userId, 3);

Expand All @@ -134,6 +151,14 @@ public function user_with_permission_can_set_best_answer(int $userId)
$this->assertEquals(3, $attributes['hasBestAnswer'], 'Expected best answer post ID to be 3');
}

public static function unauthorizedUsersOwnPostProvider(): array
{
return [
[2],
[3],
];
}

/**
* @test
*
Expand All @@ -148,12 +173,52 @@ public function user_without_permission_cannot_set_best_answer(int $userId)
$data = json_decode($response->getBody()->getContents(), true);

$attributes = $data['data']['attributes'];

$this->assertFalse($attributes['hasBestAnswer'], 'Expected no best answer post ID');
$this->assertFalse($attributes['canSelectBestAnswer'], 'Expected user to not be able to set best answer');
$this->assertFalse($this->getCanSelectBestAnswer($data['included'], $userId), 'Expected user to not be able to set best answer');

$response = $this->setBestAnswer($userId, 3);

$this->assertEquals(403, $response->getStatusCode());
}

/**
* @test
*
* @dataProvider unauthorizedUsersOwnPostProvider
*/
public function user_cannot_set_own_post_as_best_answer_if_not_permitted(int $userId)
{
$postId = $userId === 2 ? 4 : 5;

$response = $this->setBestAnswer($userId, $postId);

$this->assertEquals(403, $response->getStatusCode());
}

public static function permittedUsersOwnPostProvider(): array
{
return [
[1],
[4],
];
}

/**
* @test
*
* @dataProvider permittedUsersOwnPostProvider
*/
public function user_can_set_own_post_as_best_answer_if_permitted(int $userId)
{
$postId = $userId === 1 ? 2 : 6;

$response = $this->setBestAnswer($userId, $postId);
$this->assertEquals(200, $response->getStatusCode());

$data = json_decode($response->getBody()->getContents(), true);

$attributes = $data['data']['attributes'];

$this->assertEquals($postId, $attributes['hasBestAnswer'], "Expected best answer post ID to be {$postId}");
}
}
Loading
Loading