Extend admin permissions page to allow restriction by tag

Also fix a couple of bugs:
- Tag sorting algorithm bug in Safari
- Ensure subtag is removed when parent is removed
This commit is contained in:
Toby Zerner 2015-07-31 20:19:34 +09:30
parent d63b442227
commit d0f9115dea
22 changed files with 268 additions and 13 deletions

View File

@ -0,0 +1,10 @@
var gulp = require('flarum-gulp');
gulp({
modules: {
'tags': [
'../lib/**/*.js',
'src/**/*.js'
]
}
});

View File

@ -0,0 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "git+https://github.com/flarum/gulp.git"
}
}

View File

@ -0,0 +1,63 @@
import { extend } from 'flarum/extend';
import PermissionGrid from 'flarum/components/PermissionGrid';
import PermissionDropdown from 'flarum/components/PermissionDropdown';
import Dropdown from 'flarum/components/Dropdown';
import Button from 'flarum/components/Button';
import Tag from 'tags/models/Tag';
import tagLabel from 'tags/helpers/tagLabel';
import tagIcon from 'tags/helpers/tagIcon';
import sortTags from 'tags/utils/sortTags';
app.initializers.add('tags', app => {
app.store.models.tags = Tag;
extend(PermissionGrid.prototype, 'scopeItems', items => {
sortTags(app.store.all('tags'))
.filter(tag => tag.isRestricted())
.forEach(tag => items.add('tag' + tag.id(), {
label: tagLabel(tag),
onremove: () => tag.save({isRestricted: false}),
render: item => {
if (item.permission) {
let permission;
if (item.permission === 'forum.view') {
permission = 'view';
} else if (item.permission === 'forum.startDiscussion') {
permission = 'startDiscussion';
} else if (item.permission.indexOf('discussion.') === 0) {
permission = item.permission;
}
if (permission) {
const props = Object.assign({}, item);
props.permission = 'tag' + tag.id() + '.' + permission;
return PermissionDropdown.component(props);
}
}
return '';
}
}));
});
extend(PermissionGrid.prototype, 'scopeControlItems', items => {
const tags = sortTags(app.store.all('tags').filter(tag => !tag.isRestricted()));
if (tags.length) {
items.add('tag', Dropdown.component({
buttonClassName: 'Button Button--text',
label: 'Restrict by Tag',
icon: 'plus',
caretIcon: null,
children: tags.map(tag => Button.component({
icon: true,
children: [tagIcon(tag, {className: 'Button-icon'}), ' ', tag.name()],
onclick: () => tag.save({isRestricted: true})
}))
}));
}
});
});

View File

@ -2,6 +2,9 @@ var gulp = require('flarum-gulp');
gulp({
modules: {
'tags': 'src/**/*.js'
'tags': [
'../lib/**/*.js',
'src/**/*.js'
]
}
});

View File

@ -60,8 +60,8 @@ export default class TagDiscussionModal extends Modal {
// Look through the list of selected tags for any tags which have the tag
// we just removed as their parent. We'll need to remove them too.
this.selected
.filter(selected => selected.parent() && selected.parent() === tag)
.forEach(this.removeTag);
.filter(selected => selected.parent() === tag)
.forEach(this.removeTag.bind(this));
}
}

View File

@ -20,5 +20,6 @@ export default class Tag extends mixin(Model, {
lastTime: Model.attribute('lastTime', Model.transformDate),
lastDiscussion: Model.hasOne('lastDiscussion'),
isRestricted: Model.attribute('isRestricted'),
canStartDiscussion: Model.attribute('canStartDiscussion')
}) {}

View File

@ -15,7 +15,7 @@ export default function sortTags(tags) {
} else if (aParent === bParent) {
return aPos - bPos;
} else if (aParent) {
return aParent === b ? -1 : aParent.position() - bPos;
return aParent === b ? 1 : aParent.position() - bPos;
} else if (bParent) {
return bParent === a ? -1 : aPos - bParent.position();
}

View File

@ -0,0 +1,2 @@
@import "../lib/TagLabel.less";
@import "../lib/TagIcon.less";

View File

