From 490323c092a41655c88e739557706e378b77daeb Mon Sep 17 00:00:00 2001 From: mashed5894 Date: Wed, 22 Jan 2025 23:38:13 +0200 Subject: [PATCH 1/5] edited and added db functions get_entry_full_by_path & merge_entries --- tagstudio/src/core/library/alchemy/library.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 1214c13e1..18c3bc364 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -459,6 +459,32 @@ def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]: yield entry session.expunge(entry) + def get_entry_full_by_path(self, path: Path) -> Entry | None: + """Get the entry with the corresponding path.""" + with Session(self.engine) as session: + stmt = select(Entry).where(Entry.path == path) + stmt = ( + stmt.outerjoin(Entry.text_fields) + .outerjoin(Entry.datetime_fields) + .options(selectinload(Entry.text_fields), selectinload(Entry.datetime_fields)) + ) + stmt = ( + stmt.outerjoin(Entry.tags) + .outerjoin(TagAlias) + .options( + selectinload(Entry.tags).options( + joinedload(Tag.aliases), + joinedload(Tag.parent_tags), + ) + ) + ) + entry = session.scalar(stmt) + if not entry: + return None + session.expunge(entry) + make_transient(entry) + return entry + @property def entries_count(self) -> int: with Session(self.engine) as session: @@ -667,7 +693,13 @@ def search_tags( return res - def update_entry_path(self, entry_id: int | Entry, path: Path) -> None: + def update_entry_path(self, entry_id: int | Entry, path: Path) -> bool: + """Set the path field of an entry. + + Returns True if the action succeeded and False if the path already exists. + """ + if self.has_path_entry(path): + return False if isinstance(entry_id, Entry): entry_id = entry_id.id @@ -684,6 +716,7 @@ def update_entry_path(self, entry_id: int | Entry, path: Path) -> None: session.execute(update_stmt) session.commit() + return True def remove_tag(self, tag: Tag): with Session(self.engine, expire_on_commit=False) as session: @@ -1144,3 +1177,15 @@ def mirror_entry_fields(self, *entries: Entry) -> None: field_id=field.type_key, value=field.value, ) + + def merge_entries(self, from_entry: Entry, into_entry: Entry) -> None: + """Add fields and tags from the first entry to the second, and then delete the first.""" + for field in from_entry.fields: + self.add_field_to_entry( + entry_id=into_entry.id, + field_id=field.type_key, + value=field.value, + ) + tag_ids = [tag.id for tag in from_entry.tags] + self.add_tags_to_entry(into_entry.id, tag_ids) + self.remove_entries([from_entry.id]) From 8c53e2cdcc6d9aa87efe63c728a85de0135e328d Mon Sep 17 00:00:00 2001 From: mashed5894 Date: Wed, 22 Jan 2025 23:42:04 +0200 Subject: [PATCH 2/5] implemented edge case for entry existing on relinking --- tagstudio/src/core/utils/missing_files.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tagstudio/src/core/utils/missing_files.py b/tagstudio/src/core/utils/missing_files.py index 08dbf08cc..14e3221ac 100644 --- a/tagstudio/src/core/utils/missing_files.py +++ b/tagstudio/src/core/utils/missing_files.py @@ -53,8 +53,18 @@ def fix_missing_files(self) -> Iterator[int]: for i, entry in enumerate(self.missing_files, start=1): item_matches = self.match_missing_file(entry) if len(item_matches) == 1: - logger.info("fix_missing_files", entry=entry, item_matches=item_matches) - self.library.update_entry_path(entry.id, item_matches[0]) + logger.info( + "fix_missing_files", + entry=entry.path.as_posix(), + item_matches=item_matches[0].as_posix(), + ) + if not self.library.update_entry_path(entry.id, item_matches[0]): + try: + match = self.library.get_entry_full_by_path(item_matches[0]) + entry_full = self.library.get_entry_full(entry.id) + self.library.merge_entries(entry_full, match) + except AttributeError: + continue self.files_fixed_count += 1 # remove fixed file self.missing_files.remove(entry) From f8388eabef55ab842a4c3769c4f3d16ea664d63a Mon Sep 17 00:00:00 2001 From: mashed5894 Date: Thu, 23 Jan 2025 02:20:18 +0200 Subject: [PATCH 3/5] added test for merge_entries --- tagstudio/tests/test_library.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index ef88fe8bd..cf1d16771 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -308,6 +308,37 @@ def test_mirror_entry_fields(library: Library, entry_full): _FieldID.NOTES.name, } +def test_merge_entries(library: Library): + a = Entry( + folder=library.folder, path=Path("a"), + fields=[ + TextField(type_key=_FieldID.AUTHOR.name, value="Author McAuthorson", position=0), + TextField(type_key=_FieldID.DESCRIPTION.name, value="test description", position=2), + ] + ) + b = Entry( + folder=library.folder, path=Path("b"), + fields=[ TextField(type_key=_FieldID.NOTES.name, value="test note", position=1) ] + ) + try: + ids = library.add_entries([a, b]) + entry_a = library.get_entry_full(ids[0]) + entry_b = library.get_entry_full(ids[1]) + tag_0 = library.add_tag(Tag(id=1000, name="tag_0")) + tag_1 = library.add_tag(Tag(id=1001, name="tag_1")) + tag_2 = library.add_tag(Tag(id=1002, name="tag_2")) + library.add_tags_to_entry(ids[0], [tag_0.id, tag_2.id]) + library.add_tags_to_entry(ids[1], [tag_1.id]) + library.merge_entries(entry_a, entry_b) + assert library.has_path_entry(Path("b")) + assert not library.has_path_entry(Path("a")) + fields = [field.value for field in entry_a.fields] + assert "Author McAuthorson" in fields + assert "test description" in fields + assert "test note" in fields + assert b.has_tag(tag_0) and b.has_tag(tag_1) and b.has_tag(tag_2) + except AttributeError: + AssertionError() def test_remove_tag_from_entry(library, entry_full): removed_tag_id = -1 From 28e1ec929d0e97fcdf332b965c4f071978b75994 Mon Sep 17 00:00:00 2001 From: mashed5894 Date: Thu, 23 Jan 2025 02:36:43 +0200 Subject: [PATCH 4/5] ruff fixes --- tagstudio/tests/test_library.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index cf1d16771..e67370a7d 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -308,20 +308,23 @@ def test_mirror_entry_fields(library: Library, entry_full): _FieldID.NOTES.name, } + def test_merge_entries(library: Library): a = Entry( - folder=library.folder, path=Path("a"), + folder=library.folder, + path=Path("a"), fields=[ TextField(type_key=_FieldID.AUTHOR.name, value="Author McAuthorson", position=0), TextField(type_key=_FieldID.DESCRIPTION.name, value="test description", position=2), - ] + ], ) b = Entry( - folder=library.folder, path=Path("b"), - fields=[ TextField(type_key=_FieldID.NOTES.name, value="test note", position=1) ] + folder=library.folder, + path=Path("b"), + fields=[TextField(type_key=_FieldID.NOTES.name, value="test note", position=1)], ) try: - ids = library.add_entries([a, b]) + ids = library.add_entries([a, b]) entry_a = library.get_entry_full(ids[0]) entry_b = library.get_entry_full(ids[1]) tag_0 = library.add_tag(Tag(id=1000, name="tag_0")) @@ -340,6 +343,7 @@ def test_merge_entries(library: Library): except AttributeError: AssertionError() + def test_remove_tag_from_entry(library, entry_full): removed_tag_id = -1 for tag in entry_full.tags: From d55e23a67447f3bcdfa5ccb84ee3dd097d65a5ca Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:14:19 -0800 Subject: [PATCH 5/5] chore: format with ruff --- tagstudio/src/core/library/alchemy/library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 147a87d3f..d4a3bb350 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -1218,7 +1218,6 @@ def mirror_entry_fields(self, *entries: Entry) -> None: value=field.value, ) - def merge_entries(self, from_entry: Entry, into_entry: Entry) -> None: """Add fields and tags from the first entry to the second, and then delete the first.""" for field in from_entry.fields: