import json from dataclasses import dataclass from typing import Optional from django.apps import apps from django.db import models from django.urls import reverse 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 _excluded_fields = { "labels", "orgmode_id", "orgmode_state", "orgmode_properties", "orgmode_drawers", "orgmode_timestamps", "todoist_id", "todoist_project_id", } @classmethod def override_fields(cls) -> dict: from scrobbles.forms import NotesDictField fields = {} for base in cls.mro()[1:]: if hasattr(base, "override_fields"): base_fields = base.override_fields() fields.update(base_fields) fields["notes"] = NotesDictField(required=False) return fields def notes_as_str(self, separator: str = " | ") -> str: """Return formatted notes with line breaks and no keys""" labels_str = "" if self.labels: labels_str = ", ".join(self.labels) lines = [] if self.notes: notes = self.notes if isinstance(notes, dict): notes = [{k: v} for k, v in notes.items()] if isinstance(notes, str): notes = [notes] for note in notes: if isinstance(note, dict): timestamp, note_text = next(iter(note.items())) note_text = " ".join(note_text.strip().split()) lines.append(f"{timestamp}: {note_text}") if isinstance(note, list): lines += note if isinstance(note, str): lines.append(note) return separator.join(lines).encode("utf-8").decode("unicode_escape") def notes_as_html(self) -> str: import bleach import markdown from django.utils.safestring import mark_safe from scrobbles.dataclasses import BaseLogData if not self.notes: return "" 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", ] notes = self.notes if isinstance(notes, dict): notes = [{k: v} for k, v in notes.items()] if isinstance(notes, str): notes = [notes] note_items = [] for note in notes: if isinstance(note, dict): timestamp, note_text = next(iter(note.items())) note_items.append((timestamp, note_text.strip())) elif isinstance(note, str): escaped = note.encode("utf-8").decode("unicode_escape") for line in escaped.split("\n"): if line.strip(): note_items.append((None, line.strip())) elif isinstance(note, list): for item in note: if isinstance(item, str): note_items.append((None, item.strip())) html_parts = [] for i, (ts, text) in enumerate(note_items): if i > 0: html_parts.append('
') ts_html = "" if ts: ts_html = f'
{BaseLogData._format_timestamp(ts)}
' content_html = bleach.clean( md.convert(text), tags=allowed_tags, strip=True, ) html_parts.append( f'
{ts_html}
{content_html}
' ) return mark_safe("".join(html_parts)) class Task(LongPlayScrobblableMixin): """Basically a holder for Todoist Tasks and any other otherwise generic tasks. """ description = models.TextField(**BNULL) def __str__(self): return self.title def get_absolute_url(self): return reverse("tasks:task_detail", kwargs={"slug": self.uuid}) @property def strings(self) -> ScrobblableConstants: return ScrobblableConstants(verb="Doing", tags="memo") @property def logdata_cls(self): return TaskLogData def source_url_for_user(self, user_id) -> str: 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) return url def subtitle_for_user(self, user_id): scrobble = self.scrobbles(user_id).first() return scrobble.logdata.title or scrobble.log.get("title") @classmethod def find_or_create(cls, title: str) -> "Task": task, created = cls.objects.get_or_create(title=title) if created: task.base_run_time_seconds = 1800 task.save(update_fields=["base_run_time_seconds"]) return task def scrobbles(self, user_id): Scrobble = apps.get_model("scrobbles", "Scrobble") return Scrobble.objects.filter(user_id=user_id, task=self).order_by( "-timestamp" )