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();