mirror of
https://github.com/flarum/core.git
synced 2025-08-13 11:54:32 +02:00
Compare commits
27 Commits
dk/2.x-db-
...
sm/jas-no-
Author | SHA1 | Date | |
---|---|---|---|
|
96b8b92d42 | ||
|
a03104d61d | ||
|
7504e31399 | ||
|
aa39d0c11b | ||
|
d9e5ab4f11 | ||
|
ac27cd03dd | ||
|
a442aad3be | ||
|
51e2ab8502 | ||
|
a8777c6198 | ||
|
10514709f1 | ||
|
eb6e599df1 | ||
|
5ce1aeab47 | ||
|
389d004ddc | ||
|
72f89c0209 | ||
|
1e7eddb61e | ||
|
1302378141 | ||
|
29ede5aa27 | ||
|
d273b1920f | ||
|
b02f8190ea | ||
|
e0025df3e7 | ||
|
b8e17182e9 | ||
|
2b917372a7 | ||
|
270188b5b0 | ||
|
9149ecc7aa | ||
|
5fc2bb5eb6 | ||
|
24f3a6829f | ||
|
bf523b2325 |
@@ -23,3 +23,6 @@ indent_size = 2
|
||||
|
||||
[*.neon]
|
||||
indent_style = tab
|
||||
|
||||
[{install,update}.php]
|
||||
indent_size = 2
|
||||
|
53
.github/workflows/REUSABLE_backend.yml
vendored
53
.github/workflows/REUSABLE_backend.yml
vendored
@@ -44,7 +44,7 @@ on:
|
||||
description: Versions of databases to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "postgres:16"]'
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
|
||||
|
||||
php_ini_values:
|
||||
description: PHP ini values
|
||||
@@ -52,6 +52,12 @@ on:
|
||||
required: false
|
||||
default: error_reporting=E_ALL
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
composer_auth:
|
||||
description: The Composer auth tokens to use for private packages.
|
||||
@@ -65,7 +71,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -79,32 +85,49 @@ jobs:
|
||||
# Expands the matrix by naming DBs.
|
||||
- service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
- service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
- service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
- service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
- service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
|
||||
# Include Database prefix tests with only one PHP version.
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
|
||||
@@ -112,20 +135,24 @@ jobs:
|
||||
exclude:
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.0.30'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'sqlite:3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ matrix.service }}
|
||||
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
postgres:
|
||||
image: ${{ matrix.service }}
|
||||
ports:
|
||||
- 13306:5432
|
||||
env:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: flarum_test
|
||||
|
||||
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
||||
|
||||
@@ -146,6 +173,7 @@ jobs:
|
||||
ini-values: ${{ matrix.php_ini_values }}
|
||||
|
||||
- name: Create MySQL Database
|
||||
if: ${{ matrix.service != 'sqlite:3' }}
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||
@@ -175,10 +203,11 @@ jobs:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
DB_DRIVER: ${{ matrix.driver }}
|
||||
COMPOSER_PROCESS_TIMEOUT: 600
|
||||
|
||||
phpstan:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
8
.github/workflows/REUSABLE_frontend.yml
vendored
8
.github/workflows/REUSABLE_frontend.yml
vendored
@@ -86,6 +86,12 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token:
|
||||
description: The GitHub token to use for Bundlewatch.
|
||||
@@ -103,7 +109,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Checks & Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
if: >-
|
||||
((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || github.event_name != 'pull_request')
|
||||
|
@@ -108,10 +108,11 @@
|
||||
"php": "^8.1",
|
||||
"ext-json": "*",
|
||||
"components/font-awesome": "^5.15.0",
|
||||
"composer/composer": "^2.0",
|
||||
"composer/composer": "^2.7",
|
||||
"dflydev/fig-cookies": "^3.0",
|
||||
"doctrine/dbal": "^3.6.2",
|
||||
"dragonmantank/cron-expression": "^3.3",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"franzl/whoops-middleware": "2.0",
|
||||
"guzzlehttp/guzzle": "*",
|
||||
"illuminate/bus": "^10.0",
|
||||
@@ -150,7 +151,6 @@
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"s9e/text-formatter": "^2.13",
|
||||
"staudenmeir/eloquent-eager-limit": "^1.8.2",
|
||||
"sycho/json-api": "^0.5.0",
|
||||
"sycho/sourcemap": "^2.0.0",
|
||||
"symfony/config": "^6.3",
|
||||
"symfony/console": "^6.3",
|
||||
@@ -162,6 +162,7 @@
|
||||
"symfony/postmark-mailer": "^6.3",
|
||||
"symfony/translation": "^6.3",
|
||||
"symfony/yaml": "^6.3",
|
||||
"flarum/json-api-server": "^0.1.0",
|
||||
"wikimedia/less.php": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@@ -7,9 +7,10 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Approval\Access;
|
||||
use Flarum\Approval\Api\PostResourceFields;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Approval\Listener;
|
||||
use Flarum\Discussion\Discussion;
|
||||
@@ -36,17 +37,13 @@ return [
|
||||
->default('is_approved', true)
|
||||
->cast('is_approved', 'bool'),
|
||||
|
||||
(new Extend\ApiSerializer(BasicDiscussionSerializer::class))
|
||||
->attribute('isApproved', function (BasicDiscussionSerializer $serializer, Discussion $discussion): bool {
|
||||
return $discussion->is_approved;
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isApproved'),
|
||||
]),
|
||||
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->attribute('isApproved', function ($serializer, Post $post) {
|
||||
return (bool) $post->is_approved;
|
||||
})->attribute('canApprove', function (PostSerializer $serializer, Post $post) {
|
||||
return (bool) $serializer->getActor()->can('approvePosts', $post->discussion);
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
|
29
extensions/approval/src/Api/PostResourceFields.php
Normal file
29
extensions/approval/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Approval\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Post\Post;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isApproved')
|
||||
->writable(fn (Post $post, Context $context) => $context->getActor()->can('approve', $post))
|
||||
// set by the ApproveContent listener.
|
||||
->set(fn () => null),
|
||||
Schema\Boolean::make('canApprove')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('approvePosts', $post->discussion)),
|
||||
];
|
||||
}
|
||||
}
|
@@ -10,19 +10,23 @@
|
||||
namespace Flarum\Approval\Tests\integration;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
trait InteractsWithUnapprovedContent
|
||||
{
|
||||
protected function prepareUnapprovedDatabaseContent()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
@@ -31,7 +35,7 @@ trait InteractsWithUnapprovedContent
|
||||
['id' => 6, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 6, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
['id' => 7, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 7, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
@@ -45,7 +49,7 @@ trait InteractsWithUnapprovedContent
|
||||
['id' => 10, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 11, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 1, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0]
|
||||
],
|
||||
'group_user' => [
|
||||
|
123
extensions/approval/tests/integration/api/ApprovePostsTest.php
Normal file
123
extensions/approval/tests/integration/api/ApprovePostsTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class ApprovePostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
use InteractsWithUnapprovedContent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => Carbon::now(), 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 3, 'group_id' => 4],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.approvePosts'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function can_approve_unapproved_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/3', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isApproved' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(1, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function cannot_approve_post_without_permission()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/3', [
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isApproved' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(0, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function hiding_post_silently_approves_it()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/5', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isHidden' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(1, $this->database()->table('posts')->where('id', 5)->where('is_approved', 1)->count());
|
||||
}
|
||||
}
|
153
extensions/approval/tests/integration/api/CreatePostsTest.php
Normal file
153
extensions/approval/tests/integration/api/CreatePostsTest.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class CreatePostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
use InteractsWithUnapprovedContent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-flags', 'flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 5, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 6, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
['id' => 7, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 8, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 9, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 3, 'group_id' => 4],
|
||||
['user_id' => 2, 'group_id' => 5],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.startWithoutApproval'],
|
||||
['group_id' => 5, 'permission' => 'discussion.replyWithoutApproval'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider startDiscussionDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_start_discussion_without_approval_when_allowed(int $authenticatedAs, bool $allowed)
|
||||
{
|
||||
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.startWithoutApproval')->delete();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'attributes' => [
|
||||
'title' => 'This is a new discussion',
|
||||
'content' => 'This is a new discussion',
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('discussions')->where('id', $json['data']['id'])->value('is_approved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider replyToDiscussionDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_reply_without_approval_when_allowed(?int $authenticatedAs, bool $allowed)
|
||||
{
|
||||
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.replyWithoutApproval')->delete();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/posts', [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'This is a new reply',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'id' => 1
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('posts')->where('id', $json['data']['id'])->value('is_approved'));
|
||||
}
|
||||
|
||||
public static function startDiscussionDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'Admin' => [1, true],
|
||||
'User without permission' => [2, false],
|
||||
'Permission Given' => [3, true],
|
||||
'Another user without permission' => [4, false],
|
||||
];
|
||||
}
|
||||
|
||||
public static function replyToDiscussionDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'Admin' => [1, true],
|
||||
'User without permission' => [3, false],
|
||||
'Permission Given' => [2, true],
|
||||
'Another user without permission' => [4, false],
|
||||
];
|
||||
}
|
||||
}
|
@@ -7,25 +7,17 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Api\Controller\ListPostsController;
|
||||
use Flarum\Api\Controller\ShowDiscussionController;
|
||||
use Flarum\Api\Controller\ShowPostController;
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Flags\Access\ScopeFlagVisibility;
|
||||
use Flarum\Flags\AddCanFlagAttribute;
|
||||
use Flarum\Flags\AddFlagsApiAttributes;
|
||||
use Flarum\Flags\AddNewFlagCountAttribute;
|
||||
use Flarum\Flags\Api\Controller\CreateFlagController;
|
||||
use Flarum\Flags\Api\Controller\DeleteFlagsController;
|
||||
use Flarum\Flags\Api\Controller\ListFlagsController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Api\ForumResourceFields;
|
||||
use Flarum\Flags\Api\PostResourceFields;
|
||||
use Flarum\Flags\Api\Resource\FlagResource;
|
||||
use Flarum\Flags\Api\UserResourceFields;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Flags\Listener;
|
||||
use Flarum\Flags\PrepareFlagsApiData;
|
||||
use Flarum\Forum\Content\AssertRegistered;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Post;
|
||||
@@ -41,8 +33,6 @@ return [
|
||||
->js(__DIR__.'/js/dist/admin.js'),
|
||||
|
||||
(new Extend\Routes('api'))
|
||||
->get('/flags', 'flags.index', ListFlagsController::class)
|
||||
->post('/flags', 'flags.create', CreateFlagController::class)
|
||||
->delete('/posts/{id}/flags', 'flags.delete', DeleteFlagsController::class),
|
||||
|
||||
(new Extend\Model(User::class))
|
||||
@@ -51,27 +41,26 @@ return [
|
||||
(new Extend\Model(Post::class))
|
||||
->hasMany('flags', Flag::class, 'post_id'),
|
||||
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->hasMany('flags', FlagSerializer::class)
|
||||
->attribute('canFlag', AddCanFlagAttribute::class),
|
||||
new Extend\ApiResource(FlagResource::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('newFlagCount', AddNewFlagCountAttribute::class),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(AddFlagsApiAttributes::class),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class),
|
||||
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->addInclude(['posts.flags', 'posts.flags.user']),
|
||||
(new Extend\ApiResource(Resource\ForumResource::class))
|
||||
->fields(ForumResourceFields::class),
|
||||
|
||||
(new Extend\ApiController(ListPostsController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) {
|
||||
return $endpoint->addDefaultInclude(['posts.flags', 'posts.flags.user']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(ShowPostController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
|
||||
(new Extend\ApiController(AbstractSerializeController::class))
|
||||
->prepareDataForSerialization(PrepareFlagsApiData::class),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint) {
|
||||
return $endpoint->addDefaultInclude(['flags', 'flags.user']);
|
||||
}),
|
||||
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('guidelinesUrl', 'flarum-flags.guidelines_url'),
|
||||
|
2
extensions/flags/js/dist/forum.js
generated
vendored
2
extensions/flags/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/flags/js/dist/forum.js.map
generated
vendored
2
extensions/flags/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -151,7 +151,6 @@ export default class FlagPostModal extends FormModal {
|
||||
reason: this.reason() === 'other' ? null : this.reason(),
|
||||
reasonDetail: this.reasonDetail(),
|
||||
relationships: {
|
||||
user: app.session.user,
|
||||
post: this.attrs.post,
|
||||
},
|
||||
},
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Flags\Access;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
@@ -23,31 +22,26 @@ class ScopeFlagVisibility
|
||||
|
||||
public function __invoke(User $actor, Builder $query): void
|
||||
{
|
||||
if ($this->extensions->isEnabled('flarum-tags')) {
|
||||
$query
|
||||
->select('flags.*')
|
||||
->leftJoin('posts', 'posts.id', '=', 'flags.post_id')
|
||||
->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id')
|
||||
->whereNotExists(function ($query) use ($actor) {
|
||||
return $query->selectRaw('1')
|
||||
->from('discussion_tag')
|
||||
->whereNotIn('tag_id', function ($query) use ($actor) {
|
||||
Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'discussion.viewFlags')->select('tags.id');
|
||||
})
|
||||
->whereColumn('discussions.id', 'discussion_id');
|
||||
});
|
||||
$query
|
||||
->whereHas('post', function (Builder $query) use ($actor) {
|
||||
$query->whereVisibleTo($actor);
|
||||
})
|
||||
->where(function (Builder $query) use ($actor) {
|
||||
if ($this->extensions->isEnabled('flarum-tags')) {
|
||||
$query
|
||||
->select('flags.*')
|
||||
->whereHas('post.discussion.tags', function ($query) use ($actor) {
|
||||
$query->whereHasPermission($actor, 'discussion.viewFlags');
|
||||
});
|
||||
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->whereExists(function ($query) {
|
||||
return $query->selectRaw('1')
|
||||
->from('discussion_tag')
|
||||
->whereColumn('discussions.id', 'discussion_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhereDoesntHave('post.discussion.tags');
|
||||
}
|
||||
}
|
||||
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddCanFlagAttribute
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PostSerializer $serializer, Post $post): bool
|
||||
{
|
||||
return $serializer->getActor()->can('flag', $post) && $this->checkFlagOwnPostSetting($serializer->getActor(), $post);
|
||||
}
|
||||
|
||||
protected function checkFlagOwnPostSetting(User $actor, Post $post): bool
|
||||
{
|
||||
if ($actor->id === $post->user_id) {
|
||||
// If $actor is the post author, check to see if the setting is enabled
|
||||
return (bool) $this->settings->get('flarum-flags.can_flag_own');
|
||||
}
|
||||
|
||||
// $actor is not the post author
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddFlagsApiAttributes
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ForumSerializer $serializer): array
|
||||
{
|
||||
$attributes = [
|
||||
'canViewFlags' => $serializer->getActor()->hasPermissionLike('discussion.viewFlags')
|
||||
];
|
||||
|
||||
if ($attributes['canViewFlags']) {
|
||||
$attributes['flagCount'] = (int) $this->getFlagCount($serializer->getActor());
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
protected function getFlagCount(User $actor): int
|
||||
{
|
||||
return Flag::whereVisibleTo($actor)->distinct()->count('flags.post_id');
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddNewFlagCountAttribute
|
||||
{
|
||||
public function __invoke(CurrentUserSerializer $serializer, User $user): int
|
||||
{
|
||||
return $this->getNewFlagCount($user);
|
||||
}
|
||||
|
||||
protected function getNewFlagCount(User $actor): int
|
||||
{
|
||||
$query = Flag::whereVisibleTo($actor);
|
||||
|
||||
if ($time = $actor->read_flags_at) {
|
||||
$query->where('flags.created_at', '>', $time);
|
||||
}
|
||||
|
||||
return $query->distinct()->count('flags.post_id');
|
||||
}
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Controller;
|
||||
|
||||
use Flarum\Api\Controller\AbstractCreateController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Command\CreateFlag;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreateFlagController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = FlagSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'post',
|
||||
'post.flags',
|
||||
'user'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Flag
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new CreateFlag(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Controller;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Controller\AbstractListController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListFlagsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = FlagSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'post',
|
||||
'post.user',
|
||||
'post.discussion'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$actor->read_flags_at = Carbon::now();
|
||||
$actor->save();
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
if (in_array('post.user', $include)) {
|
||||
$include[] = 'post.user.groups';
|
||||
}
|
||||
|
||||
$flags = Flag::whereVisibleTo($actor)
|
||||
->latest('flags.created_at')
|
||||
->groupBy('post_id')
|
||||
->limit($limit + 1)
|
||||
->offset($offset)
|
||||
->get();
|
||||
|
||||
$this->loadRelations($flags, $include, $request);
|
||||
|
||||
$flags = $flags->all();
|
||||
|
||||
$areMoreResults = false;
|
||||
|
||||
if (count($flags) > $limit) {
|
||||
array_pop($flags);
|
||||
$areMoreResults = true;
|
||||
}
|
||||
|
||||
$this->addPaginationData(
|
||||
$document,
|
||||
$request,
|
||||
$this->url->to('api')->route('flags.index'),
|
||||
$areMoreResults ? null : 0
|
||||
);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
}
|
32
extensions/flags/src/Api/ForumResourceFields.php
Normal file
32
extensions/flags/src/Api/ForumResourceFields.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Flags\Flag;
|
||||
|
||||
class ForumResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('canViewFlags')
|
||||
->get(function (object $model, Context $context) {
|
||||
return $context->getActor()->hasPermissionLike('discussion.viewFlags');
|
||||
}),
|
||||
Schema\Integer::make('flagCount')
|
||||
->visible(fn (object $model, Context $context) => $context->getActor()->hasPermissionLike('discussion.viewFlags'))
|
||||
->get(function (object $model, Context $context) {
|
||||
return Flag::whereVisibleTo($context->getActor())->distinct()->count('flags.post_id');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
42
extensions/flags/src/Api/PostResourceFields.php
Normal file
42
extensions/flags/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('canFlag')
|
||||
->get(function (Post $post, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
return $actor->can('flag', $post) && (
|
||||
// $actor is not the post author
|
||||
$actor->id !== $post->user_id
|
||||
// If $actor is the post author, check to see if the setting is enabled
|
||||
|| ((bool) $this->settings->get('flarum-flags.can_flag_own'))
|
||||
);
|
||||
}),
|
||||
Schema\Relationship\ToMany::make('flags')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
}
|
165
extensions/flags/src/Api/Resource/FlagResource.php
Normal file
165
extensions/flags/src/Api/Resource/FlagResource.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Resource;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Context as FlarumContext;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Flags\Event\Created;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\Exception\InvalidParameterException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Flag>
|
||||
*/
|
||||
class FlagResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'flags';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Flag::class;
|
||||
}
|
||||
|
||||
public function query(Context $context): object
|
||||
{
|
||||
if ($context->listing(self::class)) {
|
||||
$query = Flag::query()->groupBy('post_id');
|
||||
|
||||
$this->scope($query, $context);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
return parent::query($context);
|
||||
}
|
||||
|
||||
public function scope(Builder $query, Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function newModel(Context $context): object
|
||||
{
|
||||
if ($context->creating(self::class)) {
|
||||
Flag::unguard();
|
||||
|
||||
return Flag::query()->firstOrNew([
|
||||
'post_id' => (int) Arr::get($context->body(), 'data.relationships.post.data.id'),
|
||||
'user_id' => $context->getActor()->id
|
||||
], [
|
||||
'type' => 'user',
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::newModel($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->defaultInclude(['post', 'post.flags', 'user']),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->defaultInclude(['user', 'post', 'post.user', 'post.discussion'])
|
||||
->defaultSort('-createdAt')
|
||||
->paginate()
|
||||
->after(function (FlarumContext $context, $data) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$actor->read_flags_at = Carbon::now();
|
||||
$actor->save();
|
||||
|
||||
return $data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('type'),
|
||||
Schema\Str::make('reason')
|
||||
->writableOnCreate()
|
||||
->nullable()
|
||||
->requiredOnCreateWithout(['reasonDetail'])
|
||||
->validationMessages([
|
||||
'reason.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
|
||||
]),
|
||||
Schema\Str::make('reasonDetail')
|
||||
->writableOnCreate()
|
||||
->nullable()
|
||||
->requiredOnCreateWithout(['reason'])
|
||||
->validationMessages([
|
||||
'reasonDetail.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
|
||||
]),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
|
||||
Schema\Relationship\ToOne::make('post')
|
||||
->includable()
|
||||
->writable(fn (Flag $flag, FlarumContext $context) => $context->creating())
|
||||
->set(function (Flag $flag, Post $post, FlarumContext $context) {
|
||||
if (! ($post instanceof CommentPost)) {
|
||||
throw new InvalidParameterException;
|
||||
}
|
||||
|
||||
$actor = $context->getActor();
|
||||
|
||||
$actor->assertCan('flag', $post);
|
||||
|
||||
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
|
||||
$flag->post_id = $post->id;
|
||||
}),
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('createdAt'),
|
||||
];
|
||||
}
|
||||
|
||||
public function created(object $model, Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(new Created($model, $context->getActor(), $context->body()));
|
||||
|
||||
return parent::created($model, $context);
|
||||
}
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api\Serializer;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class FlagSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'flags';
|
||||
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Flag)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Flag::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $model->type,
|
||||
'reason' => $model->reason,
|
||||
'reasonDetail' => $model->reason_detail,
|
||||
'createdAt' => $this->formatDate($model->created_at),
|
||||
];
|
||||
}
|
||||
|
||||
protected function post(Flag $flag): ?Relationship
|
||||
{
|
||||
return $this->hasOne($flag, PostSerializer::class);
|
||||
}
|
||||
|
||||
protected function user(Flag $flag): ?Relationship
|
||||
{
|
||||
return $this->hasOne($flag, BasicUserSerializer::class);
|
||||
}
|
||||
}
|
36
extensions/flags/src/Api/UserResourceFields.php
Normal file
36
extensions/flags/src/Api/UserResourceFields.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('newFlagCount')
|
||||
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->get(function (User $user, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$query = Flag::whereVisibleTo($actor);
|
||||
|
||||
if ($time = $actor->read_flags_at) {
|
||||
$query->where('flags.created_at', '>', $time);
|
||||
}
|
||||
|
||||
return $query->distinct()->count('flags.post_id');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Command;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Flags\Event\Created;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobscure\JsonApi\Exception\InvalidParameterException;
|
||||
|
||||
class CreateFlagHandler
|
||||
{
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected Dispatcher $events
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(CreateFlag $command): Flag
|
||||
{
|
||||
$actor = $command->actor;
|
||||
$data = $command->data;
|
||||
|
||||
$postId = Arr::get($data, 'relationships.post.data.id');
|
||||
$post = $this->posts->findOrFail($postId, $actor);
|
||||
|
||||
if (! ($post instanceof CommentPost)) {
|
||||
throw new InvalidParameterException;
|
||||
}
|
||||
|
||||
$actor->assertCan('flag', $post);
|
||||
|
||||
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
|
||||
throw new PermissionDeniedException();
|
||||
}
|
||||
|
||||
if (Arr::get($data, 'attributes.reason') === null && Arr::get($data, 'attributes.reasonDetail') === '') {
|
||||
throw new ValidationException([
|
||||
'message' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message')
|
||||
]);
|
||||
}
|
||||
|
||||
Flag::unguard();
|
||||
|
||||
$flag = Flag::firstOrNew([
|
||||
'post_id' => $post->id,
|
||||
'user_id' => $actor->id
|
||||
]);
|
||||
|
||||
$flag->post_id = $post->id;
|
||||
$flag->user_id = $actor->id;
|
||||
$flag->type = 'user';
|
||||
$flag->reason = Arr::get($data, 'attributes.reason');
|
||||
$flag->reason_detail = Arr::get($data, 'attributes.reasonDetail');
|
||||
$flag->created_at = Carbon::now();
|
||||
|
||||
$flag->save();
|
||||
|
||||
$this->events->dispatch(new Created($flag, $actor, $data));
|
||||
|
||||
return $flag;
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class Flag extends AbstractModel
|
||||
{
|
||||
use ScopeVisibilityTrait;
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $casts = ['created_at' => 'datetime'];
|
||||
|
||||
|
30
extensions/flags/src/FlagFactory.php
Normal file
30
extensions/flags/src/FlagFactory.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class FlagFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'user',
|
||||
'post_id' => Post::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'reason' => $this->faker->sentence,
|
||||
'reason_detail' => $this->faker->sentence,
|
||||
'created_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Flags\Api\Controller\CreateFlagController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class PrepareFlagsApiData
|
||||
{
|
||||
public function __invoke(Controller\AbstractSerializeController $controller, mixed $data, ServerRequestInterface $request): void
|
||||
{
|
||||
// For any API action that allows the 'flags' relationship to be
|
||||
// included, we need to preload this relationship onto the data (Post
|
||||
// models) so that we can selectively expose only the flags that the
|
||||
// user has permission to view.
|
||||
if ($controller instanceof Controller\ShowDiscussionController) {
|
||||
if ($data->relationLoaded('posts')) {
|
||||
$posts = $data->getRelation('posts');
|
||||
}
|
||||
}
|
||||
|
||||
if ($controller instanceof Controller\ListPostsController) {
|
||||
$posts = $data->all();
|
||||
}
|
||||
|
||||
if ($controller instanceof Controller\ShowPostController) {
|
||||
$posts = [$data];
|
||||
}
|
||||
|
||||
if ($controller instanceof CreateFlagController) {
|
||||
$posts = [$data->post];
|
||||
}
|
||||
|
||||
if (isset($posts)) {
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$postsWithPermission = [];
|
||||
|
||||
foreach ($posts as $post) {
|
||||
if (is_object($post)) {
|
||||
$post->setRelation('flags', null);
|
||||
|
||||
if ($actor->can('viewFlags', $post->discussion)) {
|
||||
$postsWithPermission[] = $post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($postsWithPermission)) {
|
||||
(new Collection($postsWithPermission))
|
||||
->load('flags', 'flags.user');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,9 +9,13 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListTest extends TestCase
|
||||
@@ -28,7 +32,7 @@ class ListTest extends TestCase
|
||||
$this->extension('flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -44,20 +48,22 @@ class ListTest extends TestCase
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'is_private' => true],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 2],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -65,7 +71,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_see_one_flag_per_post()
|
||||
public function admin_can_see_one_flag_per_visible_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -73,9 +79,9 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body, true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
@@ -84,7 +90,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function regular_user_sees_own_flags()
|
||||
public function regular_user_sees_own_flags_of_visible_posts()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -103,7 +109,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function mod_can_see_one_flag_per_post()
|
||||
public function mod_can_see_one_flag_per_visible_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
|
@@ -9,9 +9,14 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListWithTagsTest extends TestCase
|
||||
@@ -29,13 +34,13 @@ class ListWithTagsTest extends TestCase
|
||||
$this->extension('flarum-tags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['id' => 1, 'name' => 'Unrestricted', 'slug' => '1', 'position' => 0, 'parent_id' => null],
|
||||
['id' => 2, 'name' => 'Mods can view discussions', 'slug' => '2', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 3, 'name' => 'Mods can view flags', 'slug' => '3', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 4, 'name' => 'Mods can view discussions and flags', 'slug' => '4', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -50,12 +55,12 @@ class ListWithTagsTest extends TestCase
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => 'has tags where mods can view discussions but not flags', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 3, 'title' => 'has tags where mods can view flags but not discussions', 'user_id' => 1, 'comment_count' => 1],
|
||||
@@ -68,7 +73,7 @@ class ListWithTagsTest extends TestCase
|
||||
['discussion_id' => 4, 'tag_id' => 4],
|
||||
['discussion_id' => 5, 'tag_id' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
@@ -79,7 +84,7 @@ class ListWithTagsTest extends TestCase
|
||||
['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
@@ -149,9 +154,7 @@ class ListWithTagsTest extends TestCase
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
// 7 is included, even though mods can't view discussions.
|
||||
// This is because the UI doesnt allow discussions.viewFlags without viewDiscussions.
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids);
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '8', '9'], $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\posts;
|
||||
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class IncludeFlagsVisibilityTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function setup(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-tags', 'flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
'username' => 'mod',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'normal2@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'username' => 'tod',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'tod@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'username' => 'ted',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'ted@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
],
|
||||
'group_user' => [
|
||||
['group_id' => 5, 'user_id' => 2],
|
||||
['group_id' => 6, 'user_id' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 5, 'name_singular' => 'group5', 'name_plural' => 'group5', 'color' => null, 'icon' => 'fas fa-crown', 'is_hidden' => false],
|
||||
['id' => 6, 'name_singular' => 'group1', 'name_plural' => 'group1', 'color' => null, 'icon' => 'fas fa-cog', 'is_hidden' => false],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'tag1.viewForum'],
|
||||
['group_id' => 5, 'permission' => 'tag1.viewForum'],
|
||||
['group_id' => 5, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.viewForum'],
|
||||
],
|
||||
'tags' => [
|
||||
['id' => 1, 'name' => 'Tag 1', 'slug' => 'tag-1', 'is_primary' => false, 'position' => null, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 2, 'name' => 'Tag 2', 'slug' => 'tag-2', 'is_primary' => true, 'position' => 2, 'parent_id' => null, 'is_restricted' => false],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Test1', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => 'Test2', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'discussion_tag' => [
|
||||
['discussion_id' => 1, 'tag_id' => 1],
|
||||
['discussion_id' => 2, 'tag_id' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 5, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
],
|
||||
'flags' => [
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 5],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 5],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
['id' => 7, 'post_id' => 5, 'user_id' => 5],
|
||||
['id' => 8, 'post_id' => 5, 'user_id' => 5],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider listFlagsIncludesDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function user_sees_where_allowed_with_included_tags(int $actorId, array $expectedIncludes)
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => $actorId,
|
||||
])->withQueryParams([
|
||||
'include' => 'flags'
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$responseBody = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$data = $responseBody['data'];
|
||||
|
||||
$this->assertEquals(['1', '2', '3', '4', '5'], Arr::pluck($data, 'id'));
|
||||
$this->assertEqualsCanonicalizing(
|
||||
$expectedIncludes,
|
||||
collect($responseBody['included'] ?? [])
|
||||
->filter(fn ($include) => $include['type'] === 'flags')
|
||||
->pluck('id')
|
||||
->map(strval(...))
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
public function listFlagsIncludesDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'admin_sees_all' => [1, [1, 2, 3, 4, 5, 6, 7, 8]],
|
||||
'user_with_general_permission_sees_where_unrestricted_tag' => [2, [6, 7, 8]],
|
||||
'user_with_tag1_permission_sees_tag1_flags' => [3, [1, 2, 3, 4, 5]],
|
||||
'normal_user_sees_none' => [4, []],
|
||||
'normal_user_sees_own' => [5, [2, 7, 4, 8]],
|
||||
];
|
||||
}
|
||||
}
|
@@ -9,16 +9,16 @@
|
||||
|
||||
namespace Flarum\Likes;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Likes\Notification\PostLikedBlueprint;
|
||||
use Flarum\Likes\Query\LikedByFilter;
|
||||
use Flarum\Likes\Query\LikedFilter;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
@@ -39,43 +39,28 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(PostLikedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
->type(PostLikedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->hasMany('likes', BasicUserSerializer::class)
|
||||
->attribute('canLike', function (PostSerializer $serializer, $model) {
|
||||
return (bool) $serializer->getActor()->can('like', $model);
|
||||
})
|
||||
->attribute('likesCount', function (PostSerializer $serializer, $model) {
|
||||
return $model->getAttribute('likes_count') ?: 0;
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint(
|
||||
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
|
||||
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['likes']);
|
||||
}
|
||||
),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['posts.likes']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude('posts.likes')
|
||||
->loadWhere('posts.likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
|
||||
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
|
||||
->subscribe(Listener\SaveLikesToDatabase::class),
|
||||
->listen(Deleted::class, function (Deleted $event) {
|
||||
$event->post->likes()->detach();
|
||||
}),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, LikedByFilter::class)
|
||||
|
2
extensions/likes/js/dist/forum.js
generated
vendored
2
extensions/likes/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/likes/js/dist/forum.js.map
generated
vendored
2
extensions/likes/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,11 +2,15 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import LikesUserPage from './components/LikesUserPage';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
.add('user.likes', '/u/:username/likes', LikesUserPage),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postLiked', PostLikedNotification),
|
||||
|
||||
new Extend.Model(Post) //
|
||||
.hasMany<User>('likes')
|
||||
.attribute<number>('likesCount')
|
||||
|
@@ -3,14 +3,11 @@ import app from 'flarum/forum/app';
|
||||
|
||||
import addLikeAction from './addLikeAction';
|
||||
import addLikesList from './addLikesList';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
import addLikesTabToUserProfile from './addLikesTabToUserProfile';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-likes', () => {
|
||||
app.notificationComponents.postLiked = PostLikedNotification;
|
||||
|
||||
addLikeAction();
|
||||
addLikesList();
|
||||
addLikesTabToUserProfile();
|
||||
|
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Likes\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class LoadLikesRelationship
|
||||
{
|
||||
public static int $maxLikes = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
$query
|
||||
// So that we can tell if the current user has liked the post.
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxLikes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation(AbstractSerializeController $controller, mixed $data): array
|
||||
{
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// We do this because the ShowDiscussionController manipulates the posts
|
||||
// in a way that some of them are just ids.
|
||||
$loadable = $data->posts->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount('likes');
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
65
extensions/likes/src/Api/PostResourceFields.php
Normal file
65
extensions/likes/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Likes\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public static int $maxLikes = 4;
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isLiked')
|
||||
->visible(false)
|
||||
->writable(fn (Post $post, Context $context) => $context->getActor()->can('like', $post))
|
||||
->set(function (Post $post, bool $liked, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||
|
||||
if ($liked && ! $currentlyLiked) {
|
||||
$post->likes()->attach($actor->id);
|
||||
|
||||
$post->raise(new PostWasLiked($post, $actor));
|
||||
} elseif ($currentlyLiked) {
|
||||
$post->likes()->detach($actor->id);
|
||||
|
||||
$post->raise(new PostWasUnliked($post, $actor));
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Boolean::make('canLike')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('like', $post)),
|
||||
Schema\Integer::make('likesCount')
|
||||
->countRelation('likes'),
|
||||
|
||||
Schema\Relationship\ToMany::make('likes')
|
||||
->type('users')
|
||||
->includable()
|
||||
->scope(function (Builder $query, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
// So that we can tell if the current user has liked the post.
|
||||
$query
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
->limit(static::$maxLikes);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Likes\Listener;
|
||||
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Saving;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class SaveLikesToDatabase
|
||||
{
|
||||
public function subscribe(Dispatcher $events): void
|
||||
{
|
||||
$events->listen(Saving::class, $this->whenPostIsSaving(...));
|
||||
$events->listen(Deleted::class, $this->whenPostIsDeleted(...));
|
||||
}
|
||||
|
||||
public function whenPostIsSaving(Saving $event): void
|
||||
{
|
||||
$post = $event->post;
|
||||
$data = $event->data;
|
||||
|
||||
if ($post->exists && isset($data['attributes']['isLiked'])) {
|
||||
$actor = $event->actor;
|
||||
$liked = (bool) $data['attributes']['isLiked'];
|
||||
|
||||
$actor->assertCan('like', $post);
|
||||
|
||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||
|
||||
if ($liked && ! $currentlyLiked) {
|
||||
$post->likes()->attach($actor->id);
|
||||
|
||||
$post->raise(new PostWasLiked($post, $actor));
|
||||
} elseif ($currentlyLiked) {
|
||||
$post->likes()->detach($actor->id);
|
||||
|
||||
$post->raise(new PostWasUnliked($post, $actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function whenPostIsDeleted(Deleted $event): void
|
||||
{
|
||||
$event->post->likes()->detach();
|
||||
}
|
||||
}
|
@@ -10,9 +10,13 @@
|
||||
namespace Flarum\Likes\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class LikePostTest extends TestCase
|
||||
@@ -26,21 +30,21 @@ class LikePostTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'Acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'number' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 3, 'number' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 5, 'number' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'discussionRenamed', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 6, 'number' => 4, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 6, 'name_singular' => 'Acme1', 'name_plural' => 'Acme1', 'is_hidden' => 0]
|
||||
],
|
||||
@@ -72,7 +76,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertNotNull($post->likes->where('id', $authenticatedAs)->first(), $message);
|
||||
}
|
||||
|
||||
@@ -92,7 +96,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $message);
|
||||
$this->assertContainsEquals($response->getStatusCode(), [401, 403], $message);
|
||||
$this->assertNull($post->likes->where('id', $authenticatedAs)->first());
|
||||
}
|
||||
|
||||
|
@@ -10,10 +10,13 @@
|
||||
namespace Flarum\Likes\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -30,13 +33,13 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 102, 'username' => 'user102', 'email' => '102@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 103, 'username' => 'user103', 'email' => '103@machine.local', 'is_email_confirmed' => 1],
|
||||
@@ -132,7 +135,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -159,7 +162,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data[0]['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -170,7 +173,7 @@ class ListPostsTest extends TestCase
|
||||
* @dataProvider likesIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||
{
|
||||
// Show discussion endpoint
|
||||
$response = $this->send(
|
||||
@@ -181,22 +184,27 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$included = json_decode($body, true)['included'] ?? [];
|
||||
|
||||
$likes = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['likes']['data'];
|
||||
->first()['relationships']['likes']['data'] ?? null;
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertNotNull($likes, $body);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['attributes']['likesCount']);
|
||||
->first()['attributes']['likesCount'] ?? null, $body);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'), $body);
|
||||
}
|
||||
|
||||
public function likesIncludeProvider(): array
|
||||
@@ -204,7 +212,7 @@ class ListPostsTest extends TestCase
|
||||
return [
|
||||
['posts,posts.likes'],
|
||||
['posts.likes'],
|
||||
[''],
|
||||
[null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -7,10 +7,10 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Lock\Access;
|
||||
@@ -33,24 +33,38 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(DiscussionLockedBlueprint::class, BasicDiscussionSerializer::class, ['alert']),
|
||||
->type(DiscussionLockedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->cast('is_locked', 'bool'),
|
||||
|
||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||
->attribute('isLocked', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return $discussion->is_locked;
|
||||
})
|
||||
->attribute('canLock', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return $serializer->getActor()->can('lock', $discussion);
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isLocked')
|
||||
->writable(fn (Discussion $discussion, Context $context) => $context->getActor()->can('lock', $discussion))
|
||||
->set(function (Discussion $discussion, bool $isLocked, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
if ($discussion->is_locked === $isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_locked = $isLocked;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_locked
|
||||
? new DiscussionWasLocked($discussion, $actor)
|
||||
: new DiscussionWasUnlocked($discussion, $actor)
|
||||
);
|
||||
}),
|
||||
Schema\Boolean::make('canLock')
|
||||
->get(fn (Discussion $discussion, Context $context) => $context->getActor()->can('lock', $discussion)),
|
||||
]),
|
||||
|
||||
(new Extend\Post())
|
||||
->type(DiscussionLockedPost::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, Listener\SaveLockedToDatabase::class)
|
||||
->listen(DiscussionWasLocked::class, Listener\CreatePostWhenDiscussionIsLocked::class)
|
||||
->listen(DiscussionWasUnlocked::class, Listener\CreatePostWhenDiscussionIsUnlocked::class),
|
||||
|
||||
|
2
extensions/lock/js/dist/forum.js
generated
vendored
2
extensions/lock/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>D});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","forum/components/Notification");var c=o.n(s);class a extends(c()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var l=o.n(i);const u=flarum.reg.get("core","common/components/Badge");var d=o.n(u);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var y=o.n(b);const _=flarum.reg.get("core","common/extenders");var v=o.n(_);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=flarum.reg.get("core","common/query/IGambit"),S=flarum.reg.get("core","common/app");var j=o.n(S);class w extends P.BooleanGambit{key(){return j().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",w);const D=[(new(v().Search)).gambit("discussions",w),(new(v().PostTypes)).add("discussionLocked",h),new(v().Model)(l()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,t.extend)(l().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(k(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(y(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
|
||||
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>N});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","common/models/Discussion");var c=o.n(s);const a=flarum.reg.get("core","common/components/Badge");var i=o.n(a);const l=flarum.reg.get("core","forum/utils/DiscussionControls");var u=o.n(l);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=o.n(d);const k=flarum.reg.get("core","common/components/Button");var g=o.n(k);const p=flarum.reg.get("core","common/extenders");var b=o.n(p);const y=flarum.reg.get("core","forum/components/EventPost");var _=o.n(y);class v extends(_()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",v);const x=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var h=o.n(L);class P extends x.BooleanGambit{key(){return h().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",P);const w=[(new(b().Search)).gambit("discussions",P)],S=flarum.reg.get("core","forum/components/Notification");var j=o.n(S);class D extends(j()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",D);const N=[...w,(new(b().PostTypes)).add("discussionLocked",v),(new(b().Notification)).add("discussionLocked",D),new(b().Model)(c()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{(0,t.extend)(c().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(i(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(u(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.isLocked()?"unlock":"lock","_button"))))})),u().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/lock/js/dist/forum.js.map
generated
vendored
2
extensions/lock/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import Discussion from 'flarum/common/models/Discussion';
|
||||
import DiscussionLockedPost from './components/DiscussionLockedPost';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
@@ -10,6 +11,9 @@ export default [
|
||||
new Extend.PostTypes() //
|
||||
.add('discussionLocked', DiscussionLockedPost),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('discussionLocked', DiscussionLockedNotification),
|
||||
|
||||
new Extend.Model(Discussion) //
|
||||
.attribute<boolean>('isLocked')
|
||||
.attribute<boolean>('canLock'),
|
||||
|
@@ -1,15 +1,12 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
import addLockBadge from './addLockBadge';
|
||||
import addLockControl from './addLockControl';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-lock', () => {
|
||||
app.notificationComponents.discussionLocked = DiscussionLockedNotification;
|
||||
|
||||
addLockBadge();
|
||||
addLockControl();
|
||||
|
||||
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Lock\Listener;
|
||||
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Lock\Event\DiscussionWasLocked;
|
||||
use Flarum\Lock\Event\DiscussionWasUnlocked;
|
||||
|
||||
class SaveLockedToDatabase
|
||||
{
|
||||
public function handle(Saving $event): void
|
||||
{
|
||||
if (isset($event->data['attributes']['isLocked'])) {
|
||||
$isLocked = (bool) $event->data['attributes']['isLocked'];
|
||||
$discussion = $event->discussion;
|
||||
$actor = $event->actor;
|
||||
|
||||
$actor->assertCan('lock', $discussion);
|
||||
|
||||
if ((bool) $discussion->is_locked === $isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_locked = $isLocked;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_locked
|
||||
? new DiscussionWasLocked($discussion, $actor)
|
||||
: new DiscussionWasUnlocked($discussion, $actor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,16 +9,14 @@
|
||||
|
||||
namespace Flarum\Mentions;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicPostSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
@@ -27,7 +25,6 @@ use Flarum\Post\Event\Revised;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -60,50 +57,49 @@ return [
|
||||
->namespace('flarum-mentions', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(Notification\PostMentionedBlueprint::class, PostSerializer::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, PostSerializer::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
->type(Notification\PostMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionedBy', BasicPostSerializer::class)
|
||||
->hasMany('mentionsPosts', BasicPostSerializer::class)
|
||||
->hasMany('mentionsUsers', BasicUserSerializer::class)
|
||||
->hasMany('mentionsGroups', GroupSerializer::class)
|
||||
->attribute('mentionedByCount', function (BasicPostSerializer $serializer, Post $post) {
|
||||
// Only if it was eager loaded.
|
||||
return $post->getAttribute('mentioned_by_count') ?? 0;
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoad(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->load([
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
|
||||
])
|
||||
->loadWhere('posts.mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoadWhenIncluded([
|
||||
'firstPost' => [
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
|
||||
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
|
||||
],
|
||||
'lastPost' => [
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
|
||||
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
|
||||
],
|
||||
]);
|
||||
})
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show {
|
||||
return $endpoint->addDefaultInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->eagerLoadWhenIncluded([
|
||||
'posts' => [
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
|
||||
],
|
||||
]);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
|
||||
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
|
||||
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('canMentionGroups')
|
||||
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->get(fn (User $user) => $user->can('mentionGroups')),
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
// We wouldn't normally need to eager load on a single model,
|
||||
// but we do so here for visibility scoping.
|
||||
->loadWhere('mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
->load(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups'])
|
||||
->loadWhere('mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\Settings)
|
||||
->serializeToForum('allowUsernameMentionFormat', 'flarum-mentions.allow_username_format', 'boolval'),
|
||||
|
||||
@@ -119,11 +115,6 @@ return [
|
||||
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
|
||||
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {
|
||||
return $user->can('mentionGroups');
|
||||
}),
|
||||
|
||||
// Tag mentions
|
||||
(new Extend\Conditional())
|
||||
->whenExtensionEnabled('flarum-tags', fn () => [
|
||||
@@ -131,18 +122,23 @@ return [
|
||||
->render(Formatter\FormatTagMentions::class)
|
||||
->unparse(Formatter\UnparseTagMentions::class),
|
||||
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionsTags', TagSerializer::class),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->load(['posts.mentionsTags']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsTags', 'lastPost.mentionsTags',
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Relationship\ToMany::make('mentionsTags')
|
||||
->type('tags'),
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->load(['mentionsTags']),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show {
|
||||
return $endpoint->eagerLoadWhenIncluded(['posts' => ['posts.mentionsTags']]);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoadWhenIncluded(['firstPost' => ['firstPost.mentionsTags'], 'lastPost' => ['lastPost.mentionsTags']]);
|
||||
}),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->eagerLoad(['mentionsTags']);
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
2
extensions/mentions/js/dist/forum.js
generated
vendored
2
extensions/mentions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,6 +2,9 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import MentionsUserPage from './components/MentionsUserPage';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
@@ -11,6 +14,11 @@ export default [
|
||||
.hasMany<Post>('mentionedBy')
|
||||
.attribute<number>('mentionedByCount'),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postMentioned', PostMentionedNotification)
|
||||
.add('userMentioned', UserMentionedNotification)
|
||||
.add('groupMentioned', GroupMentionedNotification),
|
||||
|
||||
new Extend.Model(User) //
|
||||
.attribute<boolean>('canMentionGroups'),
|
||||
];
|
||||
|
@@ -9,9 +9,6 @@ import addMentionedByList from './addMentionedByList';
|
||||
import addPostReplyAction from './addPostReplyAction';
|
||||
import addPostQuoteButton from './addPostQuoteButton';
|
||||
import addComposerAutocomplete from './addComposerAutocomplete';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
import MentionFormats from './mentionables/formats/MentionFormats';
|
||||
import UserPage from 'flarum/forum/components/UserPage';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
@@ -40,10 +37,6 @@ app.initializers.add('flarum-mentions', function () {
|
||||
// posts or users that the user could mention.
|
||||
addComposerAutocomplete();
|
||||
|
||||
app.notificationComponents.postMentioned = PostMentionedNotification;
|
||||
app.notificationComponents.userMentioned = UserMentionedNotification;
|
||||
app.notificationComponents.groupMentioned = GroupMentionedNotification;
|
||||
|
||||
// Add notification preferences.
|
||||
extend('flarum/forum/components/NotificationGrid', 'notificationTypes', function (items) {
|
||||
items.add('postMentioned', {
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_post` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('post_mentions_post', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -16,10 +16,9 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
$schema->table('post_mentions_user', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Mentions\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Apply visibility permissions to API data's mentionedBy relationship.
|
||||
* And limit mentionedBy to 3 posts only for performance reasons.
|
||||
*/
|
||||
class LoadMentionedByRelationship
|
||||
{
|
||||
public static int $maxMentionedBy = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$query
|
||||
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsUsers'])
|
||||
->whereVisibleTo($actor)
|
||||
->oldest()
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxMentionedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation(AbstractSerializeController $controller, mixed $data, ServerRequestInterface $request): array
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// We do this because the ShowDiscussionController manipulates the posts
|
||||
// in a way that some of them are just ids.
|
||||
$loadable = $data->posts->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
|
||||
// firstPost and lastPost might have been included in the API response,
|
||||
// so we have to make sure counts are also loaded for them.
|
||||
if ($data->firstPost) {
|
||||
$loadable->push($data->firstPost);
|
||||
}
|
||||
|
||||
if ($data->lastPost) {
|
||||
$loadable->push($data->lastPost);
|
||||
}
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount([
|
||||
'mentionedBy' => function ($query) use ($actor) {
|
||||
return $query->whereVisibleTo($actor);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
37
extensions/mentions/src/Api/PostResourceFields.php
Normal file
37
extensions/mentions/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Mentions\Api;
|
||||
|
||||
use Flarum\Api\Schema;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public static int $maxMentionedBy = 4;
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('mentionedByCount')
|
||||
->countRelation('mentionedBy'),
|
||||
|
||||
Schema\Relationship\ToMany::make('mentionedBy')
|
||||
->type('posts')
|
||||
->includable()
|
||||
->scope(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
Schema\Relationship\ToMany::make('mentionsPosts')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToMany::make('mentionsUsers')
|
||||
->type('users'),
|
||||
Schema\Relationship\ToMany::make('mentionsGroups')
|
||||
->type('groups'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -10,8 +10,10 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
@@ -30,14 +32,14 @@ class GroupMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p>One of the <GROUPMENTION groupname="Mods" id="4">@"Mods"#g4</GROUPMENTION> will look at this</p></r>'],
|
||||
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="100">@"OldGroupName"#g100</GROUPMENTION></p></r>'],
|
||||
['id' => 7, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="11">@"OldGroupName"#g11</GROUPMENTION></p></r>'],
|
||||
@@ -53,7 +55,7 @@ class GroupMentionsTest extends TestCase
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
|
||||
['group_id' => 9, 'permission' => 'mentionGroups'],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 9, 'name_singular' => 'HasPermissionToMentionGroups', 'name_plural' => 'test'],
|
||||
['id' => 10, 'name_singular' => 'Hidden', 'name_plural' => 'Ninjas', 'icon' => 'fas fa-wrench', 'color' => '#000', 'is_hidden' => 1],
|
||||
['id' => 11, 'name_singular' => 'Fresh Name', 'name_plural' => 'Fresh Name', 'color' => '#ccc', 'icon' => 'fas fa-users', 'is_hidden' => 0]
|
||||
@@ -91,11 +93,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"InvalidGroup"#g99',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
],
|
||||
],
|
||||
@@ -166,11 +169,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -198,11 +202,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Admins"#g1 @"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -232,11 +237,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Members"#g3 @"Guests"#g2',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -288,11 +294,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -319,11 +326,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -350,11 +358,12 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Ninjas"#g10',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -381,6 +390,7 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'New content with @"Mods"#g4 mention',
|
||||
],
|
||||
|
@@ -10,9 +10,12 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -29,10 +32,10 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
@@ -43,7 +46,7 @@ class ListPostsTest extends TestCase
|
||||
['post_id' => 3, 'mentions_user_id' => 1],
|
||||
['post_id' => 4, 'mentions_user_id' => 2]
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
]
|
||||
]);
|
||||
@@ -112,10 +115,10 @@ class ListPostsTest extends TestCase
|
||||
protected function prepareMentionedByData(): void
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],
|
||||
@@ -167,7 +170,7 @@ class ListPostsTest extends TestCase
|
||||
$mentionedBy = $data['relationships']['mentionedBy']['data'];
|
||||
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -187,14 +190,14 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$mentionedBy = $data[0]['relationships']['mentionedBy']['data'];
|
||||
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -203,7 +206,7 @@ class ListPostsTest extends TestCase
|
||||
* @dataProvider mentionedByIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
@@ -216,15 +219,18 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
$included = json_decode($body = $response->getBody()->getContents(), true)['included'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$mentionedBy = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['mentionedBy']['data'];
|
||||
->first()['relationships']['mentionedBy']['data'] ?? null;
|
||||
|
||||
$this->assertNotNull($mentionedBy, 'Mentioned by relation not included');
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -234,7 +240,7 @@ class ListPostsTest extends TestCase
|
||||
return [
|
||||
['posts,posts.mentionedBy'],
|
||||
['posts.mentionedBy'],
|
||||
[''],
|
||||
[null],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -250,10 +256,54 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$this->assertEquals(0, $data['attributes']['mentionedByCount']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mentioned_by_count_works_on_show_endpoint()
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts/101', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$this->assertEquals(10, $data['attributes']['mentionedByCount']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mentioned_by_count_works_on_list_endpoint()
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'filter' => ['discussion' => 100],
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$post101 = collect($data)->where('id', 101)->first();
|
||||
$post112 = collect($data)->where('id', 112)->first();
|
||||
|
||||
$this->assertEquals(10, $post101['attributes']['mentionedByCount']);
|
||||
$this->assertEquals(0, $post112['attributes']['mentionedByCount']);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Formatter\Formatter;
|
||||
use Flarum\Post\CommentPost;
|
||||
@@ -33,16 +34,16 @@ class PostMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
['id' => 50, 'title' => __CLASS__, 'is_private' => true, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
|
||||
['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="potato" id="4" number="3" discussionid="2" username="potato">@potato#4</POSTMENTION></r>'],
|
||||
['id' => 6, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="i_am_a_deleted_user" id="7" number="5" discussionid="2" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#p7</POSTMENTION></r>'],
|
||||
@@ -82,11 +83,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato#4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -113,11 +115,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"POTATO$"#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -144,11 +147,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato"#p50',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -175,11 +179,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@“POTATO$”#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -206,11 +211,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"franzofflarum"#p215',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -237,11 +243,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"TOBY$"#p5 @"flarum"#2015 @"franzofflarum"#220 @"POTATO$"#3 @"POTATO$"#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -384,11 +391,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad "#p6 User"#p9',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -436,11 +444,12 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -467,6 +476,7 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
@@ -495,6 +505,7 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
@@ -523,6 +534,7 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"acme"#p11',
|
||||
],
|
||||
|
@@ -10,10 +10,14 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class TagMentionsTest extends TestCase
|
||||
{
|
||||
@@ -26,20 +30,20 @@ class TagMentionsTest extends TestCase
|
||||
$this->extension('flarum-tags', 'flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><TAGMENTION id="1" slug="test_old_slug" tagname="TestOldName">#test_old_slug</TAGMENTION></r>'],
|
||||
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="3" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
|
||||
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="2020" slug="i_am_a_deleted_tag" tagname="i_am_a_deleted_tag">#i_am_a_deleted_tag</TAGMENTION></r>'],
|
||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="5" slug="laravel">#laravel</TAGMENTION></r>'],
|
||||
],
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['id' => 1, 'name' => 'Test', 'slug' => 'test', 'is_restricted' => 0],
|
||||
['id' => 2, 'name' => 'Flarum', 'slug' => 'flarum', 'is_restricted' => 0],
|
||||
['id' => 3, 'name' => 'Support', 'slug' => 'support', 'is_restricted' => 0],
|
||||
@@ -68,11 +72,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#flarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -96,11 +101,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#戦い',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -125,11 +131,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#franzofflarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -155,11 +162,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#test',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -183,11 +191,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#dev',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -211,11 +220,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#dev',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -239,11 +249,12 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#test #flarum #support #laravel #franzofflarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -365,6 +376,7 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#laravel',
|
||||
],
|
||||
|
@@ -10,8 +10,10 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
@@ -31,16 +33,16 @@ class UserMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="TobyFlarum___" id="4" username="toby">@tobyuuu</USERMENTION></r>'],
|
||||
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="i_am_a_deleted_user" id="2021" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#2021</USERMENTION></r>'],
|
||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="Bad "#p6 User" id="5">@"Bad "#p6 User"#5</USERMENTION></r>'],
|
||||
@@ -72,11 +74,12 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -105,11 +108,12 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -136,6 +140,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"POTATO$"#3',
|
||||
],
|
||||
@@ -167,6 +172,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@“POTATO$”#3',
|
||||
],
|
||||
@@ -198,6 +204,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"franzofflarum"#82',
|
||||
],
|
||||
@@ -229,6 +236,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"TOBY$"#4 @"POTATO$"#p4 @"franzofflarum"#82 @"POTATO$"#3',
|
||||
],
|
||||
@@ -282,6 +290,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato_"#3',
|
||||
],
|
||||
@@ -312,6 +321,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato_"#3',
|
||||
],
|
||||
@@ -367,6 +377,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad "#p6 User"#5',
|
||||
],
|
||||
@@ -419,6 +430,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
@@ -450,6 +462,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
@@ -478,6 +491,7 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
|
@@ -9,14 +9,13 @@
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Nicknames\Access\UserPolicy;
|
||||
use Flarum\Nicknames\Api\UserResourceFields;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UserValidator;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
@@ -33,13 +32,9 @@ return [
|
||||
(new Extend\User())
|
||||
->displayNameDriver('nickname', NicknameDriver::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, SaveNicknameToDatabase::class),
|
||||
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->attribute('canEditNickname', function (UserSerializer $serializer, User $user) {
|
||||
return $serializer->getActor()->can('editNickname', $user);
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class)
|
||||
->field('username', UserResourceFields::username(...)),
|
||||
|
||||
(new Extend\Settings())
|
||||
->default('flarum-nicknames.set_on_registration', true)
|
||||
@@ -50,9 +45,6 @@ return [
|
||||
->serializeToForum('setNicknameOnRegistration', 'flarum-nicknames.set_on_registration', 'boolval')
|
||||
->serializeToForum('randomizeUsernameOnRegistration', 'flarum-nicknames.random_username', 'boolval'),
|
||||
|
||||
(new Extend\Validator(UserValidator::class))
|
||||
->configure(AddNicknameValidation::class),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->setFulltext(UserSearcher::class, NicknameFullTextFilter::class),
|
||||
|
||||
|
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\UserValidator;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class AddNicknameValidation
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(UserValidator $flarumValidator, Validator $validator): void
|
||||
{
|
||||
$idSuffix = $flarumValidator->getUser() ? ','.$flarumValidator->getUser()->id : '';
|
||||
$rules = $validator->getRules();
|
||||
|
||||
$rules['nickname'] = [
|
||||
function ($attribute, $value, $fail) {
|
||||
$regex = $this->settings->get('flarum-nicknames.regex');
|
||||
if ($regex && ! preg_match_all("/$regex/", $value)) {
|
||||
$fail($this->translator->trans('flarum-nicknames.api.invalid_nickname_message'));
|
||||
}
|
||||
},
|
||||
'min:'.$this->settings->get('flarum-nicknames.min'),
|
||||
'max:'.$this->settings->get('flarum-nicknames.max'),
|
||||
'nullable'
|
||||
];
|
||||
|
||||
if ($this->settings->get('flarum-nicknames.unique')) {
|
||||
$rules['nickname'][] = 'unique:users,username'.$idSuffix;
|
||||
$rules['nickname'][] = 'unique:users,nickname'.$idSuffix;
|
||||
$rules['username'][] = 'unique:users,nickname'.$idSuffix;
|
||||
}
|
||||
|
||||
$validator->setRules($rules);
|
||||
}
|
||||
}
|
61
extensions/nicknames/src/Api/UserResourceFields.php
Normal file
61
extensions/nicknames/src/Api/UserResourceFields.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Nicknames\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserResourceFields
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
$regex = $this->settings->get('flarum-nicknames.regex');
|
||||
|
||||
if (! empty($regex)) {
|
||||
$regex = "/$regex/";
|
||||
}
|
||||
|
||||
return [
|
||||
Schema\Str::make('nickname')
|
||||
->visible(false)
|
||||
->writable(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('editNickname', $user);
|
||||
})
|
||||
->nullable()
|
||||
->regex($regex ?? '', ! empty($regex))
|
||||
->minLength($this->settings->get('flarum-nicknames.min'))
|
||||
->maxLength($this->settings->get('flarum-nicknames.max'))
|
||||
->unique('users', 'nickname', true, (bool) $this->settings->get('flarum-nicknames.unique'))
|
||||
->unique('users', 'username', true, (bool) $this->settings->get('flarum-nicknames.unique'))
|
||||
->validationMessages([
|
||||
'nickname.regex' => $this->translator->trans('flarum-nicknames.api.invalid_nickname_message'),
|
||||
])
|
||||
->set(function (User $user, ?string $nickname) {
|
||||
$user->nickname = $user->username === $nickname ? null : $nickname;
|
||||
}),
|
||||
Schema\Boolean::make('canEditNickname')
|
||||
->get(fn (User $user, Context $context) => $context->getActor()->can('editNickname', $user)),
|
||||
];
|
||||
}
|
||||
|
||||
public static function username(Schema\Str $field): Schema\Str
|
||||
{
|
||||
return $field->unique('users', 'nickname', true, (bool) resolve(SettingsRepositoryInterface::class)->get('flarum-nicknames.unique'));
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Nicknames;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SaveNicknameToDatabase
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Saving $event): void
|
||||
{
|
||||
$user = $event->user;
|
||||
$data = $event->data;
|
||||
$actor = $event->actor;
|
||||
$attributes = Arr::get($data, 'attributes', []);
|
||||
|
||||
if (isset($attributes['nickname'])) {
|
||||
$actor->assertCan('editNickname', $user);
|
||||
|
||||
$nickname = $attributes['nickname'];
|
||||
|
||||
// If the user sets their nickname back to the username
|
||||
// set the nickname to null so that it just falls back to the username
|
||||
$user->nickname = $user->username === $nickname ? null : $nickname;
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Nicknames\Tests\integration;
|
||||
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
@@ -27,7 +28,7 @@ class UpdateTest extends TestCase
|
||||
|
||||
$this->extension('flarum-nicknames');
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
],
|
||||
]);
|
||||
@@ -45,6 +46,7 @@ class UpdateTest extends TestCase
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'users',
|
||||
'attributes' => [
|
||||
'nickname' => 'new nickname',
|
||||
],
|
||||
@@ -53,7 +55,7 @@ class UpdateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +74,7 @@ class UpdateTest extends TestCase
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'users',
|
||||
'attributes' => [
|
||||
'nickname' => 'new nickname',
|
||||
],
|
||||
@@ -80,8 +83,36 @@ class UpdateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
|
||||
$this->assertEquals('new nickname', User::find(2)->nickname);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function cant_edit_nickname_if_invalid_regex()
|
||||
{
|
||||
$this->setting('flarum-nicknames.set_on_registration', true);
|
||||
$this->setting('flarum-nicknames.regex', '^[A-z]+$');
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/users/2', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'users',
|
||||
'attributes' => [
|
||||
'nickname' => '007',
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(422, $response->getStatusCode(), $body);
|
||||
$this->assertStringContainsString($this->app()->getContainer()->make(TranslatorInterface::class)->trans('flarum-nicknames.api.invalid_nickname_message'), $body);
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::where('username', 'test')->firstOrFail();
|
||||
@@ -72,7 +72,7 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +94,7 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(422, $response->getStatusCode());
|
||||
$this->assertEquals(422, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +116,6 @@ class RegisterTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\ExtensionManager;
|
||||
|
||||
use Flarum\Extend;
|
||||
use Flarum\ExtensionManager\Api\Resource\TaskResource;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Frontend\Document;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
@@ -25,9 +26,10 @@ return [
|
||||
->post('/extension-manager/minor-update', 'extension-manager.minor-update', Api\Controller\MinorUpdateController::class)
|
||||
->post('/extension-manager/major-update', 'extension-manager.major-update', Api\Controller\MajorUpdateController::class)
|
||||
->post('/extension-manager/global-update', 'extension-manager.global-update', Api\Controller\GlobalUpdateController::class)
|
||||
->get('/extension-manager-tasks', 'extension-manager.tasks.index', Api\Controller\ListTasksController::class)
|
||||
->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class),
|
||||
|
||||
new Extend\ApiResource(TaskResource::class),
|
||||
|
||||
(new Extend\Frontend('admin'))
|
||||
->css(__DIR__.'/less/admin.less')
|
||||
->js(__DIR__.'/js/dist/admin.js')
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import app from 'flarum/admin/app';
|
||||
import type Mithril from 'mithril';
|
||||
import Component, { type ComponentAttrs } from 'flarum/common/Component';
|
||||
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
|
||||
import { type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
|
||||
import FormGroup, { type CommonFieldOptions } from 'flarum/common/components/FormGroup';
|
||||
import AdminPage from 'flarum/admin/components/AdminPage';
|
||||
import type ItemList from 'flarum/common/utils/ItemList';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
@@ -49,8 +50,8 @@ export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson =
|
||||
];
|
||||
}
|
||||
|
||||
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children> {
|
||||
return AdminPage.prototype.customSettingComponents();
|
||||
customSettingComponents(): ItemList<(attributes: CommonFieldOptions) => Mithril.Children> {
|
||||
return FormGroup.prototype.customFieldComponents();
|
||||
}
|
||||
|
||||
setting(key: string) {
|
||||
|
@@ -22,7 +22,7 @@ export default class QueueState {
|
||||
|
||||
return app.store.find<Task[]>('extension-manager-tasks', params || {}).then((data) => {
|
||||
this.tasks = data;
|
||||
this.total = data.payload.meta?.total;
|
||||
this.total = data.payload.meta?.total || 0;
|
||||
|
||||
m.redraw();
|
||||
|
||||
|
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\ExtensionManager\Api\Controller;
|
||||
|
||||
use Flarum\Api\Controller\AbstractListController;
|
||||
use Flarum\ExtensionManager\Api\Serializer\TaskSerializer;
|
||||
use Flarum\ExtensionManager\Task\Task;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListTasksController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = TaskSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertAdmin();
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
|
||||
$results = Task::query()
|
||||
->latest('id')
|
||||
->offset($offset)
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$total = Task::query()->count();
|
||||
|
||||
$document->addMeta('total', (string) $total);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('extension-manager.tasks.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$total
|
||||
);
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
60
extensions/package-manager/src/Api/Resource/TaskResource.php
Normal file
60
extensions/package-manager/src/Api/Resource/TaskResource.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\ExtensionManager\Api\Resource;
|
||||
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\ExtensionManager\Task\Task;
|
||||
|
||||
class TaskResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function type(): string
|
||||
{
|
||||
return 'package-manager-tasks';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Task::class;
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Index::make()
|
||||
->defaultSort('-createdAt')
|
||||
->paginate(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('status'),
|
||||
Schema\Str::make('operation'),
|
||||
Schema\Str::make('command'),
|
||||
Schema\Str::make('package'),
|
||||
Schema\Str::make('output'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
Schema\DateTime::make('startedAt'),
|
||||
Schema\DateTime::make('finishedAt'),
|
||||
Schema\Number::make('peakMemoryUsed'),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('createdAt'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\ExtensionManager\Api\Serializer;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\ExtensionManager\Task\Task;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TaskSerializer extends AbstractSerializer
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $type = 'extension-manager-tasks';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param Task $model
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes($model): array
|
||||
{
|
||||
if (! ($model instanceof Task)) {
|
||||
throw new InvalidArgumentException(
|
||||
get_class($this).' can only serialize instances of '.Task::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $model->status,
|
||||
'operation' => $model->operation,
|
||||
'command' => $model->command,
|
||||
'package' => $model->package,
|
||||
'output' => $model->output,
|
||||
'guessedCause' => $model->guessed_cause,
|
||||
'createdAt' => $model->created_at,
|
||||
'startedAt' => $model->started_at,
|
||||
'finishedAt' => $model->finished_at,
|
||||
'peakMemoryUsed' => $model->peak_memory_used,
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\ExtensionManager\Task;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class TaskRepository
|
||||
{
|
||||
/**
|
||||
* @return Builder
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
return Task::query();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param User $actor
|
||||
* @return Task
|
||||
*/
|
||||
public function findOrFail($id, User $actor = null): Task
|
||||
{
|
||||
return Task::findOrFail($id);
|
||||
}
|
||||
}
|
2
extensions/statistics/js/dist/admin.js
generated
vendored
2
extensions/statistics/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/statistics/js/dist/admin.js.map
generated
vendored
2
extensions/statistics/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
import app from 'flarum/admin/app';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import generateElementId from 'flarum/admin/utils/generateElementId';
|
||||
import generateElementId from 'flarum/common/utils/generateElementId';
|
||||
import FormModal, { IFormModalAttrs } from 'flarum/common/components/FormModal';
|
||||
|
||||
import Mithril from 'mithril';
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Statistics\Api\Controller;
|
||||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\Exception\InvalidParameterException;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\RegisteredTypesScope;
|
||||
@@ -24,7 +25,6 @@ use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tobscure\JsonApi\Exception\InvalidParameterException;
|
||||
|
||||
class ShowStatisticsData implements RequestHandlerInterface
|
||||
{
|
||||
@@ -130,12 +130,16 @@ class ShowStatisticsData implements RequestHandlerInterface
|
||||
$endDate = new DateTime();
|
||||
}
|
||||
|
||||
// if within the last 24 hours, group by hour
|
||||
$format = 'CASE WHEN '.$column.' > ? THEN \'%Y-%m-%d %H:00:00\' ELSE \'%Y-%m-%d\' END';
|
||||
$dbFormattedDatetime = match ($query->getConnection()->getDriverName()) {
|
||||
'sqlite' => 'strftime('.$format.', '.$column.')',
|
||||
default => 'DATE_FORMAT('.$column.', '.$format.')',
|
||||
};
|
||||
|
||||
$results = $query
|
||||
->selectRaw(
|
||||
'DATE_FORMAT(
|
||||
@date := '.$column.',
|
||||
IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 24 hours, group by hour
|
||||
) as time_group',
|
||||
$dbFormattedDatetime.' as time_group',
|
||||
[new DateTime('-25 hours')]
|
||||
)
|
||||
->selectRaw('COUNT(id) as count')
|
||||
|
@@ -10,8 +10,11 @@
|
||||
namespace Flarum\Statistics\tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class CanRequestCustomTimedStatisticsTest extends TestCase
|
||||
{
|
||||
@@ -36,18 +39,18 @@ class CanRequestCustomTimedStatisticsTest extends TestCase
|
||||
protected function getDatabaseData(): array
|
||||
{
|
||||
return [
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()],
|
||||
['id' => 2, 'username' => 'normal', 'email' => 'normal@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()->subDays(1)],
|
||||
['id' => 3, 'username' => 'normal2', 'email' => 'normal2@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()->subDays(2)],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 4, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(2), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)],
|
||||
@@ -99,7 +102,7 @@ class CanRequestCustomTimedStatisticsTest extends TestCase
|
||||
|
||||
$body = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body['errors'][0]['detail'] ?? '');
|
||||
|
||||
$this->assertEquals(
|
||||
$data,
|
||||
|
@@ -10,8 +10,11 @@
|
||||
namespace Flarum\Statistics\tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class CanRequestLifetimeStatisticsTest extends TestCase
|
||||
{
|
||||
@@ -36,17 +39,17 @@ class CanRequestLifetimeStatisticsTest extends TestCase
|
||||
protected function getDatabaseData(): array
|
||||
{
|
||||
return [
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 2, 'username' => 'normal', 'email' => 'normal@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->subDays(1)],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => $this->nowTime, 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => $this->nowTime->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => $this->nowTime->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 4, 'title' => __CLASS__, 'created_at' => $this->nowTime->subDays(2), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1],
|
||||
@@ -75,9 +78,9 @@ class CanRequestLifetimeStatisticsTest extends TestCase
|
||||
|
||||
$this->assertEqualsCanonicalizing(
|
||||
[
|
||||
'users' => count($db['users']),
|
||||
'discussions' => count($db['discussions']),
|
||||
'posts' => count($db['posts']),
|
||||
'users' => count($db[User::class]),
|
||||
'discussions' => count($db[Discussion::class]),
|
||||
'posts' => count($db[Post::class]),
|
||||
],
|
||||
$body
|
||||
);
|
||||
|
@@ -10,8 +10,11 @@
|
||||
namespace Flarum\Statistics\tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class CanRequestTimedStatisticsTest extends TestCase
|
||||
{
|
||||
@@ -36,17 +39,17 @@ class CanRequestTimedStatisticsTest extends TestCase
|
||||
protected function getDatabaseData(): array
|
||||
{
|
||||
return [
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()],
|
||||
['id' => 2, 'username' => 'normal', 'email' => 'normal@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()->subDays(1)],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
['id' => 4, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(2), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)],
|
||||
|
@@ -7,17 +7,16 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Controller\ListDiscussionsController;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Sticky\Api\DiscussionResourceFields;
|
||||
use Flarum\Sticky\Event\DiscussionWasStickied;
|
||||
use Flarum\Sticky\Event\DiscussionWasUnstickied;
|
||||
use Flarum\Sticky\Listener;
|
||||
use Flarum\Sticky\Listener\SaveStickyToDatabase;
|
||||
use Flarum\Sticky\PinStickiedDiscussionsToTop;
|
||||
use Flarum\Sticky\Post\DiscussionStickiedPost;
|
||||
use Flarum\Sticky\Query\StickyFilter;
|
||||
@@ -27,30 +26,24 @@ return [
|
||||
->js(__DIR__.'/js/dist/forum.js')
|
||||
->css(__DIR__.'/less/forum.less'),
|
||||
|
||||
(new Extend\Frontend('admin'))
|
||||
->js(__DIR__.'/js/dist/admin.js'),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->cast('is_sticky', 'bool'),
|
||||
|
||||
(new Extend\Post())
|
||||
->type(DiscussionStickiedPost::class),
|
||||
|
||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||
->attribute('isSticky', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return (bool) $discussion->is_sticky;
|
||||
})
|
||||
->attribute('canSticky', function (DiscussionSerializer $serializer, $discussion) {
|
||||
return (bool) $serializer->getActor()->can('sticky', $discussion);
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(DiscussionResourceFields::class)
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->addDefaultInclude(['firstPost']);
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(ListDiscussionsController::class))
|
||||
->addInclude('firstPost'),
|
||||
|
||||
(new Extend\Frontend('admin'))
|
||||
->js(__DIR__.'/js/dist/admin.js'),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, SaveStickyToDatabase::class)
|
||||
->listen(DiscussionWasStickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasStickied'])
|
||||
->listen(DiscussionWasUnstickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasUnstickied']),
|
||||
|
||||
|
47
extensions/sticky/src/Api/DiscussionResourceFields.php
Normal file
47
extensions/sticky/src/Api/DiscussionResourceFields.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Sticky\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Sticky\Event\DiscussionWasStickied;
|
||||
use Flarum\Sticky\Event\DiscussionWasUnstickied;
|
||||
|
||||
class DiscussionResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isSticky')
|
||||
->writable(function (Discussion $discussion, Context $context) {
|
||||
return $context->updating()
|
||||
&& $context->getActor()->can('sticky', $discussion);
|
||||
})
|
||||
->set(function (Discussion $discussion, bool $isSticky, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
if ($discussion->is_sticky === $isSticky) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_sticky = $isSticky;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_sticky
|
||||
? new DiscussionWasStickied($discussion, $actor)
|
||||
: new DiscussionWasUnstickied($discussion, $actor)
|
||||
);
|
||||
}),
|
||||
Schema\Boolean::make('canSticky')
|
||||
->get(fn (Discussion $discussion, Context $context) => $context->getActor()->can('sticky', $discussion)),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Sticky\Listener;
|
||||
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Sticky\Event\DiscussionWasStickied;
|
||||
use Flarum\Sticky\Event\DiscussionWasUnstickied;
|
||||
|
||||
class SaveStickyToDatabase
|
||||
{
|
||||
public function handle(Saving $event): void
|
||||
{
|
||||
if (isset($event->data['attributes']['isSticky'])) {
|
||||
$isSticky = (bool) $event->data['attributes']['isSticky'];
|
||||
$discussion = $event->discussion;
|
||||
$actor = $event->actor;
|
||||
|
||||
$actor->assertCan('sticky', $discussion);
|
||||
|
||||
if ((bool) $discussion->is_sticky === $isSticky) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_sticky = $isSticky;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_sticky
|
||||
? new DiscussionWasStickied($discussion, $actor)
|
||||
: new DiscussionWasUnstickied($discussion, $actor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ namespace Flarum\Sticky;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Tags\Search\Filter\TagFilter;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class PinStickiedDiscussionsToTop
|
||||
{
|
||||
@@ -45,22 +46,26 @@ class PinStickiedDiscussionsToTop
|
||||
$sticky->where('is_sticky', true);
|
||||
unset($sticky->orders);
|
||||
|
||||
/** @var Builder $q */
|
||||
foreach ([$sticky, $query] as $q) {
|
||||
$read = $q->newQuery()
|
||||
->selectRaw('1')
|
||||
->from('discussion_user as sticky')
|
||||
->whereColumn('sticky.discussion_id', 'id')
|
||||
->where('sticky.user_id', '=', $state->getActor()->id)
|
||||
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
|
||||
|
||||
// Add the bindings manually (rather than as the second
|
||||
// argument in orderByRaw) for now due to a bug in Laravel which
|
||||
// would add the bindings in the wrong order.
|
||||
$q->selectRaw('(is_sticky and not exists ('.$read->toSql().') and last_posted_at > ?) as is_unread_sticky', array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]));
|
||||
}
|
||||
|
||||
$query->union($sticky);
|
||||
|
||||
$read = $query->newQuery()
|
||||
->selectRaw('1')
|
||||
->from('discussion_user as sticky')
|
||||
->whereColumn('sticky.discussion_id', 'id')
|
||||
->where('sticky.user_id', '=', $state->getActor()->id)
|
||||
->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number');
|
||||
$query->orderByDesc('is_unread_sticky');
|
||||
|
||||
// Add the bindings manually (rather than as the second
|
||||
// argument in orderByRaw) for now due to a bug in Laravel which
|
||||
// would add the bindings in the wrong order.
|
||||
$query->orderByRaw('is_sticky and not exists ('.$read->toSql().') and last_posted_at > ? desc')
|
||||
->addBinding(array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]), 'union');
|
||||
|
||||
$query->unionOrders = array_merge($query->unionOrders, $query->orders);
|
||||
$query->unionOrders = array_merge($query->unionOrders ?? [], $query->orders ?? []);
|
||||
$query->unionLimit = $query->limit;
|
||||
$query->unionOffset = $query->offset;
|
||||
|
||||
|
@@ -10,8 +10,11 @@
|
||||
namespace Flarum\Sticky\tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListDiscussionsTest extends TestCase
|
||||
@@ -25,12 +28,12 @@ class ListDiscussionsTest extends TestCase
|
||||
$this->extension('flarum-tags', 'flarum-sticky');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'Muralf_', 'email' => 'muralf_@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(2), 'last_posted_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(3), 'last_posted_at' => Carbon::now()->addMinute(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
@@ -40,7 +43,7 @@ class ListDiscussionsTest extends TestCase
|
||||
['discussion_id' => 1, 'user_id' => 3, 'last_read_post_number' => 1],
|
||||
['discussion_id' => 3, 'user_id' => 3, 'last_read_post_number' => 1],
|
||||
],
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['id' => 1, 'slug' => 'general', 'position' => 0, 'parent_id' => null]
|
||||
],
|
||||
'discussion_tag' => [
|
||||
@@ -59,11 +62,11 @@ class ListDiscussionsTest extends TestCase
|
||||
$this->request('GET', '/api/discussions')
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
$this->assertEqualsCanonicalizing([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -75,11 +78,11 @@ class ListDiscussionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
$this->assertEqualsCanonicalizing([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -91,11 +94,11 @@ class ListDiscussionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$this->assertEquals([2, 4, 3, 1], Arr::pluck($data['data'], 'id'));
|
||||
$this->assertEqualsCanonicalizing([2, 4, 3, 1], Arr::pluck($data['data'], 'id'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -111,10 +114,10 @@ class ListDiscussionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
$this->assertEqualsCanonicalizing([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Sticky\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class StickyDiscussionsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-sticky');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'Muralf_', 'email' => 'muralf_@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(2), 'last_posted_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(3), 'last_posted_at' => Carbon::now()->addMinute(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
['id' => 4, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(4), 'last_posted_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 5, 'name_singular' => 'Group', 'name_plural' => 'Groups', 'color' => 'blue'],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 2, 'group_id' => 5]
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 5, 'permission' => 'discussion.sticky'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider stickyDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_sticky_if_allowed(int $actorId, bool $allowed, bool $sticky)
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/1', [
|
||||
'authenticatedAs' => $actorId,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isSticky' => $sticky
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
if ($allowed) {
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($sticky, $json['data']['attributes']['isSticky']);
|
||||
} else {
|
||||
$this->assertEquals(403, $response->getStatusCode(), $body);
|
||||
}
|
||||
}
|
||||
|
||||
public static function stickyDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[1, true, true],
|
||||
[1, true, false],
|
||||
[2, true, true],
|
||||
[2, true, false],
|
||||
[3, false, true],
|
||||
[3, false, false],
|
||||
];
|
||||
}
|
||||
}
|
@@ -7,10 +7,8 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Discussion\UserState;
|
||||
@@ -20,6 +18,7 @@ use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
use Flarum\Post\Event\Restored;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Subscriptions\Api\UserResourceFields;
|
||||
use Flarum\Subscriptions\Filter\SubscriptionFilter;
|
||||
use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage;
|
||||
use Flarum\Subscriptions\Listener;
|
||||
@@ -48,18 +47,11 @@ return [
|
||||
->namespace('flarum-subscriptions', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(NewPostBlueprint::class, BasicDiscussionSerializer::class, ['alert', 'email'])
|
||||
->type(NewPostBlueprint::class, ['alert', 'email'])
|
||||
->beforeSending(FilterVisiblePostsBeforeSending::class),
|
||||
|
||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||
->attribute('subscription', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
if ($state = $discussion->state) {
|
||||
return $state->subscription;
|
||||
}
|
||||
}),
|
||||
|
||||
(new Extend\User())
|
||||
->registerPreference('followAfterReply', 'boolval', false),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(UserResourceFields::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, Listener\SaveSubscriptionToDatabase::class)
|
||||
@@ -75,5 +67,6 @@ return [
|
||||
->addMutator(DiscussionSearcher::class, HideIgnoredFromAllDiscussionsPage::class),
|
||||
|
||||
(new Extend\User())
|
||||
->registerPreference('followAfterReply', 'boolval', false)
|
||||
->registerPreference('flarum-subscriptions.notify_for_all_posts', 'boolval', false),
|
||||
];
|
||||
|
2
extensions/subscriptions/js/dist/forum.js
generated
vendored
2
extensions/subscriptions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/subscriptions/js/dist/forum.js.map
generated
vendored
2
extensions/subscriptions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import IndexPage from 'flarum/forum/components/IndexPage';
|
||||
import Discussion from 'flarum/common/models/Discussion';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import NewPostNotification from './components/NewPostNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
@@ -10,6 +11,9 @@ export default [
|
||||
new Extend.Routes() //
|
||||
.add('following', '/following', IndexPage),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('newPost', NewPostNotification),
|
||||
|
||||
new Extend.Model(Discussion) //
|
||||
.attribute('subscription'),
|
||||
];
|
||||
|
@@ -6,13 +6,9 @@ import addSubscriptionControls from './addSubscriptionControls';
|
||||
import addSubscriptionFilter from './addSubscriptionFilter';
|
||||
import addSubscriptionSettings from './addSubscriptionSettings';
|
||||
|
||||
import NewPostNotification from './components/NewPostNotification';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('subscriptions', function () {
|
||||
app.notificationComponents.newPost = NewPostNotification;
|
||||
|
||||
addSubscriptionBadge();
|
||||
addSubscriptionControls();
|
||||
addSubscriptionFilter();
|
||||
|
37
extensions/subscriptions/src/Api/UserResourceFields.php
Normal file
37
extensions/subscriptions/src/Api/UserResourceFields.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Subscriptions\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Discussion\Discussion;
|
||||
|
||||
class UserResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('subscription')
|
||||
->writable(fn (Discussion $discussion, Context $context) => $context->updating())
|
||||
->nullable()
|
||||
->get(fn (Discussion $discussion) => $discussion->state?->subscription)
|
||||
->set(function (Discussion $discussion, ?string $subscription, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$state = $discussion->stateFor($actor);
|
||||
|
||||
if (! in_array($subscription, ['follow', 'ignore'])) {
|
||||
$subscription = null;
|
||||
}
|
||||
|
||||
$state->subscription = $subscription;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Subscriptions\tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend\ModelVisibility;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
@@ -28,18 +29,18 @@ class ReplyNotificationTest extends TestCase
|
||||
$this->extension('flarum-subscriptions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1, 'preferences' => json_encode(['flarum-subscriptions.notify_for_all_posts' => true])],
|
||||
['id' => 4, 'username' => 'acme2', 'email' => 'acme2@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 2],
|
||||
|
||||
['id' => 33, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 33, 'comment_count' => 6, 'last_post_number' => 6, 'last_post_id' => 38],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
|
||||
@@ -119,6 +120,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'attributes' => [
|
||||
'title' => 'ACME',
|
||||
],
|
||||
@@ -133,6 +135,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'attributes' => [
|
||||
'lastReadPostNumber' => 2,
|
||||
],
|
||||
@@ -148,6 +151,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'reply with predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
@@ -169,10 +173,10 @@ class ReplyNotificationTest extends TestCase
|
||||
public function deleting_last_posts_then_posting_new_one_sends_reply_notification(array $postIds)
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 5, 'last_post_number' => 5, 'last_post_id' => 10],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 5, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 6, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 2],
|
||||
['id' => 7, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 3],
|
||||
@@ -203,6 +207,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'reply with predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
@@ -249,6 +254,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'reply with predetermined content for automated testing - too-obscure',
|
||||
],
|
||||
@@ -270,6 +276,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'isApproved' => 1,
|
||||
],
|
||||
@@ -309,6 +316,7 @@ class ReplyNotificationTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'restricted-test-post',
|
||||
],
|
||||
|
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Subscriptions\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class SubscribeTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-subscriptions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1, 'preferences' => json_encode(['flarum-subscriptions.notify_for_all_posts' => true])],
|
||||
['id' => 4, 'username' => 'acme2', 'email' => 'acme2@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 2],
|
||||
|
||||
['id' => 33, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 33, 'comment_count' => 6, 'last_post_number' => 6, 'last_post_id' => 38],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
|
||||
['id' => 33, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 34, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 2],
|
||||
['id' => 35, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 3],
|
||||
['id' => 36, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 4],
|
||||
['id' => 37, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 5],
|
||||
['id' => 38, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 6],
|
||||
],
|
||||
'discussion_user' => [
|
||||
['discussion_id' => 1, 'user_id' => 1, 'last_read_post_number' => 1, 'subscription' => 'follow'],
|
||||
['discussion_id' => 1, 'user_id' => 2, 'last_read_post_number' => 1, 'subscription' => null],
|
||||
['discussion_id' => 2, 'user_id' => 1, 'last_read_post_number' => 1, 'subscription' => 'follow'],
|
||||
|
||||
['discussion_id' => 33, 'user_id' => 2, 'last_read_post_number' => 1, 'subscription' => 'follow'],
|
||||
['discussion_id' => 33, 'user_id' => 3, 'last_read_post_number' => 1, 'subscription' => 'ignore'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideStates
|
||||
*/
|
||||
public function can_subscribe_to_a_discussion(int $actorId, int $discussionId, ?string $newState)
|
||||
{
|
||||
$this->app();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/discussions/'.$discussionId, [
|
||||
'authenticatedAs' => $actorId,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'attributes' => [
|
||||
'subscription' => $newState,
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals($newState, $this->database()->table('discussion_user')->where('discussion_id', $discussionId)->where('user_id', $actorId)->value('subscription'));
|
||||
}
|
||||
|
||||
public static function provideStates()
|
||||
{
|
||||
return [
|
||||
'follow' => [2, 1, 'follow'],
|
||||
'ignore' => [2, 1, 'ignore'],
|
||||
'null' => [2, 1, null],
|
||||
];
|
||||
}
|
||||
}
|
@@ -7,13 +7,13 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Suspend\Access\UserPolicy;
|
||||
use Flarum\Suspend\AddUserSuspendAttributes;
|
||||
use Flarum\Suspend\Api\UserResourceFields;
|
||||
use Flarum\Suspend\Event\Suspended;
|
||||
use Flarum\Suspend\Event\Unsuspended;
|
||||
use Flarum\Suspend\Listener;
|
||||
@@ -39,22 +39,23 @@ return [
|
||||
->cast('suspend_reason', 'string')
|
||||
->cast('suspend_message', 'string'),
|
||||
|
||||
(new Extend\ApiSerializer(UserSerializer::class))
|
||||
->attributes(AddUserSuspendAttributes::class),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class),
|
||||
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attribute('canSuspendUsers', function (ForumSerializer $serializer) {
|
||||
return $serializer->getActor()->hasPermission('user.suspend');
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\ForumResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('canSuspendUsers')
|
||||
->get(fn (object $model, Context $context) => $context->getActor()->hasPermission('user.suspend')),
|
||||
]),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(UserSuspendedBlueprint::class, BasicUserSerializer::class, ['alert', 'email'])
|
||||
->type(UserUnsuspendedBlueprint::class, BasicUserSerializer::class, ['alert', 'email']),
|
||||
->type(UserSuspendedBlueprint::class, ['alert', 'email'])
|
||||
->type(UserUnsuspendedBlueprint::class, ['alert', 'email']),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, Listener\SaveSuspensionToDatabase::class)
|
||||
->listen(Saving::class, Listener\SavingUser::class)
|
||||
->listen(Suspended::class, Listener\SendNotificationWhenUserIsSuspended::class)
|
||||
->listen(Unsuspended::class, Listener\SendNotificationWhenUserIsUnsuspended::class),
|
||||
|
||||
|
2
extensions/suspend/js/dist/forum.js
generated
vendored
2
extensions/suspend/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/suspend/js/dist/forum.js.map
generated
vendored
2
extensions/suspend/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -3,10 +3,16 @@ import User from 'flarum/common/models/User';
|
||||
import Model from 'flarum/common/Model';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import UserSuspendedNotification from './components/UserSuspendedNotification';
|
||||
import UserUnsuspendedNotification from './components/UserUnsuspendedNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('userSuspended', UserSuspendedNotification)
|
||||
.add('userUnsuspended', UserUnsuspendedNotification),
|
||||
|
||||
new Extend.Model(User)
|
||||
.attribute<Date | null | undefined, string | null | undefined>('suspendedUntil', Model.transformDate)
|
||||
.attribute<string | null | undefined>('suspendReason')
|
||||
|
@@ -6,16 +6,11 @@ import Badge from 'flarum/common/components/Badge';
|
||||
import User from 'flarum/common/models/User';
|
||||
|
||||
import SuspendUserModal from './components/SuspendUserModal';
|
||||
import UserSuspendedNotification from './components/UserSuspendedNotification';
|
||||
import UserUnsuspendedNotification from './components/UserUnsuspendedNotification';
|
||||
import checkForSuspension from './checkForSuspension';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-suspend', () => {
|
||||
app.notificationComponents.userSuspended = UserSuspendedNotification;
|
||||
app.notificationComponents.userUnsuspended = UserUnsuspendedNotification;
|
||||
|
||||
extend(UserControls, 'moderationControls', (items, user) => {
|
||||
if (user.canSuspend()) {
|
||||
items.add(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user