Skip to content

Commit 241f94a

Browse files
committed
fix: add_organizer() accepts argument and updates existing field
1 parent 9e7ccb5 commit 241f94a

File tree

2 files changed

+112
-9
lines changed

2 files changed

+112
-9
lines changed

caldav/calendarobjectresource.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,42 @@ def set_end(self, end, move_dtstart=False):
168168

169169
i.add(self._ENDPARAM, end)
170170

171-
def add_organizer(self) -> None:
171+
def add_organizer(self, organizer=None) -> None:
172172
"""
173-
goes via self.client, finds the principal, figures out the right attendee-format and adds an
174-
organizer line to the event
173+
Add (or replace) the ORGANIZER field on the calendar component.
174+
175+
If *organizer* is omitted the current principal is used (requires
176+
``self.client`` to be set). The *organizer* argument accepts the
177+
same types as :meth:`add_attendee`:
178+
179+
* A :class:`~caldav.Principal` object
180+
* A :class:`icalendar.vCalAddress` object
181+
* A ``"mailto:user@example.com"`` string
182+
* A plain email address string (``"mailto:"`` is prepended automatically)
183+
184+
Any pre-existing ORGANIZER field is removed before the new one is added.
175185
"""
176-
if self.client is None:
177-
raise ValueError("Unexpected value None for self.client")
186+
from .collection import Principal as _Principal ## avoid circular import
178187

179-
principal = self.client.principal()
180-
## TODO: remove Organizer-field, if exists
181-
## TODO: what if walk returns more than one vevent?
182-
self.icalendar_component.add("organizer", principal.get_vcal_address())
188+
if organizer is None:
189+
if self.client is None:
190+
raise ValueError("Unexpected value None for self.client")
191+
organizer_obj = self.client.principal().get_vcal_address()
192+
elif isinstance(organizer, _Principal):
193+
organizer_obj = organizer.get_vcal_address()
194+
elif isinstance(organizer, vCalAddress):
195+
organizer_obj = organizer
196+
elif isinstance(organizer, str):
197+
if organizer.startswith("mailto:"):
198+
organizer_obj = vCalAddress(organizer)
199+
else:
200+
organizer_obj = vCalAddress("mailto:" + organizer)
201+
else:
202+
raise ValueError(f"Unsupported organizer type: {type(organizer)!r}")
203+
204+
ievent = self.icalendar_component
205+
ievent.pop("organizer", None)
206+
ievent.add("organizer", organizer_obj)
183207

184208
def split_expanded(self) -> list[Self]:
185209
"""This was used internally for processing search results.

tests/test_caldav_unit.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2787,3 +2787,82 @@ def test_resolve_properties_unmatched_paths_production_mode(self):
27872787
{"/other/path/": {"foo": "bar"}, "/yet/another/": {"baz": "qux"}}
27882788
)
27892789
assert result == {}
2790+
2791+
2792+
class TestAddOrganizer:
2793+
"""Unit tests for CalendarObjectResource.add_organizer() (issue #524)."""
2794+
2795+
_ev = """\
2796+
BEGIN:VCALENDAR
2797+
VERSION:2.0
2798+
PRODID:-//Test//Test//EN
2799+
BEGIN:VEVENT
2800+
UID:test-add-organizer@example.com
2801+
DTSTAMP:20240101T000000Z
2802+
DTSTART:20240601T100000Z
2803+
DTEND:20240601T110000Z
2804+
SUMMARY:Test event
2805+
END:VEVENT
2806+
END:VCALENDAR
2807+
"""
2808+
2809+
def _make_event(self):
2810+
return Event(data=self._ev)
2811+
2812+
def test_add_organizer_email_string(self):
2813+
"""Passing a plain email string sets the ORGANIZER field."""
2814+
ev = self._make_event()
2815+
ev.add_organizer("organizer@example.com")
2816+
organizer = ev.icalendar_component.get("organizer")
2817+
assert organizer is not None
2818+
assert "organizer@example.com" in str(organizer)
2819+
2820+
def test_add_organizer_mailto_string(self):
2821+
"""Passing a mailto: URI sets the ORGANIZER field."""
2822+
ev = self._make_event()
2823+
ev.add_organizer("mailto:organizer@example.com")
2824+
organizer = ev.icalendar_component.get("organizer")
2825+
assert str(organizer) == "mailto:organizer@example.com"
2826+
2827+
def test_add_organizer_vcal_address(self):
2828+
"""Passing a vCalAddress directly sets the ORGANIZER field."""
2829+
from icalendar import vCalAddress
2830+
2831+
ev = self._make_event()
2832+
addr = vCalAddress("mailto:organizer@example.com")
2833+
ev.add_organizer(addr)
2834+
organizer = ev.icalendar_component.get("organizer")
2835+
assert str(organizer) == "mailto:organizer@example.com"
2836+
2837+
def test_add_organizer_replaces_existing(self):
2838+
"""Calling add_organizer twice replaces the first value, no duplicate."""
2839+
ev = self._make_event()
2840+
ev.add_organizer("first@example.com")
2841+
ev.add_organizer("second@example.com")
2842+
comp = ev.icalendar_component
2843+
## icalendar stores repeated properties as a list; ORGANIZER should be
2844+
## a single value, not a list.
2845+
organizer = comp.get("organizer")
2846+
assert not isinstance(organizer, list), "ORGANIZER should not be duplicated"
2847+
assert "second@example.com" in str(organizer)
2848+
2849+
def test_add_organizer_no_arg_uses_principal(self):
2850+
"""Calling add_organizer() without arguments uses the current principal."""
2851+
from icalendar import vCalAddress
2852+
2853+
ev = self._make_event()
2854+
mock_client = mock.MagicMock()
2855+
mock_principal = mock.MagicMock()
2856+
mock_principal.get_vcal_address.return_value = vCalAddress("mailto:me@example.com")
2857+
mock_client.principal.return_value = mock_principal
2858+
ev.client = mock_client
2859+
ev.add_organizer()
2860+
organizer = ev.icalendar_component.get("organizer")
2861+
assert str(organizer) == "mailto:me@example.com"
2862+
2863+
def test_add_organizer_no_arg_no_client_raises(self):
2864+
"""Calling add_organizer() without arguments and no client raises ValueError."""
2865+
ev = self._make_event()
2866+
ev.client = None
2867+
with pytest.raises(ValueError):
2868+
ev.add_organizer()

0 commit comments

Comments
 (0)