185 lines
5.7 KiB
Python
185 lines
5.7 KiB
Python
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('<hr class="note-divider">')
|
|
|
|
ts_html = ""
|
|
if ts:
|
|
ts_html = f'<h5 class="note-timestamp">{BaseLogData._format_timestamp(ts)}</h5>'
|
|
|
|
content_html = bleach.clean(
|
|
md.convert(text),
|
|
tags=allowed_tags,
|
|
strip=True,
|
|
)
|
|
|
|
html_parts.append(
|
|
f'<div class="note-item">{ts_html}<div class="note-content">{content_html}</div></div>'
|
|
)
|
|
|
|
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"
|
|
)
|