server/tags: merge also tag relations

This commit is contained in:
rr- 2016-10-22 17:57:25 +02:00
parent 995cd4610d
commit 141c9fcdc9
5 changed files with 177 additions and 95 deletions

7
API.md
View file

@ -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**

View file

@ -1,14 +1,14 @@
<div class='tag-merge'>
<form>
<p>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.</p>
<ul>
<li class='target'>
<%= ctx.makeTextInput({required: true, text: 'Target tag', pattern: ctx.tagNamePattern}) %>
</li>
<li class='confirm'>
<li>
<p>Usages in posts, suggestions and implications will be
merged. Category and aliases need to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this tag.'}) %>
</li>
</ul>

View file

@ -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)

View file

@ -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)

View file

@ -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'), \