server/tags: merge also tag relations
This commit is contained in:
parent
995cd4610d
commit
141c9fcdc9
5 changed files with 177 additions and 95 deletions
7
API.md
7
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**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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'), \
|
||||
|
|
Loading…
Reference in a new issue