Files
vrobbler/vrobbler/apps/scrobbles/dataclasses.py
Colin Powell 009b2ba243
Some checks failed
build & deploy / build-and-deploy (push) Has been cancelled
build & deploy / test (push) Has been cancelled
[scrobbles] Big update for notes in tasks and boardgames
2026-05-31 23:17:16 -04:00

160 lines
4.8 KiB
Python

import json
from dataclasses import asdict, dataclass
from typing import Optional
from dataclass_wizard import JSONWizard
from django import forms
from django.contrib.auth import get_user_model
from django.db.models import Count
from people.models import Person
from scrobbles.forms import form_from_dataclass
User = get_user_model()
class ScrobbleLogDataEncoder(json.JSONEncoder):
def default(self, o):
try:
return o.__dict__
except AttributeError:
return {}
class ScrobbleLogDataDecoder(json.JSONDecoder):
def default(self, o):
return o.__dict__
class JSONDataclass(JSONWizard):
@property
def asdict(self):
return asdict(self)
@property
def json(self):
return json.dumps(self.asdict)
@dataclass
class BaseLogData(JSONDataclass):
description: Optional[str] = None
notes: Optional[list[str]] = None
_excluded_fields = {}
@classmethod
def form(cls):
return form_from_dataclass(cls)
@classmethod
def override_fields(cls) -> dict:
return {}
def notes_as_str(self, separator: str = " | ") -> str:
import html
import re
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:
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 ""
def notes_as_html(self) -> str:
import html
from django.utils.safestring import mark_safe
if not self.notes:
return ""
html_notes = []
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))
@dataclass
class LongPlayLogData(JSONDataclass):
long_play_complete: bool = False
@dataclass
class WithPeopleLogData(JSONDataclass):
with_people_ids: Optional[list[int]] = None
@property
def with_people(self) -> list["Person"]:
from people.models import Person
if not self.with_people_ids:
return []
return [Person.objects.filter(id=pid).first() for pid in self.with_people_ids]
@classmethod
def override_fields(cls) -> dict:
fields = {}
for base in cls.mro()[1:]:
if hasattr(base, "override_fields"):
base_fields = base.override_fields()
fields.update(base_fields)
custom_fields = {
"with_people_ids": forms.ModelMultipleChoiceField(
queryset=Person.objects.annotate(
scrobble_count=Count("scrobble_associations")
).order_by("-scrobble_count"),
required=False,
widget=forms.SelectMultiple(attrs={"size": 10}),
)
}
fields.update(custom_fields)
return fields