Skip to content

Commit 07cba06

Browse files
tobixenclaude
andcommitted
fix: add_organizer() now accepts organizer argument and replaces existing field (closes #524)
- Accept optional organizer parameter (Principal, vCalAddress, mailto: string, or plain email string) matching the same input types as add_attendee(); falls back to current principal when omitted - Pop any pre-existing ORGANIZER field before adding to prevent duplicates - Add unit tests covering all input types, duplicate replacement, the no-arg/principal path, and the no-client error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2ec3622 commit 07cba06

2 files changed

Lines changed: 112 additions & 9 deletions

File tree

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
@@ -2629,3 +2629,82 @@ def test_meta_section_returns_multiple_dicts(self, tmp_path):
26292629
"https://work.example.com/dav/",
26302630
"https://personal.example.com/dav/",
26312631
}
2632+
2633+
2634+
class TestAddOrganizer:
2635+
"""Unit tests for CalendarObjectResource.add_organizer() (issue #524)."""
2636+
2637+
_ev = """\
2638+
BEGIN:VCALENDAR
2639+
VERSION:2.0
2640+
PRODID:-//Test//Test//EN
2641+
BEGIN:VEVENT
2642+
UID:test-add-organizer@example.com
2643+
DTSTAMP:20240101T000000Z
2644+
DTSTART:20240601T100000Z
2645+
DTEND:20240601T110000Z
2646+
SUMMARY:Test event
2647+
END:VEVENT
2648+
END:VCALENDAR
2649+
"""
2650+
2651+
def _make_event(self):
2652+
return Event(data=self._ev)
2653+
2654+
def test_add_organizer_email_string(self):
2655+
"""Passing a plain email string sets the ORGANIZER field."""
2656+
ev = self._make_event()
2657+
ev.add_organizer("organizer@example.com")
2658+
organizer = ev.icalendar_component.get("organizer")
2659+
assert organizer is not None
2660+
assert "organizer@example.com" in str(organizer)
2661+
2662+
def test_add_organizer_mailto_string(self):
2663+
"""Passing a mailto: URI sets the ORGANIZER field."""
2664+
ev = self._make_event()
2665+
ev.add_organizer("mailto:organizer@example.com")
2666+
organizer = ev.icalendar_component.get("organizer")
2667+
assert str(organizer) == "mailto:organizer@example.com"
2668+
2669+
def test_add_organizer_vcal_address(self):
2670+
"""Passing a vCalAddress directly sets the ORGANIZER field."""
2671+
from icalendar import vCalAddress
2672+
2673+
ev = self._make_event()
2674+
addr = vCalAddress("mailto:organizer@example.com")
2675+
ev.add_organizer(addr)
2676+
organizer = ev.icalendar_component.get("organizer")
2677+
assert str(organizer) == "mailto:organizer@example.com"
2678+
2679+
def test_add_organizer_replaces_existing(self):
2680+
"""Calling add_organizer twice replaces the first value, no duplicate."""
2681+
ev = self._make_event()
2682+
ev.add_organizer("first@example.com")
2683+
ev.add_organizer("second@example.com")
2684+
comp = ev.icalendar_component
2685+
## icalendar stores repeated properties as a list; ORGANIZER should be
2686+
## a single value, not a list.
2687+
organizer = comp.get("organizer")
2688+
assert not isinstance(organizer, list), "ORGANIZER should not be duplicated"
2689+
assert "second@example.com" in str(organizer)
2690+
2691+
def test_add_organizer_no_arg_uses_principal(self):
2692+
"""Calling add_organizer() without arguments uses the current principal."""
2693+
from icalendar import vCalAddress
2694+
2695+
ev = self._make_event()
2696+
mock_client = mock.MagicMock()
2697+
mock_principal = mock.MagicMock()
2698+
mock_principal.get_vcal_address.return_value = vCalAddress("mailto:me@example.com")
2699+
mock_client.principal.return_value = mock_principal
2700+
ev.client = mock_client
2701+
ev.add_organizer()
2702+
organizer = ev.icalendar_component.get("organizer")
2703+
assert str(organizer) == "mailto:me@example.com"
2704+
2705+
def test_add_organizer_no_arg_no_client_raises(self):
2706+
"""Calling add_organizer() without arguments and no client raises ValueError."""
2707+
ev = self._make_event()
2708+
ev.client = None
2709+
with pytest.raises(ValueError):
2710+
ev.add_organizer()

0 commit comments

Comments
 (0)