[tasks] Fix timestamps for notes and syncing from org-mode (32973bb3)
This commit is contained in:
17
PROJECT.org
17
PROJECT.org
@ -93,7 +93,7 @@ fetching and simple saving.
|
|||||||
:LOGBOOK:
|
:LOGBOOK:
|
||||||
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||||
:END:
|
:END:
|
||||||
* Backlog [0/15] :vrobbler:project:personal:
|
* Backlog [1/16] :vrobbler:project:personal:
|
||||||
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
||||||
@ -480,6 +480,21 @@ whatever time KoReader reports, we need to know, given the date and the user
|
|||||||
profile's historic timezone, how many hours to adjust the KoReader time to get
|
profile's historic timezone, how many hours to adjust the KoReader time to get
|
||||||
to GMT to save it in the database.
|
to GMT to save it in the database.
|
||||||
|
|
||||||
|
** DONE [#B] Fix the way timestamps are stored for notes on tasks :bug:scrobbles:tasks:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 32973bb3-079b-8cdf-6495-82f8ae907299
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: d833a3df-6eb2-36bb-1863-e438e5d36151
|
||||||
|
:END:
|
||||||
|
|
||||||
|
Turns out Todoist uses a human-readable timestamp format for comments. We should
|
||||||
|
adapt that for use from org-mode as well.
|
||||||
|
|
||||||
|
Format should be: "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
|
||||||
* Version 39.1 [1/1]
|
* Version 39.1 [1/1]
|
||||||
** DONE [#A] Fix bug in tests for notes saving :bug:scrobbles:forms:
|
** DONE [#A] Fix bug in tests for notes saving :bug:scrobbles:forms:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -145,8 +145,8 @@ class NotesDictField(forms.Field):
|
|||||||
|
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
if value.strip():
|
if value.strip():
|
||||||
from datetime import datetime
|
from scrobbles.utils import make_note_timestamp
|
||||||
return {datetime.now().isoformat(): value.strip()}
|
return {make_note_timestamp(): value.strip()}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from scrobbles.notifications import ScrobbleNtfyNotification
|
|||||||
from scrobbles.utils import (
|
from scrobbles.utils import (
|
||||||
convert_to_seconds,
|
convert_to_seconds,
|
||||||
extract_domain,
|
extract_domain,
|
||||||
|
make_note_timestamp,
|
||||||
next_url_if_exists,
|
next_url_if_exists,
|
||||||
remove_last_part,
|
remove_last_part,
|
||||||
)
|
)
|
||||||
@ -528,7 +529,7 @@ def email_scrobble_board_game(
|
|||||||
stop_timestamp = timestamp + timedelta(seconds=duration_seconds)
|
stop_timestamp = timestamp + timedelta(seconds=duration_seconds)
|
||||||
|
|
||||||
if comments:
|
if comments:
|
||||||
log_data["notes"] = {str(int(stop_timestamp.timestamp())): comments}
|
log_data["notes"] = {make_note_timestamp(stop_timestamp): comments}
|
||||||
|
|
||||||
logger.info(f"Creating scrobble for {base_game} at {timestamp}")
|
logger.info(f"Creating scrobble for {base_game} at {timestamp}")
|
||||||
log_data["raw_data"] = bgstat_data
|
log_data["raw_data"] = bgstat_data
|
||||||
@ -688,7 +689,7 @@ def todoist_scrobble_update_task(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
timestamp = todoist_note.get("posted_at") or str(int(timezone.now().timestamp()))
|
timestamp = todoist_note.get("posted_at") or make_note_timestamp()
|
||||||
if not scrobble.log.get("notes"):
|
if not scrobble.log.get("notes"):
|
||||||
scrobble.log["notes"] = {}
|
scrobble.log["notes"] = {}
|
||||||
scrobble.log["notes"][timestamp] = todoist_note.get("notes")
|
scrobble.log["notes"][timestamp] = todoist_note.get("notes")
|
||||||
@ -786,12 +787,37 @@ def todoist_scrobble_task(
|
|||||||
return scrobble
|
return scrobble
|
||||||
|
|
||||||
|
|
||||||
|
ORG_HEADING_RE = re.compile(r"^(\*+\s+.*)$", re.MULTILINE)
|
||||||
|
NOTE_CONTENT_SPLIT = re.compile(r"^\*{3,}\s", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate_at_org_header(text: str) -> str:
|
||||||
|
"""Truncate text at the first org-mode heading (*** or more)."""
|
||||||
|
parts = NOTE_CONTENT_SPLIT.split(text, maxsplit=1)
|
||||||
|
return parts[0].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_org_section(body: str | None, heading: str) -> str | None:
|
||||||
|
"""Extract content under a specific org-mode sub-heading (e.g. '*** Description')."""
|
||||||
|
if not body:
|
||||||
|
return None
|
||||||
|
sections = ORG_HEADING_RE.split(body)
|
||||||
|
# sections alternates: [prefix, heading1, content1, heading2, content2, ...]
|
||||||
|
for i, section in enumerate(sections):
|
||||||
|
if section.strip().startswith(heading):
|
||||||
|
if i + 1 < len(sections):
|
||||||
|
return sections[i + 1].strip()
|
||||||
|
return ""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def emacs_scrobble_update_task(
|
def emacs_scrobble_update_task(
|
||||||
emacs_id: str,
|
emacs_id: str,
|
||||||
emacs_notes: list,
|
emacs_notes: list,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
) -> Optional[Scrobble]:
|
) -> Optional[Scrobble]:
|
||||||
|
description = _extract_org_section(description, "*** Description")
|
||||||
scrobble = Scrobble.objects.filter(
|
scrobble = Scrobble.objects.filter(
|
||||||
in_progress=True,
|
in_progress=True,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@ -818,6 +844,11 @@ def emacs_scrobble_update_task(
|
|||||||
for note in emacs_notes:
|
for note in emacs_notes:
|
||||||
timestamp = note.get("timestamp")
|
timestamp = note.get("timestamp")
|
||||||
content = note.get("content")
|
content = note.get("content")
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
content = _truncate_at_org_header(content)
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
if timestamp:
|
if timestamp:
|
||||||
existing = scrobble.log["notes"].get(timestamp)
|
existing = scrobble.log["notes"].get(timestamp)
|
||||||
if existing != content:
|
if existing != content:
|
||||||
@ -896,7 +927,7 @@ def emacs_scrobble_task(
|
|||||||
|
|
||||||
task_data.pop("notes", None)
|
task_data.pop("notes", None)
|
||||||
task_data["title"] = task_data.pop("description")
|
task_data["title"] = task_data.pop("description")
|
||||||
task_data["description"] = task_data.pop("body")
|
task_data["description"] = _extract_org_section(task_data.pop("body"), "*** Description")
|
||||||
task_data["labels"] = task_data.pop("labels")
|
task_data["labels"] = task_data.pop("labels")
|
||||||
|
|
||||||
task_data["orgmode_id"] = task_data.pop("source_id")
|
task_data["orgmode_id"] = task_data.pop("source_id")
|
||||||
|
|||||||
@ -32,6 +32,16 @@ from webdav.client import get_webdav_client
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
|
||||||
|
NOTE_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
|
||||||
|
|
||||||
|
def make_note_timestamp(dt: datetime | None = None) -> str:
|
||||||
|
if dt is None:
|
||||||
|
dt = timezone.now()
|
||||||
|
return dt.strftime(NOTE_TIMESTAMP_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,8 @@ def convert_old_todoist_log_to_new(commit=False):
|
|||||||
|
|
||||||
|
|
||||||
def convert_notes_to_dict(commit=False):
|
def convert_notes_to_dict(commit=False):
|
||||||
|
from scrobbles.utils import make_note_timestamp
|
||||||
|
|
||||||
scrobbles = Scrobble.objects.filter(log__notes__isnull=False)
|
scrobbles = Scrobble.objects.filter(log__notes__isnull=False)
|
||||||
count = 0
|
count = 0
|
||||||
for scrobble in scrobbles:
|
for scrobble in scrobbles:
|
||||||
@ -71,7 +73,7 @@ def convert_notes_to_dict(commit=False):
|
|||||||
print(f"Converting {scrobble} string note to dict")
|
print(f"Converting {scrobble} string note to dict")
|
||||||
if scrobble.log.get("notes") == "":
|
if scrobble.log.get("notes") == "":
|
||||||
scrobble.log.pop("notes")
|
scrobble.log.pop("notes")
|
||||||
key = str(int(scrobble.timestamp.timestamp()))
|
key = make_note_timestamp(scrobble.timestamp)
|
||||||
notes = scrobble.log.pop("notes")
|
notes = scrobble.log.pop("notes")
|
||||||
scrobble.log = {}
|
scrobble.log = {}
|
||||||
scrobble.log["notes"] = {key: notes}
|
scrobble.log["notes"] = {key: notes}
|
||||||
@ -92,12 +94,14 @@ def convert_notes_to_dict(commit=False):
|
|||||||
|
|
||||||
|
|
||||||
def convert_tasks_notes_list_to_dict(commit=False):
|
def convert_tasks_notes_list_to_dict(commit=False):
|
||||||
|
from scrobbles.utils import make_note_timestamp
|
||||||
|
|
||||||
scrobbles = Scrobble.objects.filter(task__isnull=False, log__notes__isnull=False)
|
scrobbles = Scrobble.objects.filter(task__isnull=False, log__notes__isnull=False)
|
||||||
count = 0
|
count = 0
|
||||||
for scrobble in scrobbles:
|
for scrobble in scrobbles:
|
||||||
notes = scrobble.log.get("notes")
|
notes = scrobble.log.get("notes")
|
||||||
if isinstance(notes, list):
|
if isinstance(notes, list):
|
||||||
key = str(int(scrobble.timestamp.timestamp()) + 10)
|
key = make_note_timestamp(scrobble.timestamp + timedelta(seconds=10))
|
||||||
parts = []
|
parts = []
|
||||||
for note in notes:
|
for note in notes:
|
||||||
if isinstance(note, dict):
|
if isinstance(note, dict):
|
||||||
@ -114,6 +118,8 @@ def convert_tasks_notes_list_to_dict(commit=False):
|
|||||||
|
|
||||||
|
|
||||||
def convert_old_boardgame_log_to_new(commit=False):
|
def convert_old_boardgame_log_to_new(commit=False):
|
||||||
|
from scrobbles.utils import make_note_timestamp
|
||||||
|
|
||||||
scrobbles = Scrobble.objects.filter(board_game__isnull=False, log__has_key="notes")
|
scrobbles = Scrobble.objects.filter(board_game__isnull=False, log__has_key="notes")
|
||||||
count = 0
|
count = 0
|
||||||
for scrobble in scrobbles:
|
for scrobble in scrobbles:
|
||||||
@ -123,7 +129,7 @@ def convert_old_boardgame_log_to_new(commit=False):
|
|||||||
notes = [notes]
|
notes = [notes]
|
||||||
if isinstance(notes, list):
|
if isinstance(notes, list):
|
||||||
key_ts = scrobble.stop_timestamp or scrobble.timestamp
|
key_ts = scrobble.stop_timestamp or scrobble.timestamp
|
||||||
scrobble.log["notes"] = {str(int(key_ts.timestamp())): "\n".join(
|
scrobble.log["notes"] = {make_note_timestamp(key_ts): "\n".join(
|
||||||
str(n) for n in notes
|
str(n) for n in notes
|
||||||
)}
|
)}
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user