from datetime import datetime from django.utils import timezone from rest_framework.test import APITestCase from apps.projects.models import Project, ProjectAccess 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, WorkspaceMembership def make_aware(year, month, day, hour=9, minute=0, second=0): current_timezone = timezone.get_current_timezone() return timezone.make_aware( datetime(year, month, day, hour, minute, second), current_timezone, ) class TimeEntryViewTests(APITestCase): def test_time_entry_list_returns_grouped_payload_for_ended_entries(self): user = User.objects.create_user(mobile="09126666666", password="secret123") workspace = Workspace.objects.create(name="Core", owner=user) first_entry = TimeEntry.objects.create( workspace=workspace, user=user, description="Morning work", start_time=make_aware(2026, 4, 24, 9, 0, 0), end_time=make_aware(2026, 4, 24, 10, 30, 0), ) TimeEntry.objects.create( workspace=workspace, user=user, description="Running work", start_time=make_aware(2026, 4, 24, 11, 0, 0), ) self.client.force_authenticate(user=user) response = self.client.get( "/api/time-entries/", { "workspace": str(workspace.id), "status": "ended", "limit": 10, "offset": 0, }, ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data["current_page_items_count"], 1) self.assertFalse(response.data["has_more"]) self.assertEqual(len(response.data["groups"]), 1) self.assertEqual(len(response.data["groups"][0]["days"]), 1) self.assertEqual( response.data["groups"][0]["days"][0]["entries"][0]["id"], str(first_entry.id), ) def test_time_entry_update_preserves_current_deleted_tags(self): 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() self.client.force_authenticate(user=user) response = self.client.patch( f"/api/time-entries/{entry.id}/", { "description": "Still editable", "tags": [str(tag.id)], }, format="json", ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data["description"], "Still editable") self.assertTrue(response.data["tag_details"][0]["is_deleted"]) def test_time_entry_update_rejects_new_deleted_tag_attachment(self): 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), ) self.client.force_authenticate(user=user) response = self.client.patch( f"/api/time-entries/{entry.id}/", {"tags": [str(deleted_tag.id)]}, format="json", ) self.assertEqual(response.status_code, 400) self.assertIn("unavailable", response.data["error"].lower()) def test_time_entry_update_can_remove_current_deleted_tag(self): 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() self.client.force_authenticate(user=user) response = self.client.patch( f"/api/time-entries/{entry.id}/", {"tags": []}, format="json", ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data["tags"], []) def test_member_cannot_create_time_entry_for_inaccessible_project(self): owner = User.objects.create_user(mobile="09120000001", password="secret123") member = User.objects.create_user(mobile="09120000002", password="secret123") workspace = Workspace.objects.create(name="Core", owner=owner) WorkspaceMembership.objects.create( workspace=workspace, user=member, role=WorkspaceMembership.Role.MEMBER, is_active=True, ) project = Project.objects.create(workspace=workspace, name="Restricted") self.client.force_authenticate(user=member) response = self.client.post( "/api/time-entries/", { "workspace_id": str(workspace.id), "project_id": str(project.id), "description": "Blocked", "start_time": make_aware(2026, 4, 24, 9, 0, 0).isoformat(), "end_time": make_aware(2026, 4, 24, 10, 0, 0).isoformat(), }, format="json", ) self.assertEqual(response.status_code, 400) self.assertTrue( any("Selected project is unavailable." in item["message"] for item in response.data["messages"]) ) def test_member_can_create_time_entry_after_project_access_is_granted(self): owner = User.objects.create_user(mobile="09120000011", password="secret123") member = User.objects.create_user(mobile="09120000012", password="secret123") workspace = Workspace.objects.create(name="Core", owner=owner) WorkspaceMembership.objects.create( workspace=workspace, user=member, role=WorkspaceMembership.Role.MEMBER, is_active=True, ) project = Project.objects.create(workspace=workspace, name="Accessible") ProjectAccess.objects.create(project=project, user=member) self.client.force_authenticate(user=member) response = self.client.post( "/api/time-entries/", { "workspace_id": str(workspace.id), "project_id": str(project.id), "description": "Allowed", "start_time": make_aware(2026, 4, 24, 9, 0, 0).isoformat(), "end_time": make_aware(2026, 4, 24, 10, 0, 0).isoformat(), }, format="json", ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data["project"], str(project.id))