From 141c9fcdc9c694fbb822f23e4d2128d3ece8dfdb Mon Sep 17 00:00:00 2001 From: rr- Date: Sat, 22 Oct 2016 17:57:25 +0200 Subject: [PATCH] server/tags: merge also tag relations --- API.md | 7 +- client/html/tag_merge.tpl | 10 +- server/szurubooru/func/posts.py | 38 ++--- server/szurubooru/func/tags.py | 51 +++++-- server/szurubooru/tests/func/test_tags.py | 166 ++++++++++++++-------- 5 files changed, 177 insertions(+), 95 deletions(-) diff --git a/API.md b/API.md index 1d48ea2..33139d5 100644 --- a/API.md +++ b/API.md @@ -618,10 +618,9 @@ data. - **Description** - Removes source tag and merges all of its usages to the target tag. Source - tag properties such as category, tag relations etc. do not get transferred - and are discarded. The target tag effectively remains unchanged with the - exception of the set of posts it's used in. + Removes source tag and merges all of its usages, suggestions and + implications to the target tag. Other tag properties such as category and + aliases do not get transferred and are discarded. ## Listing tag siblings - **Request** diff --git a/client/html/tag_merge.tpl b/client/html/tag_merge.tpl index 5c724c9..ea1a7ec 100644 --- a/client/html/tag_merge.tpl +++ b/client/html/tag_merge.tpl @@ -1,14 +1,14 @@
-

Proceeding will remove this tag and retag its posts with the tag - specified below. Aliases, suggestions and implications are discarded - and need to be handled manually.

-
  • <%= ctx.makeTextInput({required: true, text: 'Target tag', pattern: ctx.tagNamePattern}) %>
  • -
  • + +
  • +

    Usages in posts, suggestions and implications will be + merged. Category and aliases need to be handled manually.

    + <%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this tag.'}) %>
diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 1ca70fc..e03fe63 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -449,18 +449,18 @@ def merge_posts(source_post, target_post, replace_content): raise InvalidPostRelationError('Cannot merge post with itself.') def merge_tables(table, anti_dup_func, source_post_id, target_post_id): - table1 = table - table2 = sqlalchemy.orm.util.aliased(table) - update_stmt = (sqlalchemy.sql.expression.update(table1) - .where(table1.post_id == source_post_id)) + alias1 = table + alias2 = sqlalchemy.orm.util.aliased(table) + update_stmt = (sqlalchemy.sql.expression.update(alias1) + .where(alias1.post_id == source_post_id)) if anti_dup_func is not None: update_stmt = (update_stmt .where(~sqlalchemy.exists() - .where(anti_dup_func(table1, table2)) - .where(table2.post_id == target_post_id))) + .where(anti_dup_func(alias1, alias2)) + .where(alias2.post_id == target_post_id))) - update_stmt = (update_stmt.values(post_id=target_post_id)) + update_stmt = update_stmt.values(post_id=target_post_id) db.session.execute(update_stmt) def merge_tags(source_post_id, target_post_id): @@ -488,23 +488,23 @@ def merge_posts(source_post, target_post, replace_content): merge_tables(db.Comment, None, source_post_id, target_post_id) def merge_relations(source_post_id, target_post_id): - table1 = db.PostRelation - table2 = sqlalchemy.orm.util.aliased(db.PostRelation) - update_stmt = (sqlalchemy.sql.expression.update(table1) - .where(table1.parent_id == source_post_id) - .where(table1.child_id != target_post_id) + alias1 = db.PostRelation + alias2 = sqlalchemy.orm.util.aliased(db.PostRelation) + update_stmt = (sqlalchemy.sql.expression.update(alias1) + .where(alias1.parent_id == source_post_id) + .where(alias1.child_id != target_post_id) .where(~sqlalchemy.exists() - .where(table2.child_id == table1.child_id) - .where(table2.parent_id == target_post_id)) + .where(alias2.child_id == alias1.child_id) + .where(alias2.parent_id == target_post_id)) .values(parent_id=target_post_id)) db.session.execute(update_stmt) - update_stmt = (sqlalchemy.sql.expression.update(table1) - .where(table1.child_id == source_post_id) - .where(table1.parent_id != target_post_id) + update_stmt = (sqlalchemy.sql.expression.update(alias1) + .where(alias1.child_id == source_post_id) + .where(alias1.parent_id != target_post_id) .where(~sqlalchemy.exists() - .where(table2.parent_id == table1.parent_id) - .where(table2.child_id == target_post_id)) + .where(alias2.parent_id == alias1.parent_id) + .where(alias2.child_id == target_post_id)) .values(child_id=target_post_id)) db.session.execute(update_stmt) diff --git a/server/szurubooru/func/tags.py b/server/szurubooru/func/tags.py index 7a4887a..fa4ee00 100644 --- a/server/szurubooru/func/tags.py +++ b/server/szurubooru/func/tags.py @@ -223,16 +223,49 @@ def merge_tags(source_tag, target_tag): assert target_tag if source_tag.tag_id == target_tag.tag_id: raise InvalidTagRelationError('Cannot merge tag with itself.') - pt1 = db.PostTag - pt2 = sqlalchemy.orm.util.aliased(db.PostTag) - update_stmt = (sqlalchemy.sql.expression.update(pt1) - .where(db.PostTag.tag_id == source_tag.tag_id) - .where(~sqlalchemy.exists() - .where(pt2.post_id == pt1.post_id) - .where(pt2.tag_id == target_tag.tag_id)) - .values(tag_id=target_tag.tag_id)) - db.session.execute(update_stmt) + def merge_posts(source_tag_id, target_tag_id): + alias1 = db.PostTag + alias2 = sqlalchemy.orm.util.aliased(db.PostTag) + update_stmt = (sqlalchemy.sql.expression.update(alias1) + .where(alias1.tag_id == source_tag_id)) + update_stmt = (update_stmt + .where(~sqlalchemy.exists() + .where(alias1.post_id == alias2.post_id) + .where(alias2.tag_id == target_tag_id))) + update_stmt = update_stmt.values(tag_id=target_tag_id) + db.session.execute(update_stmt) + + def merge_relations(table, source_tag_id, target_tag_id): + alias1 = table + alias2 = sqlalchemy.orm.util.aliased(table) + update_stmt = (sqlalchemy.sql.expression.update(alias1) + .where(alias1.parent_id == source_tag_id) + .where(alias1.child_id != target_tag_id) + .where(~sqlalchemy.exists() + .where(alias2.child_id == alias1.child_id) + .where(alias2.parent_id == target_tag_id)) + .values(parent_id=target_tag_id)) + db.session.execute(update_stmt) + + update_stmt = (sqlalchemy.sql.expression.update(alias1) + .where(alias1.child_id == source_tag_id) + .where(alias1.parent_id != target_tag_id) + .where(~sqlalchemy.exists() + .where(alias2.parent_id == alias1.parent_id) + .where(alias2.child_id == target_tag_id)) + .values(child_id=target_tag_id)) + db.session.execute(update_stmt) + + def merge_suggestions(source_tag_id, target_tag_id): + merge_relations(db.TagSuggestion, source_tag_id, target_tag_id) + + def merge_implications(source_tag_id, target_tag_id): + merge_relations(db.TagImplication, source_tag_id, target_tag_id) + + merge_posts(source_tag.tag_id, target_tag.tag_id) + merge_suggestions(source_tag.tag_id, target_tag.tag_id) + merge_implications(source_tag.tag_id, target_tag.tag_id) delete(source_tag) diff --git a/server/szurubooru/tests/func/test_tags.py b/server/szurubooru/tests/func/test_tags.py index f438a9d..d467499 100644 --- a/server/szurubooru/tests/func/test_tags.py +++ b/server/szurubooru/tests/func/test_tags.py @@ -310,7 +310,7 @@ def test_delete(tag_factory): assert db.session.query(db.Tag).count() == 2 -def test_merge_tags_without_usages(tag_factory): +def test_merge_tags_deletes_source_tag(tag_factory): source_tag = tag_factory(names=['source']) target_tag = tag_factory(names=['target']) db.session.add_all([source_tag, target_tag]) @@ -322,7 +322,15 @@ def test_merge_tags_without_usages(tag_factory): assert tag is not None -def test_merge_tags_with_usages(tag_factory, post_factory): +def test_merge_tags_with_itself(tag_factory): + source_tag = tag_factory(names=['source']) + db.session.add(source_tag) + db.session.flush() + with pytest.raises(tags.InvalidTagRelationError): + tags.merge_tags(source_tag, source_tag) + + +def test_merge_tags_moves_usages(tag_factory, post_factory): source_tag = tag_factory(names=['source']) target_tag = tag_factory(names=['target']) post = post_factory() @@ -337,62 +345,7 @@ def test_merge_tags_with_usages(tag_factory, post_factory): assert tags.get_tag_by_name('target').post_count == 1 -def test_merge_tags_with_itself(tag_factory): - source_tag = tag_factory(names=['source']) - db.session.add(source_tag) - db.session.flush() - with pytest.raises(tags.InvalidTagRelationError): - tags.merge_tags(source_tag, source_tag) - - -def test_merge_tags_with_its_child_relation(tag_factory, post_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) - source_tag.suggestions = [target_tag] - source_tag.implications = [target_tag] - post = post_factory() - post.tags = [source_tag, target_tag] - db.session.add_all([source_tag, post]) - db.session.flush() - tags.merge_tags(source_tag, target_tag) - db.session.flush() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').post_count == 1 - - -def test_merge_tags_with_its_parent_relation(tag_factory, post_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) - target_tag.suggestions = [source_tag] - target_tag.implications = [source_tag] - post = post_factory() - post.tags = [source_tag, target_tag] - db.session.add_all([source_tag, target_tag, post]) - db.session.flush() - tags.merge_tags(source_tag, target_tag) - db.session.flush() - assert tags.try_get_tag_by_name('source') is None - assert tags.get_tag_by_name('target').post_count == 1 - - -def test_merge_tags_clears_relations(tag_factory): - source_tag = tag_factory(names=['source']) - target_tag = tag_factory(names=['target']) - referring_tag = tag_factory(names=['parent']) - referring_tag.suggestions = [source_tag] - referring_tag.implications = [source_tag] - db.session.add_all([source_tag, target_tag, referring_tag]) - db.session.flush() - assert tags.try_get_tag_by_name('parent').implications != [] - assert tags.try_get_tag_by_name('parent').suggestions != [] - tags.merge_tags(source_tag, target_tag) - db.session.commit() - assert tags.try_get_tag_by_name('source') is None - assert tags.try_get_tag_by_name('parent').implications == [] - assert tags.try_get_tag_by_name('parent').suggestions == [] - - -def test_merge_tags_when_target_exists(tag_factory, post_factory): +def test_merge_tags_doesnt_duplicate_usages(tag_factory, post_factory): source_tag = tag_factory(names=['source']) target_tag = tag_factory(names=['target']) post = post_factory() @@ -407,6 +360,103 @@ def test_merge_tags_when_target_exists(tag_factory, post_factory): assert tags.get_tag_by_name('target').post_count == 1 +def test_merge_tags_moves_child_relations(tag_factory): + source_tag = tag_factory(names=['source']) + target_tag = tag_factory(names=['target']) + related_tag = tag_factory() + source_tag.suggestions = [related_tag] + source_tag.implications = [related_tag] + db.session.add_all([source_tag, target_tag, related_tag]) + db.session.commit() + assert source_tag.suggestion_count == 1 + assert source_tag.implication_count == 1 + assert target_tag.suggestion_count == 0 + assert target_tag.implication_count == 0 + tags.merge_tags(source_tag, target_tag) + db.session.commit() + assert tags.try_get_tag_by_name('source') is None + assert tags.get_tag_by_name('target').suggestion_count == 1 + assert tags.get_tag_by_name('target').implication_count == 1 + + +def test_merge_tags_doesnt_duplicate_child_relations(tag_factory): + source_tag = tag_factory(names=['source']) + target_tag = tag_factory(names=['target']) + related_tag = tag_factory() + source_tag.suggestions = [related_tag] + source_tag.implications = [related_tag] + target_tag.suggestions = [related_tag] + target_tag.implications = [related_tag] + db.session.add_all([source_tag, target_tag, related_tag]) + db.session.commit() + assert source_tag.suggestion_count == 1 + assert source_tag.implication_count == 1 + assert target_tag.suggestion_count == 1 + assert target_tag.implication_count == 1 + tags.merge_tags(source_tag, target_tag) + db.session.commit() + assert tags.try_get_tag_by_name('source') is None + assert tags.get_tag_by_name('target').suggestion_count == 1 + assert tags.get_tag_by_name('target').implication_count == 1 + + +def test_merge_tags_moves_parent_relations(tag_factory): + source_tag = tag_factory(names=['source']) + target_tag = tag_factory(names=['target']) + related_tag = tag_factory(names=['related']) + related_tag.suggestions = [related_tag] + related_tag.implications = [related_tag] + db.session.add_all([source_tag, target_tag, related_tag]) + db.session.commit() + assert source_tag.suggestion_count == 0 + assert source_tag.implication_count == 0 + assert target_tag.suggestion_count == 0 + assert target_tag.implication_count == 0 + tags.merge_tags(source_tag, target_tag) + db.session.commit() + assert tags.try_get_tag_by_name('source') is None + assert tags.get_tag_by_name('related').suggestion_count == 1 + assert tags.get_tag_by_name('related').suggestion_count == 1 + assert tags.get_tag_by_name('target').suggestion_count == 0 + assert tags.get_tag_by_name('target').implication_count == 0 + + +def test_merge_tags_doesnt_create_relation_loop_for_children(tag_factory): + source_tag = tag_factory(names=['source']) + target_tag = tag_factory(names=['target']) + source_tag.suggestions = [target_tag] + source_tag.implications = [target_tag] + db.session.add_all([source_tag, target_tag]) + db.session.commit() + assert source_tag.suggestion_count == 1 + assert source_tag.implication_count == 1 + assert target_tag.suggestion_count == 0 + assert target_tag.implication_count == 0 + tags.merge_tags(source_tag, target_tag) + db.session.commit() + assert tags.try_get_tag_by_name('source') is None + assert tags.get_tag_by_name('target').suggestion_count == 0 + assert tags.get_tag_by_name('target').implication_count == 0 + + +def test_merge_tags_doesnt_create_relation_loop_for_parents(tag_factory): + source_tag = tag_factory(names=['source']) + target_tag = tag_factory(names=['target']) + target_tag.suggestions = [source_tag] + target_tag.implications = [source_tag] + db.session.add_all([source_tag, target_tag]) + db.session.commit() + assert source_tag.suggestion_count == 0 + assert source_tag.implication_count == 0 + assert target_tag.suggestion_count == 1 + assert target_tag.implication_count == 1 + tags.merge_tags(source_tag, target_tag) + db.session.commit() + assert tags.try_get_tag_by_name('source') is None + assert tags.get_tag_by_name('target').suggestion_count == 0 + assert tags.get_tag_by_name('target').implication_count == 0 + + def test_create_tag(fake_datetime): with patch('szurubooru.func.tags.update_tag_names'), \ patch('szurubooru.func.tags.update_tag_category_name'), \