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 @@
-