[scrobbles] Big update for notes in tasks and boardgames
Some checks failed
build & deploy / build-and-deploy (push) Has been cancelled
build & deploy / test (push) Has been cancelled

This commit is contained in:
2026-05-31 23:17:16 -04:00
parent 4f051ae250
commit 009b2ba243
13 changed files with 341 additions and 70 deletions

View File

@ -57,14 +57,28 @@ class BaseLogData(JSONDataclass):
if not self.notes:
return ""
if isinstance(self.notes, dict):
lines = []
for ts, text in self.notes.items():
note_text = " ".join(text.strip().split())
lines.append(f"{ts}: {note_text}")
return separator.join(lines)
if isinstance(self.notes, str):
return html.escape(re.sub(r"\s*\[start:\d+\|end:\d+\]$", "", self.notes))
if isinstance(self.notes, list):
cleaned_notes = []
for note in self.notes:
cleaned = html.escape(re.sub(r"\s*\[start:\d+\|end:\d+\]$", "", note))
cleaned_notes.append(cleaned)
if isinstance(note, dict):
timestamp, note_text = next(iter(note.items()))
note_text = " ".join(note_text.strip().split())
cleaned_notes.append(f"{timestamp}: {note_text}")
elif isinstance(note, str):
cleaned = html.escape(
re.sub(r"\s*\[start:\d+\|end:\d+\]$", "", note)
)
cleaned_notes.append(cleaned)
return separator.join(cleaned_notes)
return ""
@ -76,18 +90,35 @@ class BaseLogData(JSONDataclass):
if not self.notes:
return ""
notes_list = []
if isinstance(self.notes, str):
notes_list = [self.notes]
elif isinstance(self.notes, list):
notes_list = self.notes
html_notes = []
for note in notes_list:
for line in note.split("\n"):
if isinstance(self.notes, dict):
for ts, text in self.notes.items():
note_text = " ".join(text.strip().split())
html_notes.append(
f'<div class="sticky-note">{ts}: {note_text}</div>'
)
elif isinstance(self.notes, str):
for line in self.notes.split("\n"):
if line.strip():
escaped_line = html.escape(line)
html_notes.append(f'<div class="sticky-note">{escaped_line}</div>')
elif isinstance(self.notes, list):
for note in self.notes:
if isinstance(note, dict):
timestamp, note_text = next(iter(note.items()))
note_text = " ".join(note_text.strip().split())
html_notes.append(
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
)
elif isinstance(note, str):
for line in note.split("\n"):
if line.strip():
escaped_line = html.escape(line)
html_notes.append(
f'<div class="sticky-note">{escaped_line}</div>'
)
return mark_safe("".join(html_notes))

View File

