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