from datetime import datetime, timezone import pytest from django.contrib.auth import get_user_model from django.urls import reverse from scrobbles.models import Scrobble from tasks.models import Task @pytest.fixture def user(db): return get_user_model().objects.create(username="testuser") def make_todoist_task(): return { "source_id": "1234567890", "labels": ["chore", "inprogress"], "project_id": "9876543210", "title": "Do the dishes", "description": "Wash everything", "updated_at": "2026-08-23T12:00:00", "raw_data": {"todoist_type": "item", "todoist_event": "updated"}, } def make_emacs_task(): return { "source_id": "b2fd3053-ce26-4cdb-bf37-578387fef6c7", "state": "STRT", "labels": ["chore"], "description": "Do the dishes", "body": "Some details\n*** Description\nWash everything", "notes": {}, "properties": {"ID": "b2fd3053-ce26-4cdb-bf37-578387fef6c7"}, "drawers": {"PROPERTIES": {"ID": "b2fd3053-ce26-4cdb-bf37-578387fef6c7"}}, "timestamps": ["2026-08-23 12:00"], "updated_at": "2026-08-23T12:00:00", "source": "Org-mode", } @pytest.mark.django_db class TestTodoistScrobbleTaskLogData: def test_stores_generic_logdata(self, user): from scrobbles.scrobblers import todoist_scrobble_task scrobble = todoist_scrobble_task( make_todoist_task(), user.id, started=True, user_context_list=["Chore"] ) log = scrobble.log assert log["source_id"] == "1234567890" assert log["project_id"] == "9876543210" assert log["title"] == "Do the dishes" assert log["description"] == "Wash everything" assert log["labels"] == ["chore"] assert "inprogress" not in log["labels"] assert log["raw_data"] == { "todoist_type": "item", "todoist_event": "updated", } assert "todoist_id" not in log assert "todoist_label_list" not in log def test_finish_matches_in_progress_scrobble_by_source_id(self, user): from scrobbles.scrobblers import todoist_scrobble_task task = Task.find_or_create("Chore") Scrobble.objects.create( user=user, task=task, media_type=Scrobble.MediaType.TASK, source="Todoist", in_progress=True, timestamp=datetime(2026, 8, 23, 10, 0, tzinfo=timezone.utc), log={"source_id": "1234567890", "title": "Chore"}, ) scrobble = todoist_scrobble_task( make_todoist_task(), user.id, stopped=True, user_context_list=["Chore"] ) assert not scrobble.in_progress assert scrobble.played_to_completion @pytest.mark.django_db class TestEmacsScrobbleTaskLogData: def test_stores_generic_logdata(self, user): from scrobbles.scrobblers import emacs_scrobble_task scrobble = emacs_scrobble_task( make_emacs_task(), user.id, started=True, user_context_list=["chore"] ) log = scrobble.log assert log["source_id"] == "b2fd3053-ce26-4cdb-bf37-578387fef6c7" assert log["state"] == "STRT" assert log["title"] == "Do the dishes" assert log["description"] == "Wash everything" assert log["raw_data"] == { "properties": {"ID": "b2fd3053-ce26-4cdb-bf37-578387fef6c7"}, "drawers": {"PROPERTIES": {"ID": "b2fd3053-ce26-4cdb-bf37-578387fef6c7"}}, "timestamps": ["2026-08-23 12:00"], "source": "Org-mode", } assert "orgmode_id" not in log assert "orgmode_state" not in log assert "orgmode_properties" not in log @pytest.mark.django_db class TestConvertTaskLogToGeneric: def test_orgmode_conversion(self, user): from vrobbler.apps.tasks.utils import ( convert_orgmode_task_log_to_generic, ) task = Task.find_or_create("My Task") scrobble = Scrobble.objects.create( user=user, task=task, media_type=Scrobble.MediaType.TASK, source="Org-mode", in_progress=True, timestamp=datetime(2026, 8, 23, 10, 0, tzinfo=timezone.utc), log={ "title": "My Task", "orgmode_id": "org-123", "orgmode_state": "STRT", "orgmode_properties": {"ID": "org-123"}, "orgmode_drawers": {"PROPERTIES": {"ID": "org-123"}}, "orgmode_timestamps": ["2026-08-23 12:00"], }, ) convert_orgmode_task_log_to_generic(commit=True) scrobble.refresh_from_db() log = scrobble.log assert log["source_id"] == "org-123" assert log["state"] == "STRT" assert log["raw_data"] == { "properties": {"ID": "org-123"}, "drawers": {"PROPERTIES": {"ID": "org-123"}}, "timestamps": ["2026-08-23 12:00"], } assert "orgmode_id" not in log assert "orgmode_state" not in log def test_todoist_conversion(self, user): from vrobbler.apps.tasks.utils import ( convert_todoist_task_log_to_generic, ) task = Task.find_or_create("My Task") scrobble = Scrobble.objects.create( user=user, task=task, media_type=Scrobble.MediaType.TASK, source="Todoist", in_progress=True, timestamp=datetime(2026, 8, 23, 10, 0, tzinfo=timezone.utc), log={ "title": "My Task", "todoist_id": "1234567890", "todoist_project_id": "9876543210", "todoist_type": "item", "todoist_event": "updated", }, ) convert_todoist_task_log_to_generic(commit=True) scrobble.refresh_from_db() log = scrobble.log assert log["source_id"] == "1234567890" assert log["project_id"] == "9876543210" assert log["raw_data"] == { "todoist_type": "item", "todoist_event": "updated", } assert "todoist_id" not in log @pytest.mark.django_db class TestTaskEditLogForm: def test_form_excludes_readonly_fields(self): from tasks.models import TaskLogData form = TaskLogData.form() for excluded in ("labels", "source_id", "project_id", "state", "raw_data"): assert excluded not in form.base_fields def test_post_preserves_excluded_fields(self, user, client): task = Task.objects.create(title="Test Task", description="Test description") scrobble = Scrobble.objects.create( user=user, task=task, media_type=Scrobble.MediaType.TASK, log={ "notes": ["Original note"], "description": "Original description", "labels": ["work", "urgent"], "source_id": "org-123", "raw_data": {"properties": {"ID": "org-123"}}, }, ) url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id}) client.force_login(user) response = client.post( url, {"description": "Updated description", "notes": "Updated note"}, ) assert response.status_code == 302 scrobble.refresh_from_db() assert scrobble.log["description"] == "Updated description" assert scrobble.log["labels"] == ["work", "urgent"] assert scrobble.log["source_id"] == "org-123" assert scrobble.log["raw_data"] == {"properties": {"ID": "org-123"}}