diff --git a/apps/time_entries/api/serializers.py b/apps/time_entries/api/serializers.py index 4e526da..01b987b 100644 --- a/apps/time_entries/api/serializers.py +++ b/apps/time_entries/api/serializers.py @@ -93,7 +93,7 @@ class TimeEntryCreateSerializer(serializers.Serializer): """ workspace_id = serializers.UUIDField() project_id = serializers.UUIDField(required=False, allow_null=True) - start_time = serializers.DateTimeField() + start_time = serializers.DateTimeField(required=False) end_time = serializers.DateTimeField(required=False, allow_null=True) description = serializers.CharField(required=False, allow_blank=True, default="") tags = serializers.ListField(child=serializers.UUIDField(), required=False) @@ -102,6 +102,12 @@ class TimeEntryCreateSerializer(serializers.Serializer): def validate(self, attrs): user = self.context.get("request").user if self.context.get("request") else None workspace_id = attrs.get("workspace_id") + start_time = attrs.get("start_time") + end_time = attrs.get("end_time") + + if end_time is not None and start_time is None: + raise serializers.ValidationError({"start_time": "Start time is required when end time is provided."}) + project_id = attrs.pop("project_id", serializers.empty) if project_id is not serializers.empty: if project_id is None: diff --git a/apps/time_entries/services/time_entries.py b/apps/time_entries/services/time_entries.py index fa6c4c2..01163af 100644 --- a/apps/time_entries/services/time_entries.py +++ b/apps/time_entries/services/time_entries.py @@ -22,24 +22,29 @@ def _verify_workspace_access(user, workspace_id): raise PermissionDenied("You do not have access to this workspace.") -def create_time_entry(user, workspace_id, start_time, end_time=None, project=None, tags=None, description="", is_billable=False): +def create_time_entry(user, workspace_id, start_time=None, end_time=None, project=None, tags=None, description="", is_billable=False): """ Creates a new time entry. If end_time is None, it acts as a running timer. """ _verify_workspace_access(user, workspace_id) - if not end_time: - has_running_timer = TimeEntry.objects.filter( - workspace_id=workspace_id, - user=user, - end_time__isnull=True, - is_deleted=False - ).exists() - if has_running_timer: - raise ValidationError({"non_field_errors": "You already have a running timer in this workspace."}) - - if start_time and end_time and start_time >= end_time: - raise ValidationError({"end_time": "End time must be strictly after start time."}) + if not end_time: + has_running_timer = TimeEntry.objects.filter( + workspace_id=workspace_id, + user=user, + end_time__isnull=True, + is_deleted=False + ).exists() + if has_running_timer: + raise ValidationError({"non_field_errors": "You already have a running timer in this workspace."}) + + if start_time is None: + if end_time is not None: + raise ValidationError({"start_time": "Start time is required when end time is provided."}) + start_time = timezone.now() + + if start_time and end_time and start_time >= end_time: + raise ValidationError({"end_time": "End time must be strictly after start time."}) if project and project.workspace_id != workspace_id: raise ValidationError({"project": "Project must belong to the same workspace."}) diff --git a/apps/time_entries/tests/test_services.py b/apps/time_entries/tests/test_services.py index 241ccc4..05925c1 100644 --- a/apps/time_entries/tests/test_services.py +++ b/apps/time_entries/tests/test_services.py @@ -47,6 +47,18 @@ class TimeEntryServiceTests(TestCase): self.assertIsNotNone(stopped_entry.end_time) self.assertIsNotNone(stopped_entry.duration) + def test_create_running_time_entry_defaults_start_time_to_server_now(self): + before = timezone.now() + entry = create_time_entry( + user=self.user, + workspace_id=self.workspace.id, + ) + after = timezone.now() + + self.assertIsNone(entry.end_time) + self.assertGreaterEqual(entry.start_time, before) + self.assertLessEqual(entry.start_time, after) + def test_update_time_entry_preserves_deleted_project_and_tags(self): project = Project.objects.create(workspace=self.workspace, name="Deleted project") tag = Tag.objects.create( diff --git a/apps/time_entries/tests/test_views.py b/apps/time_entries/tests/test_views.py index a4782a6..6116241 100644 --- a/apps/time_entries/tests/test_views.py +++ b/apps/time_entries/tests/test_views.py @@ -19,6 +19,28 @@ def make_aware(year, month, day, hour=9, minute=0, second=0): class TimeEntryViewTests(APITestCase): + def test_create_running_time_entry_without_start_time_uses_server_time(self): + user = User.objects.create_user(mobile="09125555555", password="secret123") + workspace = Workspace.objects.create(name="Core", owner=user) + + self.client.force_authenticate(user=user) + before = timezone.now() + response = self.client.post( + "/api/time-entries/", + { + "workspace_id": str(workspace.id), + "description": "Running work", + }, + format="json", + ) + after = timezone.now() + + self.assertEqual(response.status_code, 201) + entry = TimeEntry.objects.get(id=response.data["id"]) + self.assertIsNone(entry.end_time) + self.assertGreaterEqual(entry.start_time, before) + self.assertLessEqual(entry.start_time, after) + 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)