From 1206dee2906a8100c21fee6ccfb820b3e4d57626 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sun, 23 Aug 2026 17:28:30 -0400 Subject: [PATCH] [tasks] Clean up fields and simplify form --- CHANGELOG.org | 54 ++--- PROJECT.org | 49 ++-- tests/scrobbles_tests/test_orgmode_notes.py | 2 +- tests/tasks_tests/test_task_logdata.py | 223 ++++++++++++++++++ vrobbler/apps/scrobbles/forms.py | 6 +- .../commands/convert_task_log_data.py | 5 + vrobbler/apps/scrobbles/scrobblers.py | 59 +++-- vrobbler/apps/scrobbles/views.py | 9 +- vrobbler/apps/tasks/models.py | 52 ++-- vrobbler/apps/tasks/utils.py | 48 +++- vrobbler/apps/tasks/webhooks.py | 36 +-- .../templates/scrobbles/scrobble_detail.html | 9 +- 12 files changed, 427 insertions(+), 125 deletions(-) create mode 100644 tests/tasks_tests/test_task_logdata.py diff --git a/CHANGELOG.org b/CHANGELOG.org index 2d0ae34..5e045c2 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,5 +1,30 @@ #+title: CHANGELOG +* Version 65.6 [1/1] +** DONE [#B] TODOIST_* variable names flagged as TODOs (false positives) :tasks:metadata:cleanup: +:PROPERTIES: +:ID: b2fd3053-ce26-4cdb-bf37-578387fef6c7 +:END: + +*** Description + +Task logdata now uses generic, source-agnostic field names instead of +per-source prefixed keys. `TaskLogData` gained `source_id`, `project_id`, +`state` and `raw_data` (the full remaining source payload kept for future +reference, matching the music track `raw_data` convention); the old `orgmode_*` +and `todoist_*` keys are gone. The todoist and emacs scrobblers and webhooks +normalize into the generic fields, and all `log__orgmode_id` / `log__todoist_id` +lookups became `log__source_id`. `Task.source_url_for_user()` now reads +`source_id` (fixing the `todist_id` typo and the never-matching log `source` +check). New `convert_orgmode_task_log_to_generic` / +`convert_todoist_task_log_to_generic` helpers (wired into the +`convert_task_log_data` management command) migrate existing scrobble logs. +The scrobble "Edit Log" form no longer renders the read-only fields (labels, +source_id, project_id, state, raw_data) as disabled inputs — they are excluded +from the form and preserved on save — and the form now defaults to being +toggled open. Task labels render as read-only badges in the detail view +instead. + * Version 65.5 [2/2] ** DONE [#A] Fix database backup bug where running in celery task fails :bug:backups:celery:tasks: :PROPERTIES: @@ -34,35 +59,6 @@ The gitea action, Drone pipeline, Makefile deploy target, and the rc.d script itself are updated so every deploy restarts the three celery workers plus celerybeat. -* Version 65.4 [2/2] -** DONE [#B] Rename the default,charts celery worker to vrobbler_celery_charts :celery:deploy: -:PROPERTIES: -:ID: 92686951-8c87-4eae-8c48-1bed87567e93 -:END: - -*** Description - -The FreeBSD rc.d worker that consumes the `default,charts` queues is renamed -from `vrobbler_celery` to `vrobbler_celery_charts`, matching the naming of its -companion workers (`vrobbler_celery_priority`, `vrobbler_celery_background`). -The gitea action, Drone pipeline, Makefile deploy target, and the rc.d script -itself are updated so every deploy restarts the three celery workers plus -celerybeat. - -** DONE [#A] Fix database backup bug where running in celery task fails :bug:backups:celery:tasks: -:PROPERTIES: -:ID: 4117482f-4774-49b8-93b2-5b97adb194bd -:END: - -*** Description - -The `backup_database` celery task failed with `No such file or directory -'pg_dump'` even though the manual management command worked. FreeBSD rc.d -celery workers run under daemon(8) with a minimal PATH that omits -`/usr/local/bin`. The task now resolves `pg_dump` via `shutil.which()`, -falling back to the common PostgreSQL install locations, instead of relying -on the inherited PATH. - * Version 65.3 [1/1] ** DONE [#B] Trail scrobbles without names should try to use trial heads loc :trails:metadata: :PROPERTIES: diff --git a/PROJECT.org b/PROJECT.org index fc27dfb..5925a93 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -505,21 +505,6 @@ This may be a no-op situation as we've moved away from Google Books. But maybe w - =Book.find_or_create()= only matches by =original_title=, which is fragile. Should also match by Google Books ID or author name (like Track does). -** TODO [#B] TODOIST_* variable names flagged as TODOs (false positives) :tasks:metadata:cleanup: -*** Description - -Need to clean up how we store task data in logdata. - -*** Implementation - -Should clean up the task logdata dict so that we have fewer source-specific names, try to generalize task detail implementation from the input source, while keeping the full task payload for future reference. - -- Files: - - ~vrobbler/settings.py~ (lines 72-73) -- =TODOIST_CLIENT_ID=, =TODOIST_CLIENT_SECRET= - - ~vrobbler/apps/scrobbles/constants.py~ (line 39) -- =TODOIST_TASK_URL= - - ~vrobbler/apps/tasks/models.py~ (line 13) -- =TODOIST_TASK_URL= -- These are variable/constant names for the Todoist integration, not actual - TODOs. No action needed, but worth noting they show up in searches. ** TODO [#B] Add AllTrails as a source for Trail data :trails:feature: :PROPERTIES: :ID: 39313362-cdfe-46e7-bbd4-9139a65c0b3c @@ -626,5 +611,37 @@ The Edit log form should have from top to bottom: - People (which should be similar to the Bird widget on BirdLocation and allow setting per user score, win true/false, rank, new true/false, seat_ordrer) - Expansion ids (which should a multi-select widget of expansions for this game) - Location (which should be a drop down of BoardGameLocations for this user) - +** DONE [#B] TODOIST_* variable names flagged as TODOs (false positives) :tasks:metadata:cleanup: +:PROPERTIES: +:ID: b2fd3053-ce26-4cdb-bf37-578387fef6c7 +:END: +*** Description +Need to clean up how we store task data in logdata. + +*** Implementation + +Task logdata now uses generic, source-agnostic field names instead of +per-source prefixed keys. `TaskLogData` gained `source_id`, `project_id`, +`state` and `raw_data` (the full remaining source payload kept for future +reference, matching the music track `raw_data` convention); the old +`orgmode_id`/`orgmode_state`/`orgmode_properties`/ +`orgmode_drawers`/`orgmode_timestamps` and `todoist_id`/`todoist_project_id` +keys are gone. The todoist and emacs scrobblers and webhooks normalize into the +generic fields, and all `log__orgmode_id` / `log__todoist_id` lookups became +`log__source_id`. `Task.source_url_for_user()` now reads `source_id` (fixing the +`todist_id` typo and the never-matching log `source` check). New +`convert_orgmode_task_log_to_generic` / `convert_todoist_task_log_to_generic` +helpers (wired into the `convert_task_log_data` management command) migrate +existing scrobble logs. The scrobble "Edit Log" form no longer renders the +read-only fields (labels, source_id, project_id, state, raw_data) as disabled +inputs — they are excluded from the form and preserved on save — and the form +now defaults to being toggled open. Task labels render as read-only badges in +the detail view instead. + +- Files: + - ~vrobbler/settings.py~ (lines 108-109) -- =TODOIST_CLIENT_ID=, =TODOIST_CLIENT_SECRET= + - ~vrobbler/apps/scrobbles/constants.py~ (line 74) -- =TODOIST_TASK_URL= +- These are variable/constant names for the Todoist integration, not actual + TODOs. No action needed, but worth noting they show up in searches. + diff --git a/tests/scrobbles_tests/test_orgmode_notes.py b/tests/scrobbles_tests/test_orgmode_notes.py index aa6b5d1..60d6fe5 100644 --- a/tests/scrobbles_tests/test_orgmode_notes.py +++ b/tests/scrobbles_tests/test_orgmode_notes.py @@ -105,7 +105,7 @@ def test_emacs_scrobble_update_task_stores_cleaned_notes(): timestamp=datetime(2026, 6, 1, 10, 0, tzinfo=timezone.utc), log={ "title": "My Org Task", - "orgmode_id": "org-123", + "source_id": "org-123", "notes": {}, }, ) diff --git a/tests/tasks_tests/test_task_logdata.py b/tests/tasks_tests/test_task_logdata.py new file mode 100644 index 0000000..3773388 --- /dev/null +++ b/tests/tasks_tests/test_task_logdata.py @@ -0,0 +1,223 @@ +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"}} diff --git a/vrobbler/apps/scrobbles/forms.py b/vrobbler/apps/scrobbles/forms.py index 847859d..614c456 100644 --- a/vrobbler/apps/scrobbles/forms.py +++ b/vrobbler/apps/scrobbles/forms.py @@ -82,12 +82,12 @@ def form_from_dataclass(dataclass): form_fields[f.name] = override_fields[f.name] continue + if f.name in dataclass._excluded_fields: + continue + required = f.default is None and f.default_factory is None form_fields[f.name] = django_form_field_from_type(f.type, required=required) - if f.name in dataclass._excluded_fields: - form_fields[f.name].disabled = True - form_cls = type(f"{dataclass.__name__}Form", (forms.Form,), form_fields) if "notes" in form_cls.base_fields and "notes" not in override_fields: diff --git a/vrobbler/apps/scrobbles/management/commands/convert_task_log_data.py b/vrobbler/apps/scrobbles/management/commands/convert_task_log_data.py index 43a0d70..334463f 100644 --- a/vrobbler/apps/scrobbles/management/commands/convert_task_log_data.py +++ b/vrobbler/apps/scrobbles/management/commands/convert_task_log_data.py @@ -1,7 +1,10 @@ from django.core.management.base import BaseCommand + from vrobbler.apps.tasks.utils import ( convert_old_boardgame_log_to_new, + convert_orgmode_task_log_to_generic, convert_tasks_notes_list_to_dict, + convert_todoist_task_log_to_generic, ) @@ -21,3 +24,5 @@ class Command(BaseCommand): print("No changes will be saved, use --commit to save") convert_tasks_notes_list_to_dict(commit) convert_old_boardgame_log_to_new(commit) + convert_orgmode_task_log_to_generic(commit) + convert_todoist_task_log_to_generic(commit) diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index 01b8a7d..ad9ac64 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -715,7 +715,7 @@ def todoist_scrobble_task_finish( ) -> Optional[Scrobble]: scrobble = Scrobble.objects.filter( user_id=user_id, - log__todoist_id=todoist_task.get("todoist_id"), + log__source_id=todoist_task.get("source_id"), in_progress=True, played_to_completion=False, ).first() @@ -737,7 +737,7 @@ def todoist_scrobble_update_task( scrobble = Scrobble.objects.filter( in_progress=True, user_id=user_id, - log__todoist_id=todoist_note.get("task_id"), + log__source_id=todoist_note.get("task_id"), ).first() if not scrobble: @@ -775,16 +775,14 @@ def todoist_scrobble_task( stopped: bool = False, user_context_list: list[str] = [], ) -> Scrobble: - title = get_title_from_labels( - todoist_task.get("todoist_label_list", []), user_context_list - ) + title = get_title_from_labels(todoist_task.get("labels", []), user_context_list) task = Task.find_or_create(title) timestamp = pendulum.parse(todoist_task.pop("updated_at", timezone.now())) in_progress_scrobble = Scrobble.objects.filter( user_id=user_id, in_progress=True, - log__todoist_id=todoist_task.get("todoist_id"), + log__source_id=todoist_task.get("source_id"), task=task, ).last() @@ -792,9 +790,9 @@ def todoist_scrobble_task( logger.info( "[todoist_scrobble_task] cannot stop already stopped task", extra={ - "todoist_type": todoist_task["todoist_type"], - "todoist_event": todoist_task["todoist_event"], - "todoist_id": todoist_task["todoist_id"], + "todoist_type": todoist_task["raw_data"]["todoist_type"], + "todoist_event": todoist_task["raw_data"]["todoist_event"], + "todoist_id": todoist_task["source_id"], }, ) return @@ -803,9 +801,9 @@ def todoist_scrobble_task( logger.info( "[todoist_scrobble_task] cannot start already started task", extra={ - "todoist_type": todoist_task["todoist_type"], - "todoist_event": todoist_task["todoist_event"], - "todoist_id": todoist_task["todoist_id"], + "todoist_type": todoist_task["raw_data"]["todoist_type"], + "todoist_event": todoist_task["raw_data"]["todoist_event"], + "todoist_id": todoist_task["source_id"], }, ) return in_progress_scrobble @@ -815,19 +813,15 @@ def todoist_scrobble_task( logger.info( "[todoist_scrobble_task] finishing", extra={ - "todoist_type": todoist_task["todoist_type"], - "todoist_event": todoist_task["todoist_event"], - "todoist_id": todoist_task["todoist_id"], + "todoist_type": todoist_task["raw_data"]["todoist_type"], + "todoist_event": todoist_task["raw_data"]["todoist_event"], + "todoist_id": todoist_task["source_id"], }, ) return todoist_scrobble_task_finish(todoist_task, user_id, timestamp) - todoist_task["title"] = todoist_task.pop("description") - todoist_task["description"] = todoist_task.pop("details") - labels = todoist_task.pop("todoist_label_list", []) + labels = todoist_task.pop("labels", []) todoist_task["labels"] = [l for l in labels if l.lower() != "inprogress"] - todoist_task.pop("todoist_type") - todoist_task.pop("todoist_event") scrobble_dict = { "user_id": user_id, @@ -925,7 +919,7 @@ def emacs_scrobble_update_task( scrobble = Scrobble.objects.filter( in_progress=True, user_id=user_id, - log__orgmode_id=emacs_id, + log__source_id=emacs_id, source="Org-mode", ).first() @@ -984,7 +978,7 @@ def emacs_scrobble_task( stopped: bool = False, user_context_list: list[str] = [], ) -> Scrobble | None: - orgmode_id = task_data.get("source_id") + source_id = task_data.get("source_id") title = get_title_from_labels(task_data.get("labels", []), user_context_list) task = Task.find_or_create(title) @@ -993,7 +987,7 @@ def emacs_scrobble_task( in_progress_scrobble = Scrobble.objects.filter( user_id=user_id, in_progress=True, - log__orgmode_id=orgmode_id, + log__source_id=source_id, task=task, ).last() @@ -1001,7 +995,7 @@ def emacs_scrobble_task( logger.info( "[emacs_scrobble_task] cannot stop already stopped task", extra={ - "orgmode_id": orgmode_id, + "source_id": source_id, }, ) return @@ -1010,7 +1004,7 @@ def emacs_scrobble_task( logger.info( "[emacs_scrobble_task] cannot start already started task", extra={ - "orgmode_id": orgmode_id, + "source_id": source_id, }, ) return in_progress_scrobble @@ -1020,7 +1014,7 @@ def emacs_scrobble_task( logger.info( "[emacs_scrobble_task] finishing", extra={ - "orgmode_id": orgmode_id, + "source_id": source_id, }, ) in_progress_scrobble.stop(timestamp=timestamp, force_finish=True) @@ -1036,12 +1030,13 @@ def emacs_scrobble_task( ) task_data["labels"] = task_data.pop("labels") - task_data["orgmode_id"] = task_data.pop("source_id") - task_data["orgmode_state"] = task_data.pop("state") - task_data["orgmode_properties"] = task_data.pop("properties") - task_data["orgmode_drawers"] = task_data.pop("drawers") - task_data["orgmode_timestamps"] = task_data.pop("timestamps") - task_data.pop("source") + task_data["state"] = task_data.pop("state") + task_data["raw_data"] = { + "properties": task_data.pop("properties"), + "drawers": task_data.pop("drawers"), + "timestamps": task_data.pop("timestamps"), + "source": task_data.pop("source"), + } scrobble_dict = { "user_id": user_id, diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 8ffd822..3277ba7 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -1414,10 +1414,11 @@ class ScrobbleDetailView(DetailView): if form.is_valid(): data = form.cleaned_data.copy() - for field_name, field in form.fields.items(): - if field.disabled: - original_value = (self.object.log or {}).get(field_name) - data[field_name] = original_value + # Preserve log keys not editable in the form (e.g. _excluded_fields + # like source ids, raw payloads, or importer-only fields). + for key, value in (self.object.log or {}).items(): + if key not in data: + data[key] = value if data.get("with_people_ids") is not None: data["with_people_ids"] = [p.id for p in data["with_people_ids"]] diff --git a/vrobbler/apps/tasks/models.py b/vrobbler/apps/tasks/models.py index a0027b7..699a78b 100644 --- a/vrobbler/apps/tasks/models.py +++ b/vrobbler/apps/tasks/models.py @@ -5,37 +5,29 @@ from typing import Optional from django.apps import apps from django.db import models from django.urls import reverse +from scrobbles.constants import TODOIST_TASK_URL from scrobbles.dataclasses import BaseLogData from scrobbles.mixins import LongPlayScrobblableMixin, ScrobblableConstants BNULL = {"blank": True, "null": True} -TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}" - @dataclass class TaskLogData(BaseLogData): title: Optional[str] = None labels: Optional[list[str]] = None - orgmode_id: Optional[str] = None - orgmode_state: Optional[str] = None - orgmode_properties: Optional[dict] = None - orgmode_drawers: Optional[list] = None - orgmode_timestamps: Optional[list] = None - - todoist_id: Optional[str] = None - todoist_project_id: Optional[str] = None + source_id: Optional[str] = None + project_id: Optional[str] = None + state: Optional[str] = None + raw_data: Optional[dict] = None _excluded_fields = { "labels", - "orgmode_id", - "orgmode_state", - "orgmode_properties", - "orgmode_drawers", - "orgmode_timestamps", - "todoist_id", - "todoist_project_id", + "source_id", + "project_id", + "state", + "raw_data", } @classmethod @@ -86,9 +78,25 @@ class TaskLogData(BaseLogData): md = markdown.Markdown(extensions=["extra"]) allowed_tags = [ - "p", "br", "strong", "em", "a", "ul", "ol", "li", - "code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6", - "hr", "img", + "p", + "br", + "strong", + "em", + "a", + "ul", + "ol", + "li", + "code", + "pre", + "blockquote", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "img", ] notes = self.notes @@ -160,8 +168,8 @@ class Task(LongPlayScrobblableMixin): url = "" scrobble = self.scrobbles(user_id).first() if scrobble: - if scrobble.log.get("source") == "todoist": - url = TODOIST_TASK_URL.format(id=scrobble.logdata.todist_id) + if scrobble.source == "Todoist" and scrobble.logdata.source_id: + url = TODOIST_TASK_URL.format(id=scrobble.logdata.source_id) return url def subtitle_for_user(self, user_id): diff --git a/vrobbler/apps/tasks/utils.py b/vrobbler/apps/tasks/utils.py index e4cb861..cdc5711 100644 --- a/vrobbler/apps/tasks/utils.py +++ b/vrobbler/apps/tasks/utils.py @@ -63,6 +63,48 @@ def convert_old_todoist_log_to_new(commit=False): print(f"Updated {scrobbles.count()} todoist tasks logs") +def convert_orgmode_task_log_to_generic(commit=False): + scrobbles = Scrobble.objects.filter(source="Org-mode", log__has_key="orgmode_id") + for scrobble in scrobbles: + scrobble.log["source_id"] = scrobble.log.pop("orgmode_id") + scrobble.log["state"] = scrobble.log.pop("orgmode_state") + + raw_data = {} + for key, payload_key in ( + ("orgmode_properties", "properties"), + ("orgmode_drawers", "drawers"), + ("orgmode_timestamps", "timestamps"), + ("orgmode_body", "body"), + ): + if key in scrobble.log: + raw_data[payload_key] = scrobble.log.pop(key) + scrobble.log["raw_data"] = raw_data + + print(f"Updating scrobble {scrobble.id}") + if commit: + scrobble.save(update_fields=["log"]) + print(f"Updated {scrobbles.count()} orgmode task logs to generic logdata") + + +def convert_todoist_task_log_to_generic(commit=False): + scrobbles = Scrobble.objects.filter(source="Todoist", log__has_key="todoist_id") + for scrobble in scrobbles: + scrobble.log["source_id"] = scrobble.log.pop("todoist_id") + if "todoist_project_id" in scrobble.log: + scrobble.log["project_id"] = scrobble.log.pop("todoist_project_id") + + raw_data = {} + for key in ("todoist_type", "todoist_event"): + if key in scrobble.log: + raw_data[key] = scrobble.log.pop(key) + scrobble.log["raw_data"] = raw_data + + print(f"Updating scrobble {scrobble.id}") + if commit: + scrobble.save(update_fields=["log"]) + print(f"Updated {scrobbles.count()} todoist task logs to generic logdata") + + def convert_notes_to_dict(commit=False): from scrobbles.utils import make_note_timestamp @@ -129,9 +171,9 @@ def convert_old_boardgame_log_to_new(commit=False): notes = [notes] if isinstance(notes, list): key_ts = scrobble.stop_timestamp or scrobble.timestamp - scrobble.log["notes"] = {make_note_timestamp(key_ts): "\n".join( - str(n) for n in notes - )} + scrobble.log["notes"] = { + make_note_timestamp(key_ts): "\n".join(str(n) for n in notes) + } count += 1 if commit: scrobble.save(update_fields=["log"]) diff --git a/vrobbler/apps/tasks/webhooks.py b/vrobbler/apps/tasks/webhooks.py index 98d5742..c7bedef 100644 --- a/vrobbler/apps/tasks/webhooks.py +++ b/vrobbler/apps/tasks/webhooks.py @@ -2,11 +2,14 @@ import json import logging from django.utils import timezone -from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from profiles.models import UserProfile from rest_framework import status -from rest_framework.authentication import BaseAuthentication, TokenAuthentication +from rest_framework.authentication import ( + BaseAuthentication, + TokenAuthentication, +) from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -118,30 +121,35 @@ class TodoistWebhookView(APIView): if is_item_type and is_updated and (task_started or task_stopped): todoist_task = { - "todoist_id": event_data.get("id"), - "todoist_label_list": event_data.get("labels"), - "todoist_type": todoist_type, - "todoist_event": todoist_event, + "source_id": event_data.get("id"), + "labels": event_data.get("labels"), + "project_id": event_data.get("project_id"), + "title": event_data.get("content"), + "description": event_data.get("description"), "updated_at": event_data.get("updated_at"), - "todoist_project_id": event_data.get("project_id"), - "description": event_data.get("content"), - "details": event_data.get("description"), + "raw_data": { + "todoist_type": todoist_type, + "todoist_event": todoist_event, + }, } if is_note_type and is_added: task_data = event_data.get("item", {}) todoist_note = { "task_id": event_data.get("item_id"), - "todoist_id": event_data.get("id"), - "todoist_label_list": task_data.get("labels"), - "todoist_type": todoist_type, - "todoist_event": todoist_event, + "source_id": event_data.get("id"), + "labels": task_data.get("labels"), + "title": task_data.get("content"), + "description": task_data.get("description"), "updated_at": task_data.get("updated_at"), "posted_at": event_data.get("posted_at"), - "details": task_data.get("description"), "notes": event_data.get("content"), "is_deleted": ( True if event_data.get("is_deleted") == "true" else False ), + "raw_data": { + "todoist_type": todoist_type, + "todoist_event": todoist_event, + }, } if (is_added and not todoist_note) or (is_updated and not todoist_task): diff --git a/vrobbler/templates/scrobbles/scrobble_detail.html b/vrobbler/templates/scrobbles/scrobble_detail.html index 393c7fb..7a982ea 100644 --- a/vrobbler/templates/scrobbles/scrobble_detail.html +++ b/vrobbler/templates/scrobbles/scrobble_detail.html @@ -77,6 +77,13 @@

{{ object.logdata.title }}

{% endif %} +{% if object.media_type == "Task" and object.logdata.labels %} +

+{% for label in object.logdata.labels %} +{{ label }} +{% endfor %} +

+{% endif %} {% if object.media_type == "AgentSession" and object.logdata.title %}

{{ object.logdata.title }}

@@ -300,7 +307,7 @@ -
+
{% csrf_token %} {% for field in log_form %}