Added tag merging
This commit is contained in:
parent
be4b742d21
commit
2d529107db
11 changed files with 113 additions and 29 deletions
|
@ -74,6 +74,7 @@ changeTagImplications = moderator, administrator
|
||||||
changeTagSuggestions = moderator, administrator
|
changeTagSuggestions = moderator, administrator
|
||||||
banTags = moderator, administrator
|
banTags = moderator, administrator
|
||||||
deleteTags = moderator, administrator
|
deleteTags = moderator, administrator
|
||||||
|
mergeTags = moderator, administrator
|
||||||
|
|
||||||
listComments = regularUser, powerUser, moderator, administrator
|
listComments = regularUser, powerUser, moderator, administrator
|
||||||
addComments = regularUser, powerUser, moderator, administrator
|
addComments = regularUser, powerUser, moderator, administrator
|
||||||
|
|
|
@ -46,6 +46,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
|
||||||
deleteOwnComments: 'deleteOwnComments',
|
deleteOwnComments: 'deleteOwnComments',
|
||||||
deleteAllComments: 'deleteAllComments',
|
deleteAllComments: 'deleteAllComments',
|
||||||
deleteTags: 'deleteTags',
|
deleteTags: 'deleteTags',
|
||||||
|
mergeTags: 'mergeTags',
|
||||||
|
|
||||||
listTags: 'listTags',
|
listTags: 'listTags',
|
||||||
massTag: 'massTag',
|
massTag: 'massTag',
|
||||||
|
|
|
@ -37,6 +37,7 @@ App.Presenters.TagPresenter = function(
|
||||||
privileges.canBan = auth.hasPrivilege(auth.privileges.banTags);
|
privileges.canBan = auth.hasPrivilege(auth.privileges.banTags);
|
||||||
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
|
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
|
||||||
privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags);
|
privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags);
|
||||||
|
privileges.canMerge = auth.hasPrivilege(auth.privileges.mergeTags);
|
||||||
|
|
||||||
promise.wait(
|
promise.wait(
|
||||||
util.promiseTemplate('tag'),
|
util.promiseTemplate('tag'),
|
||||||
|
@ -92,6 +93,7 @@ App.Presenters.TagPresenter = function(
|
||||||
$el.find('form').submit(function(e) { e.preventDefault(); });
|
$el.find('form').submit(function(e) { e.preventDefault(); });
|
||||||
$el.find('form button[name=update]').click(updateButtonClicked);
|
$el.find('form button[name=update]').click(updateButtonClicked);
|
||||||
$el.find('form button[name=delete]').click(deleteButtonClicked);
|
$el.find('form button[name=delete]').click(deleteButtonClicked);
|
||||||
|
$el.find('form button[name=merge]').click(mergeButtonClicked);
|
||||||
implicationsTagInput = App.Controls.TagInput($el.find('[name=implications]'));
|
implicationsTagInput = App.Controls.TagInput($el.find('[name=implications]'));
|
||||||
suggestionsTagInput = App.Controls.TagInput($el.find('[name=suggestions]'));
|
suggestionsTagInput = App.Controls.TagInput($el.find('[name=suggestions]'));
|
||||||
}
|
}
|
||||||
|
@ -141,6 +143,17 @@ App.Presenters.TagPresenter = function(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeButtonClicked(e) {
|
||||||
|
if (targetTag = window.prompt('What tag should this be merged to?')) {
|
||||||
|
promise.wait(api.put('/tags/' + tag.name + '/merge', {targetTag: targetTag}))
|
||||||
|
.then(function(response) {
|
||||||
|
router.navigate('#/tags');
|
||||||
|
}).fail(function(response) {
|
||||||
|
window.alert(response.json && response.json.error || 'An error occured.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderPosts(posts) {
|
function renderPosts(posts) {
|
||||||
var $target = $el.find('.post-list ul');
|
var $target = $el.find('.post-list ul');
|
||||||
_.each(posts, function(post) {
|
_.each(posts, function(post) {
|
||||||
|
|
|
@ -74,6 +74,9 @@
|
||||||
<% if (privileges.canDelete) { %>
|
<% if (privileges.canDelete) { %>
|
||||||
<button type="submit" name="delete">Delete</button>
|
<button type="submit" name="delete">Delete</button>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (privileges.canMerge) { %>
|
||||||
|
<button type="submit" name="merge">Merge with…</button>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
|
@ -37,6 +37,7 @@ final class TagController extends AbstractController
|
||||||
$router->get('/api/tags/:tagName', [$this, 'getTag']);
|
$router->get('/api/tags/:tagName', [$this, 'getTag']);
|
||||||
$router->get('/api/tags/:tagName/siblings', [$this, 'getTagSiblings']);
|
$router->get('/api/tags/:tagName/siblings', [$this, 'getTagSiblings']);
|
||||||
$router->put('/api/tags/:tagName', [$this, 'updateTag']);
|
$router->put('/api/tags/:tagName', [$this, 'updateTag']);
|
||||||
|
$router->put('/api/tags/:tagName/merge', [$this, 'mergeTag']);
|
||||||
$router->delete('/api/tags/:tagName', [$this, 'deleteTag']);
|
$router->delete('/api/tags/:tagName', [$this, 'deleteTag']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +106,15 @@ final class TagController extends AbstractController
|
||||||
return $this->tagService->deleteTag($tag);
|
return $this->tagService->deleteTag($tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mergeTag($tagName)
|
||||||
|
{
|
||||||
|
$targetTagName = $this->inputReader->targetTag;
|
||||||
|
$sourceTag = $this->tagService->getByName($tagName);
|
||||||
|
$targetTag = $this->tagService->getByName($targetTagName);
|
||||||
|
$this->privilegeService->assertPrivilege(Privilege::MERGE_TAGS);
|
||||||
|
return $this->tagService->mergeTag($sourceTag, $targetTag);
|
||||||
|
}
|
||||||
|
|
||||||
private function getFullFetchConfig()
|
private function getFullFetchConfig()
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
|
|
|
@ -47,6 +47,7 @@ class Privilege
|
||||||
const CHANGE_TAG_SUGGESTIONS = 'changeTagSuggestions';
|
const CHANGE_TAG_SUGGESTIONS = 'changeTagSuggestions';
|
||||||
const BAN_TAGS = 'banTags';
|
const BAN_TAGS = 'banTags';
|
||||||
const DELETE_TAGS = 'deleteTags';
|
const DELETE_TAGS = 'deleteTags';
|
||||||
|
const MERGE_TAGS = 'mergeTags';
|
||||||
|
|
||||||
const LIST_COMMENTS = 'listComments';
|
const LIST_COMMENTS = 'listComments';
|
||||||
const ADD_COMMENTS = 'addComments';
|
const ADD_COMMENTS = 'addComments';
|
||||||
|
|
|
@ -186,6 +186,29 @@ class TagService
|
||||||
$this->transactionManager->commit($transactionFunc);
|
$this->transactionManager->commit($transactionFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mergeTag(Tag $sourceTag, Tag $targetTag)
|
||||||
|
{
|
||||||
|
$transactionFunc = function() use ($sourceTag, $targetTag)
|
||||||
|
{
|
||||||
|
$posts = $this->postDao->findByTagName($sourceTag->getName());
|
||||||
|
foreach ($posts as $post)
|
||||||
|
{
|
||||||
|
$newTags = $post->getTags();
|
||||||
|
$newTags = array_filter($newTags, function(Tag $tag) use ($sourceTag) {
|
||||||
|
return $tag->getId() !== $sourceTag->getId();
|
||||||
|
});
|
||||||
|
$newTags []= $targetTag;
|
||||||
|
$post->setTags($newTags);
|
||||||
|
$this->postDao->save($post);
|
||||||
|
$this->postHistoryService->savePostChange($post);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tagHistoryService->saveTagDeletion($sourceTag);
|
||||||
|
$this->tagDao->deleteById($sourceTag->getId());
|
||||||
|
};
|
||||||
|
$this->transactionManager->commit($transactionFunc);
|
||||||
|
}
|
||||||
|
|
||||||
private function updateTagName(Tag $tag, $newName)
|
private function updateTagName(Tag $tag, $newName)
|
||||||
{
|
{
|
||||||
$otherTag = $this->tagDao->findByName($newName);
|
$otherTag = $this->tagDao->findByName($newName);
|
||||||
|
|
|
@ -4,6 +4,7 @@ use Szurubooru\Config;
|
||||||
use Szurubooru\Dao\PublicFileDao;
|
use Szurubooru\Dao\PublicFileDao;
|
||||||
use Szurubooru\DatabaseConnection;
|
use Szurubooru\DatabaseConnection;
|
||||||
use Szurubooru\Entities\Post;
|
use Szurubooru\Entities\Post;
|
||||||
|
use Szurubooru\Entities\Tag;
|
||||||
use Szurubooru\Entities\User;
|
use Szurubooru\Entities\User;
|
||||||
use Szurubooru\Injector;
|
use Szurubooru\Injector;
|
||||||
use Szurubooru\Tests\AbstractTestCase;
|
use Szurubooru\Tests\AbstractTestCase;
|
||||||
|
@ -35,6 +36,14 @@ abstract class AbstractDatabaseTestCase extends AbstractTestCase
|
||||||
$this->databaseConnection->close();
|
$this->databaseConnection->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function getTestTag($name = 'test')
|
||||||
|
{
|
||||||
|
$tag = new Tag();
|
||||||
|
$tag->setName($name);
|
||||||
|
$tag->setCreationTime(date('c'));
|
||||||
|
return $tag;
|
||||||
|
}
|
||||||
|
|
||||||
protected static function getTestPost()
|
protected static function getTestPost()
|
||||||
{
|
{
|
||||||
$post = new Post();
|
$post = new Post();
|
||||||
|
|
|
@ -13,9 +13,9 @@ final class TagDaoFilterTest extends AbstractDatabaseTestCase
|
||||||
{
|
{
|
||||||
public function testCategories()
|
public function testCategories()
|
||||||
{
|
{
|
||||||
$tag1 = $this->getTestTag('test 1');
|
$tag1 = self::getTestTag('test 1');
|
||||||
$tag2 = $this->getTestTag('test 2');
|
$tag2 = self::getTestTag('test 2');
|
||||||
$tag3 = $this->getTestTag('test 3');
|
$tag3 = self::getTestTag('test 3');
|
||||||
$tag2->setCategory('misc');
|
$tag2->setCategory('misc');
|
||||||
$tag3->setCategory('other');
|
$tag3->setCategory('other');
|
||||||
$tagDao = $this->getTagDao();
|
$tagDao = $this->getTagDao();
|
||||||
|
@ -36,9 +36,9 @@ final class TagDaoFilterTest extends AbstractDatabaseTestCase
|
||||||
|
|
||||||
public function testCompositeCategories()
|
public function testCompositeCategories()
|
||||||
{
|
{
|
||||||
$tag1 = $this->getTestTag('test 1');
|
$tag1 = self::getTestTag('test 1');
|
||||||
$tag2 = $this->getTestTag('test 2');
|
$tag2 = self::getTestTag('test 2');
|
||||||
$tag3 = $this->getTestTag('test 3');
|
$tag3 = self::getTestTag('test 3');
|
||||||
$tag2->setCategory('misc');
|
$tag2->setCategory('misc');
|
||||||
$tag3->setCategory('other');
|
$tag3->setCategory('other');
|
||||||
$tagDao = $this->getTagDao();
|
$tagDao = $this->getTagDao();
|
||||||
|
@ -61,12 +61,4 @@ final class TagDaoFilterTest extends AbstractDatabaseTestCase
|
||||||
{
|
{
|
||||||
return new TagDao($this->databaseConnection);
|
return new TagDao($this->databaseConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTestTag($name)
|
|
||||||
{
|
|
||||||
$tag = new Tag();
|
|
||||||
$tag->setName($name);
|
|
||||||
$tag->setCreationTime(date('c'));
|
|
||||||
return $tag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ final class TagDaoTest extends AbstractDatabaseTestCase
|
||||||
|
|
||||||
public function testSaving()
|
public function testSaving()
|
||||||
{
|
{
|
||||||
$tag = $this->getTestTag('test');
|
$tag = self::getTestTag('test');
|
||||||
$tag->setCreationTime(date('c', mktime(0, 0, 0, 10, 1, 2014)));
|
$tag->setCreationTime(date('c', mktime(0, 0, 0, 10, 1, 2014)));
|
||||||
$this->assertFalse($tag->isBanned());
|
$this->assertFalse($tag->isBanned());
|
||||||
$tag->setBanned(true);
|
$tag->setBanned(true);
|
||||||
|
@ -26,17 +26,17 @@ final class TagDaoTest extends AbstractDatabaseTestCase
|
||||||
|
|
||||||
public function testSavingRelations()
|
public function testSavingRelations()
|
||||||
{
|
{
|
||||||
$tag1 = $this->getTestTag('test 1');
|
$tag1 = self::getTestTag('test 1');
|
||||||
$tag2 = $this->getTestTag('test 2');
|
$tag2 = self::getTestTag('test 2');
|
||||||
$tag3 = $this->getTestTag('test 3');
|
$tag3 = self::getTestTag('test 3');
|
||||||
$tag4 = $this->getTestTag('test 4');
|
$tag4 = self::getTestTag('test 4');
|
||||||
$tagDao = $this->getTagDao();
|
$tagDao = $this->getTagDao();
|
||||||
$tagDao->save($tag1);
|
$tagDao->save($tag1);
|
||||||
$tagDao->save($tag2);
|
$tagDao->save($tag2);
|
||||||
$tagDao->save($tag3);
|
$tagDao->save($tag3);
|
||||||
$tagDao->save($tag4);
|
$tagDao->save($tag4);
|
||||||
|
|
||||||
$tag = $this->getTestTag('test');
|
$tag = self::getTestTag('test');
|
||||||
$tag->setImpliedTags([$tag1, $tag3]);
|
$tag->setImpliedTags([$tag1, $tag3]);
|
||||||
$tag->setSuggestedTags([$tag2, $tag4]);
|
$tag->setSuggestedTags([$tag2, $tag4]);
|
||||||
|
|
||||||
|
@ -84,13 +84,4 @@ final class TagDaoTest extends AbstractDatabaseTestCase
|
||||||
{
|
{
|
||||||
return new TagDao($this->databaseConnection);
|
return new TagDao($this->databaseConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTestTag($name)
|
|
||||||
{
|
|
||||||
$tag = new Tag();
|
|
||||||
$tag->setName($name);
|
|
||||||
$tag->setCreationTime(date('c'));
|
|
||||||
$tag->setCategory('default');
|
|
||||||
return $tag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Szurubooru\Tests\Services;
|
namespace Szurubooru\Tests\Services;
|
||||||
|
use Szurubooru\Dao\PostDao;
|
||||||
use Szurubooru\Dao\PublicFileDao;
|
use Szurubooru\Dao\PublicFileDao;
|
||||||
|
use Szurubooru\Dao\TagDao;
|
||||||
|
use Szurubooru\Entities\Post;
|
||||||
use Szurubooru\Entities\Tag;
|
use Szurubooru\Entities\Tag;
|
||||||
use Szurubooru\Injector;
|
use Szurubooru\Injector;
|
||||||
use Szurubooru\Services\TagService;
|
use Szurubooru\Services\TagService;
|
||||||
|
@ -116,6 +119,43 @@ final class TagServiceTest extends AbstractDatabaseTestCase
|
||||||
$this->assertEquals('[{"name":"test","usages":0,"banned":false}]', $fileDao->load('tags.json'));
|
$this->assertEquals('[{"name":"test","usages":0,"banned":false}]', $fileDao->load('tags.json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMerging()
|
||||||
|
{
|
||||||
|
$tag1 = self::getTestTag('test 1');
|
||||||
|
$tag2 = self::getTestTag('test 2');
|
||||||
|
$tag3 = self::getTestTag('test 3');
|
||||||
|
|
||||||
|
$tagDao = Injector::get(TagDao::class);
|
||||||
|
$tagDao->save($tag1);
|
||||||
|
$tagDao->save($tag2);
|
||||||
|
$tagDao->save($tag3);
|
||||||
|
|
||||||
|
$post1 = self::getTestPost();
|
||||||
|
$post2 = self::getTestPost();
|
||||||
|
$post3 = self::getTestPost();
|
||||||
|
$post1->setTags([$tag1]);
|
||||||
|
$post2->setTags([$tag1, $tag3]);
|
||||||
|
$post3->setTags([$tag2, $tag3]);
|
||||||
|
|
||||||
|
$postDao = Injector::get(PostDao::class);
|
||||||
|
$postDao->save($post1);
|
||||||
|
$postDao->save($post2);
|
||||||
|
$postDao->save($post3);
|
||||||
|
|
||||||
|
$tagService = $this->getTagService();
|
||||||
|
$tagService->mergeTag($tag1, $tag2);
|
||||||
|
|
||||||
|
$this->assertNull($tagDao->findByName($tag1->getName()));
|
||||||
|
$this->assertNotNull($tagDao->findByName($tag2->getName()));
|
||||||
|
|
||||||
|
$post1 = $postDao->findById($post1->getId());
|
||||||
|
$post2 = $postDao->findById($post2->getId());
|
||||||
|
$post3 = $postDao->findById($post3->getId());
|
||||||
|
$this->assertEntitiesEqual([$tag2], array_values($post1->getTags()));
|
||||||
|
$this->assertEntitiesEqual([$tag2, $tag3], array_values($post2->getTags()));
|
||||||
|
$this->assertEntitiesEqual([$tag2, $tag3], array_values($post3->getTags()));
|
||||||
|
}
|
||||||
|
|
||||||
public function testExportMultiple()
|
public function testExportMultiple()
|
||||||
{
|
{
|
||||||
$tag1 = new Tag();
|
$tag1 = new Tag();
|
||||||
|
|
Loading…
Reference in a new issue