From 86f4c66b53ba21cf51bdf18616da555f9a265389 Mon Sep 17 00:00:00 2001 From: Kasper Hartwich Date: Sat, 26 Sep 2026 05:08:39 +0000 Subject: [PATCH 1/2] feat(messages): expose sender name and send time on Message The API returns sender.fullName and sendDateTime for every message, but Message dropped them, so the JSON output of `aula messages` could not say who wrote a message or when. Both are optional and default to None. --- src/aula/models/message.py | 4 ++++ tests/models/test_message.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/aula/models/message.py b/src/aula/models/message.py index 71b13d9..c5d9b19 100644 --- a/src/aula/models/message.py +++ b/src/aula/models/message.py @@ -12,6 +12,8 @@ class Message(AulaDataClass): id: str content_html: str attachments: list[Attachment] = field(default_factory=list) + sender_name: str | None = None + send_datetime: str | None = None _raw: dict | None = field(default=None, repr=False) @property @@ -40,5 +42,7 @@ def from_dict(cls, data: dict[str, Any]) -> Message: id=str(message_id) if message_id is not None else "", content_html=text if isinstance(text, str) else "", attachments=parse_attachments(data.get("attachments")), + sender_name=get_in(data, "sender.fullName", default=None) or None, + send_datetime=data.get("sendDateTime") or None, _raw=data, ) diff --git a/tests/models/test_message.py b/tests/models/test_message.py index fee5a04..11e81ec 100644 --- a/tests/models/test_message.py +++ b/tests/models/test_message.py @@ -79,3 +79,24 @@ def test_message_from_dict_without_attachments(): msg = Message.from_dict({"id": "m3", "text": {"html": ""}}) assert msg.attachments == [] assert msg.has_attachments is False + + +def test_message_from_dict_sender_and_send_time(): + data = { + "id": "m2", + "text": {"html": "

Hej

"}, + "sender": {"fullName": "Anne Lærer", "shortName": "AL"}, + "sendDateTime": "2026-09-25T08:15:00+02:00", + } + msg = Message.from_dict(data) + assert msg.sender_name == "Anne Lærer" + assert msg.send_datetime == "2026-09-25T08:15:00+02:00" + result = dict(msg) + assert result["sender_name"] == "Anne Lærer" + assert result["send_datetime"] == "2026-09-25T08:15:00+02:00" + + +def test_message_from_dict_without_sender(): + msg = Message.from_dict({"id": "m3", "text": "plain"}) + assert msg.sender_name is None + assert msg.send_datetime is None From 56fedfb8ba4eb6b6ffa660842df7bd455df7f59f Mon Sep 17 00:00:00 2001 From: Kasper Hartwich Date: Sat, 26 Sep 2026 05:15:45 +0000 Subject: [PATCH 2/2] fix(messages): keep Message's positional fields, parse send time Review follow-up: sender_name and send_datetime are keyword-only, so Message(id, html, attachments, raw) keeps meaning what it did. send_datetime goes through parse_api_datetime like Post's timestamps, giving None for a missing or unreadable value. Tests cover empty and odd senders, unreadable timestamps and the positional order. --- src/aula/models/message.py | 10 +++++++--- tests/models/test_message.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/aula/models/message.py b/src/aula/models/message.py index c5d9b19..01e70e8 100644 --- a/src/aula/models/message.py +++ b/src/aula/models/message.py @@ -1,6 +1,8 @@ +import datetime from dataclasses import dataclass, field from typing import Any +from ..utils.dates import parse_api_datetime from ..utils.html import html_to_markdown, html_to_plain from ..utils.mapping import get_in from .attachment import Attachment, parse_attachments @@ -12,9 +14,11 @@ class Message(AulaDataClass): id: str content_html: str attachments: list[Attachment] = field(default_factory=list) - sender_name: str | None = None - send_datetime: str | None = None _raw: dict | None = field(default=None, repr=False) + # Keyword-only, so the positional order above (and ``_raw`` in it) stays + # what it was before these were added. + sender_name: str | None = field(default=None, kw_only=True) + send_datetime: datetime.datetime | None = field(default=None, kw_only=True) @property def content(self) -> str: @@ -43,6 +47,6 @@ def from_dict(cls, data: dict[str, Any]) -> Message: content_html=text if isinstance(text, str) else "", attachments=parse_attachments(data.get("attachments")), sender_name=get_in(data, "sender.fullName", default=None) or None, - send_datetime=data.get("sendDateTime") or None, + send_datetime=parse_api_datetime(data.get("sendDateTime")), _raw=data, ) diff --git a/tests/models/test_message.py b/tests/models/test_message.py index 11e81ec..27aa36e 100644 --- a/tests/models/test_message.py +++ b/tests/models/test_message.py @@ -1,5 +1,9 @@ """Tests for aula.models.message.""" +import datetime + +import pytest + from aula.models.message import Message @@ -90,13 +94,36 @@ def test_message_from_dict_sender_and_send_time(): } msg = Message.from_dict(data) assert msg.sender_name == "Anne Lærer" - assert msg.send_datetime == "2026-09-25T08:15:00+02:00" + assert msg.send_datetime == datetime.datetime.fromisoformat("2026-09-25T08:15:00+02:00") result = dict(msg) assert result["sender_name"] == "Anne Lærer" - assert result["send_datetime"] == "2026-09-25T08:15:00+02:00" + assert "send_datetime" in result def test_message_from_dict_without_sender(): msg = Message.from_dict({"id": "m3", "text": "plain"}) assert msg.sender_name is None assert msg.send_datetime is None + + +@pytest.mark.parametrize( + "sender", + [None, {}, {"fullName": None}, {"fullName": ""}, "not a dict"], +) +def test_message_from_dict_sender_missing_or_empty(sender): + msg = Message.from_dict({"id": "m4", "text": "x", "sender": sender}) + assert msg.sender_name is None + + +@pytest.mark.parametrize("value", [None, "", "not a date", 12345]) +def test_message_from_dict_send_datetime_unreadable(value): + msg = Message.from_dict({"id": "m5", "text": "x", "sendDateTime": value}) + assert msg.send_datetime is None + + +def test_message_positional_arguments_unchanged(): + """``_raw`` is still the fourth positional field: the new ones are keyword-only.""" + msg = Message("1", "text", [], {"original": True}) + assert msg._raw == {"original": True} + assert msg.sender_name is None + assert "_raw" not in dict(msg)