Skip to content

Commit b351014

Browse files
authored
fix unable to delete tagged node issue (#586)
1 parent b3116a9 commit b351014

File tree

6 files changed

+212
-2
lines changed

6 files changed

+212
-2
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""add ondelete cascade to nodes tags
2+
3+
Revision ID: 88b6b2d497ea
4+
Revises: f0e0da122a9a
5+
Create Date: 2025-02-01 08:20:24.361716
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "88b6b2d497ea"
17+
down_revision: Union[str, None] = "f0e0da122a9a"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_foreign_key(
25+
"nodes_user_id_fkey",
26+
"nodes",
27+
"users",
28+
["user_id"],
29+
["id"],
30+
ondelete="CASCADE",
31+
use_alter=True,
32+
)
33+
op.drop_constraint("nodes_tags_node_id_fkey", "nodes_tags", type_="foreignkey")
34+
op.create_foreign_key(
35+
None, "nodes_tags", "nodes", ["node_id"], ["id"], ondelete="CASCADE"
36+
)
37+
op.create_foreign_key(
38+
"tag_user_id_fk", "tags", "users", ["user_id"], ["id"], use_alter=True
39+
)
40+
# ### end Alembic commands ###
41+
42+
43+
def downgrade() -> None:
44+
# ### commands auto generated by Alembic - please adjust! ###
45+
op.drop_constraint("tag_user_id_fk", "tags", type_="foreignkey")
46+
op.drop_constraint(None, "nodes_tags", type_="foreignkey")
47+
op.create_foreign_key(
48+
"nodes_tags_node_id_fkey", "nodes_tags", "nodes", ["node_id"], ["id"]
49+
)
50+
op.drop_constraint("nodes_user_id_fkey", "nodes", type_="foreignkey")
51+
# ### end Alembic commands ###
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""add on casecase=delete for tags association table
2+
3+
Revision ID: bafd773c8533
4+
Revises: 88b6b2d497ea
5+
Create Date: 2025-02-01 08:59:18.660765
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = 'bafd773c8533'
16+
down_revision: Union[str, None] = '88b6b2d497ea'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.drop_constraint('nodes_tags_tag_id_fkey', 'nodes_tags', type_='foreignkey')
24+
op.create_foreign_key(None, 'nodes_tags', 'tags', ['tag_id'], ['id'], ondelete='CASCADE')
25+
# ### end Alembic commands ###
26+
27+
28+
def downgrade() -> None:
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
op.drop_constraint(None, 'nodes_tags', type_='foreignkey')
31+
op.create_foreign_key('nodes_tags_tag_id_fkey', 'nodes_tags', 'tags', ['tag_id'], ['id'])
32+
# ### end Alembic commands ###

papermerge/core/features/nodes/tests/test_nodes_router.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,58 @@ def test_delete_nodes_with_descendants(
581581
assert found is None
582582

583583

584+
def test_delete_tagged_folder(make_folder, db_session, user, auth_api_client):
585+
folder = make_folder(title="My Documents", user=user, parent=user.home_folder)
586+
587+
nodes_dbapi.assign_node_tags(
588+
db_session,
589+
node_id=folder.id,
590+
tags=["tag1", "tag2"],
591+
user_id=user.id,
592+
)
593+
594+
# delete tagged folder
595+
response = auth_api_client.delete(f"/nodes/", json=[str(folder.id)])
596+
597+
assert response.status_code == 200, response.json()
598+
599+
# folder is not there anymore
600+
q = db_session.query(orm.Folder).filter(orm.Folder.id == folder.id)
601+
assert db_session.query(q.exists()).scalar() is False
602+
603+
# but both tags are
604+
q_tag1 = db_session.query(orm.Tag).filter(orm.Tag.name == "tag1")
605+
q_tag2 = db_session.query(orm.Tag).filter(orm.Tag.name == "tag2")
606+
assert db_session.query(q_tag1.exists()).scalar() is True
607+
assert db_session.query(q_tag2.exists()).scalar() is True
608+
609+
610+
def test_delete_tagged_document(make_document, db_session, user, auth_api_client):
611+
doc = make_document(title="My Contract.pdf", user=user, parent=user.home_folder)
612+
613+
nodes_dbapi.assign_node_tags(
614+
db_session,
615+
node_id=doc.id,
616+
tags=["tag1", "tag2"],
617+
user_id=user.id,
618+
)
619+
620+
# delete tagged document
621+
response = auth_api_client.delete(f"/nodes/", json=[str(doc.id)])
622+
623+
assert response.status_code == 200, response.json()
624+
625+
# document is not there anymore
626+
q = db_session.query(orm.Document).filter(orm.Document.id == doc.id)
627+
assert db_session.query(q.exists()).scalar() is False
628+
629+
# but both tags are
630+
q_tag1 = db_session.query(orm.Tag).filter(orm.Tag.name == "tag1")
631+
q_tag2 = db_session.query(orm.Tag).filter(orm.Tag.name == "tag2")
632+
assert db_session.query(q_tag1.exists()).scalar() is True
633+
assert db_session.query(q_tag2.exists()).scalar() is True
634+
635+
584636
def test_get_node_tags_router_when_node_is_folder(
585637
make_folder, db_session, user, auth_api_client
586638
):

papermerge/core/features/tags/db/api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ def get_tag(
9595
error = schema.Error(messages=[str(e)])
9696
return None, error
9797

98+
if db_item is None:
99+
raise EntityNotFound
100+
98101
return schema.Tag.model_validate(db_item), None
99102

100103

papermerge/core/features/tags/db/orm.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
class NodeTagsAssociation(Base):
99
__tablename__ = "nodes_tags"
1010
id: Mapped[int] = mapped_column(primary_key=True)
11-
node_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("nodes.id"))
11+
node_id: Mapped[uuid.UUID] = mapped_column(
12+
ForeignKey("nodes.id", ondelete="CASCADE")
13+
)
1214
tag_id: Mapped[uuid.UUID] = mapped_column(
13-
ForeignKey("tags.id"),
15+
ForeignKey("tags.id", ondelete="CASCADE"),
1416
)
1517

1618

papermerge/core/features/tags/tests/test_router_tags.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import uuid
2+
13
from sqlalchemy import func
24

35
from papermerge.core.db.engine import Session
46
from papermerge.core import orm, schema
57
from papermerge.core.tests.types import AuthTestClient
8+
from papermerge.core.features.nodes.db import api as nodes_dbapi
69

710

811
def test_create_tag_route(auth_api_client: AuthTestClient, db_session: Session):
@@ -49,6 +52,19 @@ def test_get_tag_route(auth_api_client: AuthTestClient, make_tag, user):
4952
assert response.status_code == 200, response.json()
5053

5154

55+
def test_get_non_existing_tag(auth_api_client: AuthTestClient, make_tag, user):
56+
"""
57+
In this use case valid UUID is passed - however there is no tag
58+
with such UUID
59+
"""
60+
61+
response = auth_api_client.get(
62+
f"/tags/{uuid.uuid4()}",
63+
)
64+
65+
assert response.status_code == 404, response.json()
66+
67+
5268
def test_delete_tag_route(auth_api_client: AuthTestClient, db_session, make_tag, user):
5369
tag: schema.Tag = make_tag(name="draft", bg_color="red", user=user)
5470

@@ -133,3 +149,57 @@ def test_get_all_tags_no_pagination_per_user(make_tag, make_api_client):
133149
items_user_b = [schema.Tag(**item) for item in response.json()]
134150

135151
assert len(items_user_b) == user_b_tags_count
152+
153+
154+
def test_delete_tag_which_has_associated_folder(
155+
make_folder, make_tag, db_session, user, auth_api_client
156+
):
157+
folder = make_folder(title="My Documents", user=user, parent=user.home_folder)
158+
tag = make_tag(name="important", user=user)
159+
160+
nodes_dbapi.assign_node_tags(
161+
db_session,
162+
node_id=folder.id,
163+
tags=["important"],
164+
user_id=user.id,
165+
)
166+
167+
# delete tagged folder
168+
response = auth_api_client.delete(f"/tags/{tag.id}")
169+
170+
assert response.status_code == 204
171+
172+
# folder still exists
173+
q = db_session.query(orm.Folder).filter(orm.Folder.id == folder.id)
174+
assert db_session.query(q.exists()).scalar() is True
175+
176+
# but tag was deleted
177+
q_tag = db_session.query(orm.Tag).filter(orm.Tag.name == "important")
178+
assert db_session.query(q_tag.exists()).scalar() is False
179+
180+
181+
def test_delete_tag_which_has_associated_document(
182+
make_document, make_tag, db_session, user, auth_api_client
183+
):
184+
doc = make_document(title="My Contract", user=user, parent=user.home_folder)
185+
tag = make_tag(name="important", user=user)
186+
187+
nodes_dbapi.assign_node_tags(
188+
db_session,
189+
node_id=doc.id,
190+
tags=["important"],
191+
user_id=user.id,
192+
)
193+
194+
# delete tagged folder
195+
response = auth_api_client.delete(f"/tags/{tag.id}")
196+
197+
assert response.status_code == 204
198+
199+
# document still exists
200+
q = db_session.query(orm.Document).filter(orm.Document.id == doc.id)
201+
assert db_session.query(q.exists()).scalar() is True
202+
203+
# but tag was deleted
204+
q_tag = db_session.query(orm.Tag).filter(orm.Tag.name == "important")
205+
assert db_session.query(q_tag.exists()).scalar() is False

0 commit comments

Comments
 (0)