1""":rfc:`5545` VTODO component."""
2
3from __future__ import annotations
4
5import uuid
6from datetime import date, datetime, timedelta
7from typing import TYPE_CHECKING, Literal, Sequence
8
9from icalendar.attr import (
10 X_MOZ_LASTACK_property,
11 X_MOZ_SNOOZE_TIME_property,
12 attendees_property,
13 categories_property,
14 class_property,
15 color_property,
16 conferences_property,
17 contacts_property,
18 create_single_property,
19 description_property,
20 exdates_property,
21 images_property,
22 get_duration_property,
23 get_end_property,
24 get_start_end_duration_with_validation,
25 get_start_property,
26 location_property,
27 organizer_property,
28 priority_property,
29 property_del_duration,
30 property_doc_duration_template,
31 property_get_duration,
32 property_set_duration,
33 rdates_property,
34 rrules_property,
35 sequence_property,
36 set_duration_with_locking,
37 set_end_with_locking,
38 set_start_with_locking,
39 status_property,
40 summary_property,
41 uid_property,
42 url_property,
43)
44from icalendar.cal.component import Component
45
46if TYPE_CHECKING:
47 from icalendar.alarms import Alarms
48 from icalendar.enums import CLASS, STATUS
49 from icalendar.prop import vCalAddress
50 from icalendar.prop.conference import Conference
51
52
53class Todo(Component):
54 """
55 A "VTODO" calendar component is a grouping of component
56 properties that represents an action item or assignment. For
57 example, it can be used to represent an item of work assigned to
58 an individual, such as "Prepare for the upcoming conference
59 seminar on Internet Calendaring".
60
61 Examples:
62 Create a new Todo:
63
64 >>> from icalendar import Todo
65 >>> todo = Todo.new()
66 >>> print(todo.to_ical())
67 BEGIN:VTODO
68 DTSTAMP:20250517T080612Z
69 UID:d755cef5-2311-46ed-a0e1-6733c9e15c63
70 END:VTODO
71
72 """
73
74 name = "VTODO"
75
76 required = (
77 "UID",
78 "DTSTAMP",
79 )
80 singletons = (
81 "CLASS",
82 "COLOR",
83 "COMPLETED",
84 "CREATED",
85 "DESCRIPTION",
86 "DTSTAMP",
87 "DTSTART",
88 "GEO",
89 "LAST-MODIFIED",
90 "LOCATION",
91 "ORGANIZER",
92 "PERCENT-COMPLETE",
93 "PRIORITY",
94 "RECURRENCE-ID",
95 "SEQUENCE",
96 "STATUS",
97 "SUMMARY",
98 "UID",
99 "URL",
100 "DUE",
101 "DURATION",
102 )
103 exclusive = (
104 "DUE",
105 "DURATION",
106 )
107 multiple = (
108 "ATTACH",
109 "ATTENDEE",
110 "CATEGORIES",
111 "COMMENT",
112 "CONTACT",
113 "EXDATE",
114 "RSTATUS",
115 "RELATED",
116 "RESOURCES",
117 "RDATE",
118 "RRULE",
119 )
120 DTSTART = create_single_property(
121 "DTSTART",
122 "dt",
123 (datetime, date),
124 date,
125 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.', # noqa: E501
126 )
127 DUE = create_single_property(
128 "DUE",
129 "dt",
130 (datetime, date),
131 date,
132 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.', # noqa: E501
133 )
134 DURATION = property(
135 property_get_duration,
136 property_set_duration,
137 property_del_duration,
138 property_doc_duration_template.format(component="VTODO"),
139 )
140
141 def _get_start_end_duration(self):
142 """Verify the calendar validity and return the right attributes."""
143 return get_start_end_duration_with_validation(self, "DTSTART", "DUE", "VTODO")
144
145 @property
146 def start(self) -> date | datetime:
147 """The start of the VTODO.
148
149 Invalid values raise an InvalidCalendar.
150 If there is no start, we also raise an IncompleteComponent error.
151
152 You can get the start, end and duration of a Todo as follows:
153
154 >>> from datetime import datetime
155 >>> from icalendar import Todo
156 >>> todo = Todo()
157 >>> todo.start = datetime(2021, 1, 1, 12)
158 >>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes
159 >>> todo.duration # 1800 seconds == 30 minutes
160 datetime.timedelta(seconds=1800)
161 >>> print(todo.to_ical())
162 BEGIN:VTODO
163 DTSTART:20210101T120000
164 DUE:20210101T123000
165 END:VTODO
166 """
167 return get_start_property(self)
168
169 @start.setter
170 def start(self, start: date | datetime | None):
171 """Set the start."""
172 self.DTSTART = start
173
174 @property
175 def end(self) -> date | datetime:
176 """The end of the todo.
177
178 Invalid values raise an InvalidCalendar error.
179 If there is no end, we also raise an IncompleteComponent error.
180 """
181 return get_end_property(self, "DUE")
182
183 @end.setter
184 def end(self, end: date | datetime | None):
185 """Set the end."""
186 self.DUE = end
187
188 @property
189 def duration(self) -> timedelta:
190 """The duration of the VTODO.
191
192 Returns the DURATION property if set, otherwise calculated from start and end.
193 You can set the duration to automatically adjust the end time while keeping
194 start locked.
195
196 Setting the duration will:
197 1. Keep the start time locked (unchanged)
198 2. Adjust the end time to start + duration
199 3. Remove any existing DUE property
200 4. Set the DURATION property
201 """
202 return get_duration_property(self)
203
204 @duration.setter
205 def duration(self, value: timedelta):
206 if not isinstance(value, timedelta):
207 raise TypeError(f"Use timedelta, not {type(value).__name__}.")
208
209 # Use the set_duration method with default start-locked behavior
210 self.set_duration(value, locked="start")
211
212 def set_duration(
213 self, duration: timedelta | None, locked: Literal["start", "end"] = "start"
214 ):
215 """Set the duration of the event relative to either start or end.
216
217 Args:
218 duration: The duration to set, or None to convert to DURATION property
219 locked: Which property to keep unchanged ('start' or 'end')
220 """
221 set_duration_with_locking(self, duration, locked, "DUE")
222
223 def set_start(
224 self, start: date | datetime, locked: Literal["duration", "end"] | None = None
225 ):
226 """Set the start with explicit locking behavior.
227
228 Args:
229 start: The start time to set
230 locked: Which property to keep unchanged ('duration', 'end', or None
231 for auto-detect)
232 """
233 set_start_with_locking(self, start, locked, "DUE")
234
235 def set_end(
236 self, end: date | datetime, locked: Literal["start", "duration"] = "start"
237 ):
238 """Set the end of the component, keeping either the start or the duration same.
239
240 Args:
241 end: The end time to set
242 locked: Which property to keep unchanged ('start' or 'duration')
243 """
244 set_end_with_locking(self, end, locked, "DUE")
245
246 X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property
247 X_MOZ_LASTACK = X_MOZ_LASTACK_property
248
249 @property
250 def alarms(self) -> Alarms:
251 """Compute the alarm times for this component.
252
253 >>> from datetime import datetime
254 >>> from icalendar import Todo
255 >>> todo = Todo() # empty without alarms
256 >>> todo.start = datetime(2024, 10, 26, 10, 21)
257 >>> len(todo.alarms.times)
258 0
259
260 Note that this only uses DTSTART and DUE, but ignores
261 RDATE, EXDATE, and RRULE properties.
262 """
263 from icalendar.alarms import Alarms
264
265 return Alarms(self)
266
267 color = color_property
268 sequence = sequence_property
269 categories = categories_property
270 rdates = rdates_property
271 exdates = exdates_property
272 rrules = rrules_property
273 uid = uid_property
274 summary = summary_property
275 description = description_property
276 classification = class_property
277 url = url_property
278 organizer = organizer_property
279 location = location_property
280 priority = priority_property
281 contacts = contacts_property
282 status = status_property
283 attendees = attendees_property
284 images = images_property
285 conferences = conferences_property
286
287 @classmethod
288 def new(
289 cls,
290 /,
291 attendees: list[vCalAddress] | None = None,
292 categories: Sequence[str] = (),
293 classification: CLASS | None = None,
294 color: str | None = None,
295 comments: list[str] | str | None = None,
296 contacts: list[str] | str | None = None,
297 conferences: list[Conference] | None = None,
298 created: date | None = None,
299 description: str | None = None,
300 end: date | datetime | None = None,
301 last_modified: date | None = None,
302 location: str | None = None,
303 organizer: vCalAddress | str | None = None,
304 priority: int | None = None,
305 sequence: int | None = None,
306 stamp: date | None = None,
307 start: date | datetime | None = None,
308 status: STATUS | None = None,
309 summary: str | None = None,
310 uid: str | uuid.UUID | None = None,
311 url: str | None = None,
312 ):
313 """Create a new TODO with all required properties.
314
315 This creates a new Todo in accordance with :rfc:`5545`.
316
317 Arguments:
318 attendees: The :attr:`attendees` of the todo.
319 categories: The :attr:`categories` of the todo.
320 classification: The :attr:`classification` of the todo.
321 color: The :attr:`color` of the todo.
322 comments: The :attr:`Component.comments` of the todo.
323 conferences: The :attr:`conferences` of the todo.
324 created: The :attr:`Component.created` of the todo.
325 description: The :attr:`description` of the todo.
326 end: The :attr:`end` of the todo.
327 last_modified: The :attr:`Component.last_modified` of the todo.
328 location: The :attr:`location` of the todo.
329 organizer: The :attr:`organizer` of the todo.
330 sequence: The :attr:`sequence` of the todo.
331 stamp: The :attr:`Component.DTSTAMP` of the todo.
332 If None, this is set to the current time.
333 start: The :attr:`start` of the todo.
334 status: The :attr:`status` of the todo.
335 summary: The :attr:`summary` of the todo.
336 uid: The :attr:`uid` of the todo.
337 If None, this is set to a new :func:`uuid.uuid4`.
338 url: The :attr:`url` of the todo.
339
340 Returns:
341 :class:`Todo`
342
343 Raises:
344 InvalidCalendar: If the content is not valid according to :rfc:`5545`.
345
346 .. warning:: As time progresses, we will be stricter with the validation.
347 """
348 todo = super().new(
349 stamp=stamp if stamp is not None else cls._utc_now(),
350 created=created,
351 last_modified=last_modified,
352 comments=comments,
353 )
354 todo.summary = summary
355 todo.description = description
356 todo.uid = uid if uid is not None else uuid.uuid4()
357 todo.start = start
358 todo.end = end
359 todo.color = color
360 todo.categories = categories
361 todo.sequence = sequence
362 todo.classification = classification
363 todo.url = url
364 todo.organizer = organizer
365 todo.location = location
366 todo.priority = priority
367 todo.contacts = contacts
368 todo.status = status
369 todo.attendees = attendees
370 todo.conferences = conferences
371 if cls._validate_new:
372 cls._validate_start_and_end(start, end)
373 return todo
374
375
376__all__ = ["Todo"]