@ -1,3 +1,4 @@
import json
from dataclasses import fields
from typing import Union, get_args, get_origin
@ -89,7 +90,7 @@ def form_from_dataclass(dataclass):
form_cls = type(f"{dataclass.__name__}Form", (forms.Form,), form_fields)
if "notes" in form_cls.base_fields:
if "notes" in form_cls.base_fields and "notes" not in override_fields:
form_cls.base_fields["notes"] = forms.CharField(
required=False,
widget=forms.Textarea(attrs={"rows": 4}),
@ -101,3 +102,47 @@ def form_from_dataclass(dataclass):
form_cls.clean_notes = clean_notes
return form_cls
class NotesDictWidget(forms.Widget):
template_name = "tasks/task_notes_widget.html"
class Media:
js = ("tasks/task_notes.js",)
def value_from_datadict(self, data, files, name):
timestamps = data.getlist(f"{name}_timestamps")
contents = data.getlist(f"{name}_contents")
return {
"timestamps": timestamps,
"contents": contents,
}
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
notes = {}
if value:
if isinstance(value, str):
try:
notes = json.loads(value)
except (json.JSONDecodeError, TypeError):
notes = {}
elif isinstance(value, dict):
notes = value
context["widget"]["notes"] = notes
return context
class NotesDictField(forms.Field):
widget = NotesDictWidget
def clean(self, value):
if not value:
return {}
result = {}
timestamps = value.get("timestamps", [])
contents = value.get("contents", [])
for i, ts in enumerate(timestamps):
if i < len(contents) and ts and contents[i].strip():
result[ts] = contents[i].strip()
return result if result else {}

View File

@ -1,9 +1,7 @@
from django.core.management.base import BaseCommand
from vrobbler.apps.tasks.utils import (
convert_notes_to_dict,
convert_old_boardgame_log_to_new,
convert_old_orgmode_log_to_new,
convert_old_todoist_log_to_new,
convert_tasks_notes_list_to_dict,
)
@ -21,7 +19,5 @@ class Command(BaseCommand):
commit = True
else:
print("No changes will be saved, use --commit to save")
convert_old_orgmode_log_to_new(commit)
convert_old_todoist_log_to_new(commit)
convert_notes_to_dict(commit)
convert_tasks_notes_list_to_dict(commit)
convert_old_boardgame_log_to_new(commit)

View File

@ -450,11 +450,11 @@ def email_scrobble_board_game(
locations[location_dict.get("id")] = location
scrobbles_created = []
second = 0
for play_dict in bgstat_data.get("plays", []):
hour = None
minute = None
second = None
comments = None
if "comments" in play_dict.keys():
for line in play_dict.get("comments", "").split("\n"):
if "Learning to play" in line:
@ -469,7 +469,7 @@ def email_scrobble_board_game(
except IndexError:
second = 0
log_data["notes"] = [play_dict.get("comments")]
comments = play_dict.get("comments")
log_data["expansion_ids"] = []
try:
base_game = base_games[play_dict.get("gameRefId")]
@ -527,6 +527,9 @@ def email_scrobble_board_game(
duration_seconds = base_game.run_time_seconds
stop_timestamp = timestamp + timedelta(seconds=duration_seconds)
if comments:
log_data["notes"] = {str(int(stop_timestamp.timestamp())): comments}
logger.info(f"Creating scrobble for {base_game} at {timestamp}")
log_data["raw_data"] = bgstat_data
scrobble_dict = {
@ -685,9 +688,10 @@ def todoist_scrobble_update_task(
)
return
timestamp = todoist_note.get("posted_at") or str(int(timezone.now().timestamp()))
if not scrobble.log.get("notes"):
scrobble.log["notes"] = []
scrobble.log["notes"].append(todoist_note.get("notes"))
scrobble.log["notes"] = {}
scrobble.log["notes"][timestamp] = todoist_note.get("notes")
scrobble.save(update_fields=["log"])
logger.info(
"[todoist_scrobble_update_task] todoist note added",
@ -783,7 +787,10 @@ def todoist_scrobble_task(
def emacs_scrobble_update_task(
emacs_id: str, emacs_notes: dict, user_id: int
emacs_id: str,
emacs_notes: list,
user_id: int,
description: Optional[str] = None,
) -> Optional[Scrobble]:
scrobble = Scrobble.objects.filter(
in_progress=True,
@ -803,27 +810,30 @@ def emacs_scrobble_update_task(
)
return
notes_updated = False
log_updated = False
if not scrobble.log.get("notes"):
scrobble.log["notes"] = {}
for note in emacs_notes:
try:
existing_note_ts = [
n.get("timestamp") for n in scrobble.log.get("notes", [])
]
except AttributeError:
existing_note_ts = []
if not scrobble.log.get('notes"'):
scrobble.log["notes"] = []
if note.get("timestamp") not in existing_note_ts:
scrobble.log["notes"].append({note.get("timestamp"): note.get("content")})
notes_updated = True
timestamp = note.get("timestamp")
content = note.get("content")
if timestamp:
existing = scrobble.log["notes"].get(timestamp)
if existing != content:
scrobble.log["notes"][timestamp] = content
log_updated = True
if notes_updated:
if description is not None and scrobble.log.get("description") != description:
scrobble.log["description"] = description
log_updated = True
if log_updated:
scrobble.save(update_fields=["log"])
logger.info(
"[emacs_scrobble_update_task] emacs note added",
"[emacs_scrobble_update_task] emacs scrobble updated",
extra={
"emacs_note": emacs_notes,
"emacs_id": emacs_id,
"user_id": user_id,
"media_type": Scrobble.MediaType.TASK,
},
@ -865,7 +875,7 @@ def emacs_scrobble_task(
logger.info(
"[emacs_scrobble_task] cannot start already started task",
extra={
"ormode_id": orgmode_id,
"orgmode_id": orgmode_id,
},
)
return in_progress_scrobble
@ -884,9 +894,7 @@ def emacs_scrobble_task(
if in_progress_scrobble:
return in_progress_scrobble
notes = task_data.pop("notes")
if notes:
task_data["notes"] = [note.get("content") for note in notes]
task_data.pop("notes", None)
task_data["title"] = task_data.pop("description")
task_data["description"] = task_data.pop("body")
task_data["labels"] = task_data.pop("labels")

View File

@ -1024,7 +1024,11 @@ class ScrobbleDetailView(DetailView):
FormClass = self.get_form_class()
log = self.object.log or {}
log["notes"] = self.object.logdata.notes_as_str(separator="\n")
notes = log.get("notes")
if isinstance(notes, dict):
log["notes"] = notes
else:
log["notes"] = self.object.logdata.notes_as_str(separator="\n")
return FormClass(initial=log)