Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.canSelectAsBestAnswer() || !app.session.user;
};

const isThisBestAnswer = (discussion: Discussion, post: Post): boolean => {
const bestAnswerPost = discussion.bestAnswerPost();
const bestAnswerPost = discussion.bestAnswerPost?.();
const hasBestAnswer = discussion.hasBestAnswer();
return hasBestAnswer !== undefined && hasBestAnswer && bestAnswerPost !== null && bestAnswerPost.id() === post.id();
return hasBestAnswer !== undefined && hasBestAnswer && !!bestAnswerPost && bestAnswerPost.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>('canSelectAsBestAnswer'),
];
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@

namespace FoF\BestAnswer\Api;

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

class DiscussionAttributes
class PostAttributes
{
/**
* @var BestAnswerRepository
Expand All @@ -27,9 +27,9 @@ public function __construct(BestAnswerRepository $bestAnswerRepository)
$this->bestAnswerRepository = $bestAnswerRepository;
}

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

return $attributes;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Console/NotifyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ public function __construct(SettingsRepositoryInterface $settings, NotificationS
public function handle()
{
$days = (int) $this->settings->get('fof-best-answer.select_best_answer_reminder_days');
$canSelectOwn = (bool) (int) $this->settings->get('fof-best-answer.allow_select_own_post');
$time = Carbon::now()->subDays($days);

// set a max time period to go back, so we don't spam really old discussions too.
Expand Down Expand Up @@ -89,11 +88,12 @@ public function handle()

$errors = [];

$query->chunkById(20, function ($discussions) use ($canSelectOwn, &$errors) {
$query->chunkById(20, function ($discussions) use (&$errors) {
// Filter out discussions where the user can't select a post as best answer.
// - 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) {
$discussions = $discussions->filter(function ($d) {
$canSelectOwn = $d->user->can('fof-best-answer.allow_select_own_post', $d);
$hasPermission = $d->user->can('selectBestAnswerOwnDiscussion', $d);
$canSelectPosts = $canSelectOwn || $d->posts()->where('user_id', '!=', $d->user_id)->count() != 0;

Expand Down
2 changes: 1 addition & 1 deletion src/Repository/BestAnswerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,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('fof-best-answer.allow_select_own_post', $post->discussion);
}

return true;
Expand Down
57 changes: 53 additions & 4 deletions tests/integration/api/SetBestAnswerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,22 @@ public function setUp(): void
],
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'user_id' => 2, 'created_at' => Carbon::now(), 'comment_count' => 2],
['id' => 2, 'title' => 'Another discussion', 'user_id' => 3, 'created_at' => Carbon::now(), 'comment_count' => 1],
],
'posts' => [
['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' => 2, 'user_id' => 3, 'type' => 'comment', 'content' => 'question', 'created_at' => Carbon::now()],
['id' => 5, 'discussion_id' => 2, 'user_id' => 3, 'type' => 'comment', 'content' => 'answer', 'created_at' => Carbon::now()],
],
'discussion_tag' => [
['discussion_id' => 1, 'tag_id' => 2],
['discussion_id' => 2, '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],
Expand All @@ -72,6 +77,22 @@ public function notAllowedUsersProvider(): array
];
}

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

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

public function getDiscussion(int $userId): ResponseInterface
{
return $this->send(
Expand All @@ -85,12 +106,12 @@ public function getDiscussion(int $userId): ResponseInterface
);
}

public function setBestAnswer(int $userId, int $postId): ResponseInterface
public function setBestAnswer(int $userId, int $postId, int $discussionId = 1): ResponseInterface
{
return $this->send(
$this->request(
'PATCH',
'/api/discussions/1',
'/api/discussions/'.$discussionId,
[
'json' => [
'data' => [
Expand Down Expand Up @@ -121,7 +142,6 @@ public function user_with_permission_can_set_best_answer(int $userId)
$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');

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

Expand Down Expand Up @@ -150,10 +170,39 @@ public function user_without_permission_cannot_set_best_answer(int $userId)
$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');

$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)
{
$response = $this->setBestAnswer($userId, 5, 2);

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

/**
* @test
*
* @dataProvider permittedUsersOwnPostProvider
*/
public function user_can_set_own_post_as_best_answer_if_permitted(int $userId)
{
$response = $this->setBestAnswer($userId, 5, 2);

$this->assertEquals(200, $response->getStatusCode());

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

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

$this->assertEquals(5, $attributes['hasBestAnswer'], 'Expected best answer post ID to be 5');
}
}
1 change: 0 additions & 1 deletion tests/integration/api/UnsetBestAnswerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ public function user_can_unset_best_answer_in_own_discussion_and_select_a_differ

$attributes = $data['data']['attributes'];
$this->assertFalse($attributes['hasBestAnswer']);
$this->assertTrue($attributes['canSelectBestAnswer'], 'Expected user to be able to set a best answer');

// Set a different post as best answer
$response = $this->send(
Expand Down
Loading