fix(time-entries): use server time for running timers
This commit is contained in:
@@ -93,7 +93,7 @@ class TimeEntryCreateSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
workspace_id = serializers.UUIDField()
|
workspace_id = serializers.UUIDField()
|
||||||
project_id = serializers.UUIDField(required=False, allow_null=True)
|
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)
|
end_time = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
description = serializers.CharField(required=False, allow_blank=True, default="")
|
description = serializers.CharField(required=False, allow_blank=True, default="")
|
||||||
tags = serializers.ListField(child=serializers.UUIDField(), required=False)
|
tags = serializers.ListField(child=serializers.UUIDField(), required=False)
|
||||||
@@ -102,6 +102,12 @@ class TimeEntryCreateSerializer(serializers.Serializer):
|
|||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
user = self.context.get("request").user if self.context.get("request") else None
|
user = self.context.get("request").user if self.context.get("request") else None
|
||||||
workspace_id = attrs.get("workspace_id")
|
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)
|
project_id = attrs.pop("project_id", serializers.empty)
|
||||||
if project_id is not serializers.empty:
|
if project_id is not serializers.empty:
|
||||||
if project_id is None:
|
if project_id is None:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def _verify_workspace_access(user, workspace_id):
|
|||||||
raise PermissionDenied("You do not have access to this workspace.")
|
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.
|
Creates a new time entry. If end_time is None, it acts as a running timer.
|
||||||
"""
|
"""
|
||||||
@@ -38,6 +38,11 @@ def create_time_entry(user, workspace_id, start_time, end_time=None, project=Non
|
|||||||
if has_running_timer:
|
if has_running_timer:
|
||||||
raise ValidationError({"non_field_errors": "You already have a running timer in this workspace."})
|
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:
|
if start_time and end_time and start_time >= end_time:
|
||||||
raise ValidationError({"end_time": "End time must be strictly after start time."})
|
raise ValidationError({"end_time": "End time must be strictly after start time."})
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ class TimeEntryServiceTests(TestCase):
|
|||||||
self.assertIsNotNone(stopped_entry.end_time)
|
self.assertIsNotNone(stopped_entry.end_time)
|
||||||
self.assertIsNotNone(stopped_entry.duration)
|
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):
|
def test_update_time_entry_preserves_deleted_project_and_tags(self):
|
||||||
project = Project.objects.create(workspace=self.workspace, name="Deleted project")
|
project = Project.objects.create(workspace=self.workspace, name="Deleted project")
|
||||||
tag = Tag.objects.create(
|
tag = Tag.objects.create(
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ def make_aware(year, month, day, hour=9, minute=0, second=0):
|
|||||||
|
|
||||||
|
|
||||||
class TimeEntryViewTests(APITestCase):
|
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):
|
def test_time_entry_list_returns_grouped_payload_for_ended_entries(self):
|
||||||
user = User.objects.create_user(mobile="09126666666", password="secret123")
|
user = User.objects.create_user(mobile="09126666666", password="secret123")
|
||||||
workspace = Workspace.objects.create(name="Core", owner=user)
|
workspace = Workspace.objects.create(name="Core", owner=user)
|
||||||
|
|||||||
Reference in New Issue
Block a user