@ -1,7 +1,8 @@
@import "../lib/TagLabel.less";
@import "../lib/TagIcon.less";
@import "TagCloud.less";
@import "TagDiscussionModal.less";
@import "TagIcon.less";
@import "TagLabel.less";
@import "TagTiles.less";
.DiscussionHero {

View File

@ -6,6 +6,7 @@
border-radius: @border-radius;
background: @control-bg;
color: @control-color;
text-transform: none;
&.untagged {
background: transparent;

View File

@ -1,4 +1,4 @@
<?php namespace Flarum\Tags;
<?php namespace Flarum\Tags\Api;
use Flarum\Api\Serializers\Serializer;
@ -8,7 +8,7 @@ class TagSerializer extends Serializer
protected function getDefaultAttributes($tag)
{
return [
$attributes = [
'name' => $tag->name,
'description' => $tag->description,
'slug' => $tag->slug,
@ -23,11 +23,17 @@ class TagSerializer extends Serializer
'lastTime' => $tag->last_time ? $tag->last_time->toRFC3339String() : null,
'canStartDiscussion' => $tag->can($this->actor, 'startDiscussion')
];
if ($this->actor->isAdmin()) {
$attributes['isRestricted'] = (bool) $tag->is_restricted;
}
return $attributes;
}
protected function parent()
{
return $this->hasOne('Flarum\Tags\TagSerializer');
return $this->hasOne('Flarum\Tags\Api\TagSerializer');
}
protected function lastDiscussion()

View File

@ -0,0 +1,40 @@
<?php namespace Flarum\Tags\Api;
use Flarum\Tags\Commands\EditTag;
use Flarum\Api\Actions\SerializeResourceAction;
use Flarum\Api\JsonApiRequest;
use Illuminate\Contracts\Bus\Dispatcher;
use Tobscure\JsonApi\Document;
class UpdateAction extends SerializeResourceAction
{
/**
* @var Dispatcher
*/
protected $bus;
/**
* @inheritdoc
*/
public $serializer = 'Flarum\Tags\Api\TagSerializer';
/**
* @param Dispatcher $bus
*/
public function __construct(Dispatcher $bus)
{
$this->bus = $bus;
}
/**
* @param JsonApiRequest $request
* @param Document $document
* @return \Flarum\Core\Tags\Tag
*/
protected function data(JsonApiRequest $request, Document $document)
{
return $this->bus->dispatch(
new EditTag($request->get('id'), $request->actor, $request->get('data'))
);
}
}

View File

@ -0,0 +1,40 @@
<?php namespace Flarum\Tags\Commands;
use Flarum\Core\Tags\Tag;
use Flarum\Core\Users\User;
class EditTag
{
/**
* The ID of the tag to edit.
*
* @var int
*/
public $tagId;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* The attributes to update on the tag.
*
* @var array
*/
public $data;
/**
* @param int $tagId The ID of the tag to edit.
* @param User $actor The user performing the action.
* @param array $data The attributes to update on the tag.
*/
public function __construct($tagId, User $actor, array $data)
{
$this->tagId = $tagId;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@ -0,0 +1,45 @@
<?php namespace Flarum\Tags\Commands;
use Flarum\Tags\Tag;
use Flarum\Tags\TagRepository;
class EditTagHandler
{
/**
* @var TagRepository
*/
protected $tags;
/**
* @param TagRepository $tags
*/
public function __construct(TagRepository $tags)
{
$this->tags = $tags;
}
/**
* @param EditTag $command
* @return Tag
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(EditTag $command)
{
$actor = $command->actor;
$data = $command->data;
$tag = $this->tags->findOrFail($command->tagId, $actor);
$tag->assertCan($actor, 'edit');
$attributes = array_get($data, 'attributes', []);
if (isset($attributes['isRestricted'])) {
$tag->is_restricted = (bool) $attributes['isRestricted'];
}
$tag->save();
return $tag;
}
}

View File

@ -4,6 +4,7 @@ use Flarum\Events\ApiRelationship;
use Flarum\Events\WillSerializeData;
use Flarum\Events\BuildApiAction;
use Flarum\Events\ApiAttributes;
use Flarum\Events\RegisterApiRoutes;
use Flarum\Api\Actions\Forum;
use Flarum\Api\Actions\Discussions;
use Flarum\Api\Serializers\ForumSerializer;
@ -18,18 +19,19 @@ class AddApiAttributes
$events->listen(WillSerializeData::class, [$this, 'loadTagsRelationship']);
$events->listen(BuildApiAction::class, [$this, 'includeTagsRelationship']);
$events->listen(ApiAttributes::class, [$this, 'addAttributes']);
$events->listen(RegisterApiRoutes::class, [$this, 'addRoutes']);
}
public function addTagsRelationship(ApiRelationship $event)
{
if ($event->serializer instanceof ForumSerializer &&
$event->relationship === 'tags') {
return $event->serializer->hasMany('Flarum\Tags\TagSerializer', 'tags');
return $event->serializer->hasMany('Flarum\Tags\Api\TagSerializer', 'tags');
}
if ($event->serializer instanceof DiscussionSerializer &&
$event->relationship === 'tags') {
return $event->serializer->hasMany('Flarum\Tags\TagSerializer', 'tags');
return $event->serializer->hasMany('Flarum\Tags\Api\TagSerializer', 'tags');
}
}
@ -69,4 +71,9 @@ class AddApiAttributes
$event->attributes['canTag'] = $event->model->can($event->actor, 'tag');
}
}
public function addRoutes(RegisterApiRoutes $event)
{
$event->patch('/tags/{id}', 'tags.update', 'Flarum\Tags\Api\UpdateAction');
}
}

View File

@ -41,6 +41,13 @@ class AddClientAssets
'tags.tag_cloud_title',
'tags.deleted'
]);
$event->adminAssets([
__DIR__.'/../../js/admin/dist/extension.js',
__DIR__.'/../../less/admin/extension.less'
]);
$event->adminBootstrapper('tags/main');
}
public function addRoutes(RegisterForumRoutes $event)

View File

@ -2,9 +2,14 @@
use Flarum\Core\Model;
use Flarum\Core\Discussions\Discussion;
use Flarum\Core\Support\VisibleScope;
use Flarum\Core\Support\Locked;
class Tag extends Model
{
use VisibleScope;
use Locked;
protected $table = 'tags';
protected $dates = ['last_time'];

View File

@ -1,11 +1,28 @@
<?php namespace Flarum\Tags;
use Illuminate\Database\Eloquent\Builder;
use Flarum\Core\Models\User;
use Flarum\Core\Users\User;
use Flarum\Tags\Tag;
class TagRepository
{
/**
* Find a tag by ID, optionally making sure it is visible to a certain
* user, or throw an exception.
*
* @param int $id
* @param User $actor
* @return Tag
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, User $actor = null)
{
$query = Tag::where('id', $id);
return $this->scopeVisibleTo($query, $actor)->firstOrFail();
}
/**
* Find all tags, optionally making sure they are visible to a
* certain user.
@ -13,7 +30,7 @@ class TagRepository
* @param User|null $user
* @return \Illuminate\Database\Eloquent\Collection
*/
public function find(User $user = null)
public function all(User $user = null)
{
$query = Tag::newQuery();