diff --git a/data/config.ini b/data/config.ini index fe312c88..ebdbdda8 100644 --- a/data/config.ini +++ b/data/config.ini @@ -74,6 +74,7 @@ changeTagImplications = moderator, administrator changeTagSuggestions = moderator, administrator banTags = moderator, administrator deleteTags = moderator, administrator +mergeTags = moderator, administrator listComments = regularUser, powerUser, moderator, administrator addComments = regularUser, powerUser, moderator, administrator diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js index dc717965..38ddc97a 100644 --- a/public_html/js/Auth.js +++ b/public_html/js/Auth.js @@ -46,6 +46,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) { deleteOwnComments: 'deleteOwnComments', deleteAllComments: 'deleteAllComments', deleteTags: 'deleteTags', + mergeTags: 'mergeTags', listTags: 'listTags', massTag: 'massTag', diff --git a/public_html/js/Presenters/TagPresenter.js b/public_html/js/Presenters/TagPresenter.js index 9c782523..138f145a 100644 --- a/public_html/js/Presenters/TagPresenter.js +++ b/public_html/js/Presenters/TagPresenter.js @@ -37,6 +37,7 @@ App.Presenters.TagPresenter = function( privileges.canBan = auth.hasPrivilege(auth.privileges.banTags); privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory); privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags); + privileges.canMerge = auth.hasPrivilege(auth.privileges.mergeTags); promise.wait( util.promiseTemplate('tag'), @@ -92,6 +93,7 @@ App.Presenters.TagPresenter = function( $el.find('form').submit(function(e) { e.preventDefault(); }); $el.find('form button[name=update]').click(updateButtonClicked); $el.find('form button[name=delete]').click(deleteButtonClicked); + $el.find('form button[name=merge]').click(mergeButtonClicked); implicationsTagInput = App.Controls.TagInput($el.find('[name=implications]')); 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) { var $target = $el.find('.post-list ul'); _.each(posts, function(post) { diff --git a/public_html/templates/tag.tpl b/public_html/templates/tag.tpl index 9c8e6566..8b3865e8 100644 --- a/public_html/templates/tag.tpl +++ b/public_html/templates/tag.tpl @@ -74,6 +74,9 @@ <% if (privileges.canDelete) { %> <% } %> + <% if (privileges.canMerge) { %> + + <% } %> <% } %> diff --git a/src/Controllers/TagController.php b/src/Controllers/TagController.php index 899605cc..1b0be3bd 100644 --- a/src/Controllers/TagController.php +++ b/src/Controllers/TagController.php @@ -37,6 +37,7 @@ final class TagController extends AbstractController $router->get('/api/tags/:tagName', [$this, 'getTag']); $router->get('/api/tags/:tagName/siblings', [$this, 'getTagSiblings']); $router->put('/api/tags/:tagName', [$this, 'updateTag']); + $router->put('/api/tags/:tagName/merge', [$this, 'mergeTag']); $router->delete('/api/tags/:tagName', [$this, 'deleteTag']); } @@ -105,6 +106,15 @@ final class TagController extends AbstractController 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() { return diff --git a/src/Privilege.php b/src/Privilege.php index d206abd1..26765ba0 100644 --- a/src/Privilege.php +++ b/src/Privilege.php @@ -47,6 +47,7 @@ class Privilege const CHANGE_TAG_SUGGESTIONS = 'changeTagSuggestions'; const BAN_TAGS = 'banTags'; const DELETE_TAGS = 'deleteTags'; + const MERGE_TAGS = 'mergeTags'; const LIST_COMMENTS = 'listComments'; const ADD_COMMENTS = 'addComments'; diff --git a/src/Services/TagService.php b/src/Services/TagService.php index d76a6557..a7de8457 100644 --- a/src/Services/TagService.php +++ b/src/Services/TagService.php @@ -186,6 +186,29 @@ class TagService $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) { $otherTag = $this->tagDao->findByName($newName); diff --git a/tests/AbstractDatabaseTestCase.php b/tests/AbstractDatabaseTestCase.php index 29801b88..3bb1084d 100644 --- a/tests/AbstractDatabaseTestCase.php +++ b/tests/AbstractDatabaseTestCase.php @@ -4,6 +4,7 @@ use Szurubooru\Config; use Szurubooru\Dao\PublicFileDao; use Szurubooru\DatabaseConnection; use Szurubooru\Entities\Post; +use Szurubooru\Entities\Tag; use Szurubooru\Entities\User; use Szurubooru\Injector; use Szurubooru\Tests\AbstractTestCase; @@ -35,6 +36,14 @@ abstract class AbstractDatabaseTestCase extends AbstractTestCase $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() { $post = new Post(); diff --git a/tests/Dao/TagDaoFilterTest.php b/tests/Dao/TagDaoFilterTest.php index e6209a09..ea138953 100644 --- a/tests/Dao/TagDaoFilterTest.php +++ b/tests/Dao/TagDaoFilterTest.php @@ -13,9 +13,9 @@ final class TagDaoFilterTest extends AbstractDatabaseTestCase { public function testCategories() { - $tag1 = $this->getTestTag('test 1'); - $tag2 = $this->getTestTag('test 2'); - $tag3 = $this->getTestTag('test 3'); + $tag1 = self::getTestTag('test 1'); + $tag2 = self::getTestTag('test 2'); + $tag3 = self::getTestTag('test 3'); $tag2->setCategory('misc'); $tag3->setCategory('other'); $tagDao = $this->getTagDao(); @@ -36,9 +36,9 @@ final class TagDaoFilterTest extends AbstractDatabaseTestCase public function testCompositeCategories() { - $tag1 = $this->getTestTag('test 1'); - $tag2 = $this->getTestTag('test 2'); - $tag3 = $this->getTestTag('test 3'); + $tag1 = self::getTestTag('test 1'); + $tag2 = self::getTestTag('test 2'); + $tag3 = self::getTestTag('test 3'); $tag2->setCategory('misc'); $tag3->setCategory('other'); $tagDao = $this->getTagDao(); @@ -61,12 +61,4 @@ final class TagDaoFilterTest extends AbstractDatabaseTestCase { return new TagDao($this->databaseConnection); } - - private function getTestTag($name) - { - $tag = new Tag(); - $tag->setName($name); - $tag->setCreationTime(date('c')); - return $tag; - } } diff --git a/tests/Dao/TagDaoTest.php b/tests/Dao/TagDaoTest.php index 6b362c5f..99ab73c8 100644 --- a/tests/Dao/TagDaoTest.php +++ b/tests/Dao/TagDaoTest.php @@ -13,7 +13,7 @@ final class TagDaoTest extends AbstractDatabaseTestCase public function testSaving() { - $tag = $this->getTestTag('test'); + $tag = self::getTestTag('test'); $tag->setCreationTime(date('c', mktime(0, 0, 0, 10, 1, 2014))); $this->assertFalse($tag->isBanned()); $tag->setBanned(true); @@ -26,17 +26,17 @@ final class TagDaoTest extends AbstractDatabaseTestCase public function testSavingRelations() { - $tag1 = $this->getTestTag('test 1'); - $tag2 = $this->getTestTag('test 2'); - $tag3 = $this->getTestTag('test 3'); - $tag4 = $this->getTestTag('test 4'); + $tag1 = self::getTestTag('test 1'); + $tag2 = self::getTestTag('test 2'); + $tag3 = self::getTestTag('test 3'); + $tag4 = self::getTestTag('test 4'); $tagDao = $this->getTagDao(); $tagDao->save($tag1); $tagDao->save($tag2); $tagDao->save($tag3); $tagDao->save($tag4); - $tag = $this->getTestTag('test'); + $tag = self::getTestTag('test'); $tag->setImpliedTags([$tag1, $tag3]); $tag->setSuggestedTags([$tag2, $tag4]); @@ -84,13 +84,4 @@ final class TagDaoTest extends AbstractDatabaseTestCase { return new TagDao($this->databaseConnection); } - - private function getTestTag($name) - { - $tag = new Tag(); - $tag->setName($name); - $tag->setCreationTime(date('c')); - $tag->setCategory('default'); - return $tag; - } } diff --git a/tests/Services/TagServiceTest.php b/tests/Services/TagServiceTest.php index 43b1c9c2..f83fbc61 100644 --- a/tests/Services/TagServiceTest.php +++ b/tests/Services/TagServiceTest.php @@ -1,6 +1,9 @@ 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() { $tag1 = new Tag();