fix(time-entries): preserve deleted tags in timesheet edits

This commit is contained in:
2026-04-27 22:58:27 +03:30
parent 7bd60fd641
commit 02c9c17c30
5 changed files with 331 additions and 86 deletions

View File

@@ -4,6 +4,8 @@ from django.utils import timezone
from apps.time_entries.api.serializers import TimeEntrySerializer
from apps.time_entries.models import TimeEntry
from apps.projects.models import Project
from apps.tags.models import Tag
from apps.users.models import User
from apps.workspaces.models import Workspace
@@ -27,3 +29,31 @@ def test_time_entry_serializer_keeps_seconds(db):
assert data["start_time"] == start_time.strftime("%Y-%m-%d %H:%M:%S")
assert data["end_time"] == end_time.strftime("%Y-%m-%d %H:%M:%S")
def test_time_entry_serializer_includes_deleted_project_and_tags(db):
user = User.objects.create_user(mobile="09124444444", password="secret123")
workspace = Workspace.objects.create(name="Core", owner=user)
project = Project.objects.create(workspace=workspace, name="Legacy Project")
tag = Tag.objects.create(workspace=workspace, name="Legacy Tag", color="#334155")
project.delete()
tag.delete()
entry = TimeEntry.objects.create(
workspace=workspace,
user=user,
project=Project.all_objects.get(id=project.id),
description="Historical work",
start_time=timezone.now(),
end_time=timezone.now(),
)
entry.tags.set([Tag.all_objects.get(id=tag.id)])
data = TimeEntrySerializer(entry).data
assert data["project"] == str(project.id)
assert data["project_details"]["name"] == "Legacy Project"
assert data["project_details"]["is_deleted"] is True
assert data["tags"] == [str(tag.id)]
assert data["tag_details"][0]["name"] == "Legacy Tag"
assert data["tag_details"][0]["is_deleted"] is True

View File

@@ -4,7 +4,9 @@ import pytest
from django.utils import timezone
from rest_framework.exceptions import ValidationError
from apps.time_entries.services.time_entries import create_time_entry, stop_time_entry
from apps.projects.models import Project
from apps.tags.models import Tag
from apps.time_entries.services.time_entries import create_time_entry, stop_time_entry, update_time_entry
from apps.users.models import User
from apps.workspaces.models import Workspace
@@ -45,3 +47,32 @@ def test_stop_time_entry_sets_end_time_and_duration(workspace_owner):
assert stopped_entry.end_time is not None
assert stopped_entry.duration is not None
def test_update_time_entry_preserves_deleted_project_and_tags(workspace_owner):
user, workspace = workspace_owner
project = Project.objects.create(workspace=workspace, name="Deleted project")
tag = Tag.objects.create(workspace=workspace, name="Deleted tag", color="#0f172a")
entry = create_time_entry(
user=user,
workspace_id=workspace.id,
start_time=timezone.now() - timedelta(hours=1),
end_time=timezone.now(),
project=project,
tags=[tag],
description="Before delete",
)
project.delete()
tag.delete()
updated_entry = update_time_entry(
entry,
project=Project.all_objects.get(id=project.id),
tags=[Tag.all_objects.get(id=tag.id)],
description="After delete",
)
assert updated_entry.description == "After delete"
assert updated_entry.project_id == project.id
assert list(Tag.all_objects.filter(time_entries=updated_entry).values_list("id", flat=True)) == [tag.id]

View File

@@ -3,6 +3,7 @@ from datetime import datetime
from django.utils import timezone
from rest_framework.test import APIClient
from apps.tags.models import Tag
from apps.time_entries.models import TimeEntry
from apps.users.models import User
from apps.workspaces.models import Workspace
@@ -49,3 +50,91 @@ def test_time_entry_list_returns_grouped_payload_for_ended_entries(db):
assert len(response.data["groups"]) == 1
assert len(response.data["groups"][0]["days"]) == 1
assert response.data["groups"][0]["days"][0]["entries"][0]["id"] == str(first_entry.id)
def test_time_entry_update_preserves_current_deleted_tags(db):
user = User.objects.create_user(mobile="09127777777", password="secret123")
workspace = Workspace.objects.create(name="Core", owner=user)
tag = Tag.objects.create(workspace=workspace, name="Legacy Tag", color="#475569")
entry = TimeEntry.objects.create(
workspace=workspace,
user=user,
description="Old",
start_time=make_aware(2026, 4, 24, 9, 0, 0),
end_time=make_aware(2026, 4, 24, 10, 30, 0),
)
entry.tags.set([tag])
tag.delete()
client = APIClient()
client.force_authenticate(user=user)
response = client.patch(
f"/api/time-entries/{entry.id}/",
{
"description": "Still editable",
"tags": [str(tag.id)],
},
format="json",
)
assert response.status_code == 200
assert response.data["description"] == "Still editable"
assert response.data["tag_details"][0]["is_deleted"] is True
def test_time_entry_update_rejects_new_deleted_tag_attachment(db):
user = User.objects.create_user(mobile="09128888888", password="secret123")
workspace = Workspace.objects.create(name="Core", owner=user)
deleted_tag = Tag.objects.create(workspace=workspace, name="Deleted tag", color="#475569")
deleted_tag.delete()
entry = TimeEntry.objects.create(
workspace=workspace,
user=user,
description="Entry",
start_time=make_aware(2026, 4, 24, 9, 0, 0),
end_time=make_aware(2026, 4, 24, 10, 30, 0),
)
client = APIClient()
client.force_authenticate(user=user)
response = client.patch(
f"/api/time-entries/{entry.id}/",
{
"tags": [str(deleted_tag.id)],
},
format="json",
)
assert response.status_code == 400
assert "unavailable" in response.data["error"].lower()
def test_time_entry_update_can_remove_current_deleted_tag(db):
user = User.objects.create_user(mobile="09129999999", password="secret123")
workspace = Workspace.objects.create(name="Core", owner=user)
deleted_tag = Tag.objects.create(workspace=workspace, name="Deleted tag", color="#475569")
entry = TimeEntry.objects.create(
workspace=workspace,
user=user,
description="Entry",
start_time=make_aware(2026, 4, 24, 9, 0, 0),
end_time=make_aware(2026, 4, 24, 10, 30, 0),
)
entry.tags.set([deleted_tag])
deleted_tag.delete()
client = APIClient()
client.force_authenticate(user=user)
response = client.patch(
f"/api/time-entries/{entry.id}/",
{
"tags": [],
},
format="json",
)
assert response.status_code == 200
assert response.data["tags"] == []