mirror of
https://github.com/flarum/core.git
synced 2025-07-31 13:40:20 +02:00
Fixes #2492 * Added api/groups/{id} endpoint for retrieving a single group by its id * Fixed GroupRepository incorrectly opening query to User instead of Group model * Added filtering & paging abilities to GET api/groups endpoint * Added test for sorting for GET api/groups endpoint Co-authored-by: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com>
This commit is contained in:
@@ -10,8 +10,10 @@
|
|||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
use Flarum\Api\Serializer\GroupSerializer;
|
use Flarum\Api\Serializer\GroupSerializer;
|
||||||
use Flarum\Group\Group;
|
use Flarum\Group\Filter\GroupFilterer;
|
||||||
use Flarum\Http\RequestUtil;
|
use Flarum\Http\RequestUtil;
|
||||||
|
use Flarum\Http\UrlGenerator;
|
||||||
|
use Flarum\Query\QueryCriteria;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Tobscure\JsonApi\Document;
|
use Tobscure\JsonApi\Document;
|
||||||
|
|
||||||
@@ -22,6 +24,38 @@ class ListGroupsController extends AbstractListController
|
|||||||
*/
|
*/
|
||||||
public $serializer = GroupSerializer::class;
|
public $serializer = GroupSerializer::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public $sortFields = ['nameSingular', 'namePlural', 'isHidden'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $limit = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var GroupFilterer
|
||||||
|
*/
|
||||||
|
protected $filterer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var UrlGenerator
|
||||||
|
*/
|
||||||
|
protected $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param GroupFilterer $filterer
|
||||||
|
* @param UrlGenerator $url
|
||||||
|
*/
|
||||||
|
public function __construct(GroupFilterer $filterer, UrlGenerator $url)
|
||||||
|
{
|
||||||
|
$this->filterer = $filterer;
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
@@ -29,10 +63,25 @@ class ListGroupsController extends AbstractListController
|
|||||||
{
|
{
|
||||||
$actor = RequestUtil::getActor($request);
|
$actor = RequestUtil::getActor($request);
|
||||||
|
|
||||||
$results = Group::whereVisibleTo($actor)->get();
|
$filters = $this->extractFilter($request);
|
||||||
|
$sort = $this->extractSort($request);
|
||||||
|
$sortIsDefault = $this->sortIsDefault($request);
|
||||||
|
|
||||||
$this->loadRelations($results, []);
|
$limit = $this->extractLimit($request);
|
||||||
|
$offset = $this->extractOffset($request);
|
||||||
|
|
||||||
return $results;
|
$criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault);
|
||||||
|
|
||||||
|
$queryResults = $this->filterer->filter($criteria, $limit, $offset);
|
||||||
|
|
||||||
|
$document->addPaginationLinks(
|
||||||
|
$this->url->to('api')->route('groups.index'),
|
||||||
|
$request->getQueryParams(),
|
||||||
|
$offset,
|
||||||
|
$limit,
|
||||||
|
$queryResults->areMoreResults() ? null : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return $queryResults->getResults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
51
src/Api/Controller/ShowGroupController.php
Normal file
51
src/Api/Controller/ShowGroupController.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?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\Api\Controller;
|
||||||
|
|
||||||
|
use Flarum\Api\Serializer\GroupSerializer;
|
||||||
|
use Flarum\Group\GroupRepository;
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Tobscure\JsonApi\Document;
|
||||||
|
|
||||||
|
class ShowGroupController extends AbstractShowController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var GroupRepository
|
||||||
|
*/
|
||||||
|
protected $groups;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public $serializer = GroupSerializer::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Flarum\Group\GroupRepository $groups
|
||||||
|
*/
|
||||||
|
public function __construct(GroupRepository $groups)
|
||||||
|
{
|
||||||
|
$this->groups = $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function data(ServerRequestInterface $request, Document $document)
|
||||||
|
{
|
||||||
|
$id = Arr::get($request->getQueryParams(), 'id');
|
||||||
|
$actor = RequestUtil::getActor($request);
|
||||||
|
|
||||||
|
$group = $this->groups->findOrFail($id, $actor);
|
||||||
|
|
||||||
|
return $group;
|
||||||
|
}
|
||||||
|
}
|
@@ -224,6 +224,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
|||||||
$route->toController(Controller\CreateGroupController::class)
|
$route->toController(Controller\CreateGroupController::class)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show a single group
|
||||||
|
$map->get(
|
||||||
|
'/groups/{id}',
|
||||||
|
'groups.show',
|
||||||
|
$route->toController(Controller\ShowGroupController::class)
|
||||||
|
);
|
||||||
|
|
||||||
// Edit a group
|
// Edit a group
|
||||||
$map->patch(
|
$map->patch(
|
||||||
'/groups/{id}',
|
'/groups/{id}',
|
||||||
|
@@ -13,6 +13,8 @@ use Flarum\Discussion\Filter\DiscussionFilterer;
|
|||||||
use Flarum\Discussion\Query as DiscussionQuery;
|
use Flarum\Discussion\Query as DiscussionQuery;
|
||||||
use Flarum\Foundation\AbstractServiceProvider;
|
use Flarum\Foundation\AbstractServiceProvider;
|
||||||
use Flarum\Foundation\ContainerUtil;
|
use Flarum\Foundation\ContainerUtil;
|
||||||
|
use Flarum\Group\Filter as GroupFilter;
|
||||||
|
use Flarum\Group\Filter\GroupFilterer;
|
||||||
use Flarum\Post\Filter as PostFilter;
|
use Flarum\Post\Filter as PostFilter;
|
||||||
use Flarum\Post\Filter\PostFilterer;
|
use Flarum\Post\Filter\PostFilterer;
|
||||||
use Flarum\User\Filter\UserFilterer;
|
use Flarum\User\Filter\UserFilterer;
|
||||||
@@ -41,6 +43,9 @@ class FilterServiceProvider extends AbstractServiceProvider
|
|||||||
UserQuery\EmailFilterGambit::class,
|
UserQuery\EmailFilterGambit::class,
|
||||||
UserQuery\GroupFilterGambit::class,
|
UserQuery\GroupFilterGambit::class,
|
||||||
],
|
],
|
||||||
|
GroupFilterer::class => [
|
||||||
|
GroupFilter\HiddenFilter::class,
|
||||||
|
],
|
||||||
PostFilterer::class => [
|
PostFilterer::class => [
|
||||||
PostFilter\AuthorFilter::class,
|
PostFilter\AuthorFilter::class,
|
||||||
PostFilter\DiscussionFilter::class,
|
PostFilter\DiscussionFilter::class,
|
||||||
|
40
src/Group/Filter/GroupFilterer.php
Normal file
40
src/Group/Filter/GroupFilterer.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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\Group\Filter;
|
||||||
|
|
||||||
|
use Flarum\Filter\AbstractFilterer;
|
||||||
|
use Flarum\Group\GroupRepository;
|
||||||
|
use Flarum\User\User;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class GroupFilterer extends AbstractFilterer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var GroupRepository
|
||||||
|
*/
|
||||||
|
protected $groups;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param GroupRepository $groups
|
||||||
|
* @param array $filters
|
||||||
|
* @param array $filterMutators
|
||||||
|
*/
|
||||||
|
public function __construct(GroupRepository $groups, array $filters, array $filterMutators)
|
||||||
|
{
|
||||||
|
parent::__construct($filters, $filterMutators);
|
||||||
|
|
||||||
|
$this->groups = $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getQuery(User $actor): Builder
|
||||||
|
{
|
||||||
|
return $this->groups->query()->whereVisibleTo($actor);
|
||||||
|
}
|
||||||
|
}
|
26
src/Group/Filter/HiddenFilter.php
Normal file
26
src/Group/Filter/HiddenFilter.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?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\Group\Filter;
|
||||||
|
|
||||||
|
use Flarum\Filter\FilterInterface;
|
||||||
|
use Flarum\Filter\FilterState;
|
||||||
|
|
||||||
|
class HiddenFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function getFilterKey(): string
|
||||||
|
{
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filter(FilterState $filterState, string $filterValue, bool $negate)
|
||||||
|
{
|
||||||
|
$filterState->getQuery()->where('is_hidden', $negate ? '!=' : '=', $filterValue);
|
||||||
|
}
|
||||||
|
}
|
@@ -21,7 +21,7 @@ class GroupRepository
|
|||||||
*/
|
*/
|
||||||
public function query()
|
public function query()
|
||||||
{
|
{
|
||||||
return User::query();
|
return Group::query();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -65,6 +65,148 @@ class ListTest extends TestCase
|
|||||||
$this->assertEquals(['1', '2', '3', '4', '10'], Arr::pluck($data['data'], 'id'));
|
$this->assertEquals(['1', '2', '3', '4', '10'], Arr::pluck($data['data'], 'id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function filters_only_public_groups_for_admin()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
])
|
||||||
|
->withQueryParams([
|
||||||
|
'filter' => ['hidden' => 0],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// The four default groups created by the installer without our hidden group
|
||||||
|
$this->assertEquals(['1', '2', '3', '4'], Arr::pluck($data['data'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function filters_only_hidden_groups_for_admin()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
])
|
||||||
|
->withQueryParams([
|
||||||
|
'filter' => ['hidden' => 1],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Only our hidden group
|
||||||
|
$this->assertEquals(['10'], Arr::pluck($data['data'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function filters_only_public_groups_for_guest()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups')
|
||||||
|
->withQueryParams([
|
||||||
|
'filter' => ['hidden' => 0],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// The four default groups created by the installer without our hidden group
|
||||||
|
$this->assertEquals(['1', '2', '3', '4'], Arr::pluck($data['data'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function hides_hidden_groups_when_filtering_for_guest()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups')
|
||||||
|
->withQueryParams([
|
||||||
|
'filter' => ['hidden' => 1],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// When guest attempts to filter for hidden groups, system should
|
||||||
|
// still apply scoping and exclude those groups from results
|
||||||
|
$this->assertEquals([], Arr::pluck($data['data'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function paginates_groups_without_filter()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups')
|
||||||
|
->withQueryParams([
|
||||||
|
'page' => ['limit' => '2', 'offset' => '2'],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Show second page of groups
|
||||||
|
$this->assertEquals(['3', '4'], Arr::pluck($data['data'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function paginates_groups_with_filter()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups')
|
||||||
|
->withQueryParams([
|
||||||
|
'filter' => ['hidden' => 1],
|
||||||
|
'page' => ['limit' => '1', 'offset' => '1'],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Show second page of groups. Because there is only one hidden group,
|
||||||
|
// second page should be empty.
|
||||||
|
$this->assertEmpty($data['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function sorts_groups_by_name()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
])
|
||||||
|
->withQueryParams([
|
||||||
|
'sort' => 'nameSingular',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Ascending alphabetical order is: Admin - Guest - Hidden - Member - Mod
|
||||||
|
$this->assertEquals(['1', '2', '10', '3', '4'], Arr::pluck($data['data'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
protected function hiddenGroup(): array
|
protected function hiddenGroup(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
126
tests/integration/api/groups/ShowTest.php
Normal file
126
tests/integration/api/groups/ShowTest.php
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<?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\Tests\integration\api\groups;
|
||||||
|
|
||||||
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
|
use Flarum\Testing\integration\TestCase;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class ShowTest extends TestCase
|
||||||
|
{
|
||||||
|
use RetrievesAuthorizedUsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->prepareDatabase([
|
||||||
|
'groups' => [
|
||||||
|
$this->hiddenGroup(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shows_public_group_for_guest()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Default group created by the installer should be returned
|
||||||
|
$this->assertEquals('1', Arr::get($data, 'data.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shows_public_group_for_admin()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups/1', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Default group created by the installer should be returned
|
||||||
|
$this->assertEquals('1', Arr::get($data, 'data.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function hides_hidden_group_for_guest()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups/10')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hidden group should not be returned for guest
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shows_hidden_group_for_admin()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups/10', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Hidden group should be returned for admin
|
||||||
|
$this->assertEquals('10', Arr::get($data, 'data.id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function rejects_request_for_non_existing_group()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('GET', '/api/groups/999', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
// If group does not exist in database, controller
|
||||||
|
// should reject the request with 404 Not found
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function hiddenGroup(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => 10,
|
||||||
|
'name_singular' => 'Hidden',
|
||||||
|
'name_plural' => 'Ninjas',
|
||||||
|
'color' => null,
|
||||||
|
'icon' => 'fas fa-wrench',
|
||||||
|
'is_hidden' => 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user