Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/todo.py: 60%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

103 statements  

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 

8 

9from icalendar.attr import ( 

10 CONCEPTS_TYPE_SETTER, 

11 LINKS_TYPE_SETTER, 

12 RELATED_TO_TYPE_SETTER, 

13 X_MOZ_LASTACK_property, 

14 X_MOZ_SNOOZE_TIME_property, 

15 attendees_property, 

16 categories_property, 

17 class_property, 

18 color_property, 

19 conferences_property, 

20 contacts_property, 

21 create_single_property, 

22 description_property, 

23 exdates_property, 

24 get_duration_property, 

25 get_end_property, 

26 get_start_end_duration_with_validation, 

27 get_start_property, 

28 images_property, 

29 location_property, 

30 organizer_property, 

31 priority_property, 

32 property_del_duration, 

33 property_doc_duration_template, 

34 property_get_duration, 

35 property_set_duration, 

36 rdates_property, 

37 rrules_property, 

38 sequence_property, 

39 set_duration_with_locking, 

40 set_end_with_locking, 

41 set_start_with_locking, 

42 status_property, 

43 summary_property, 

44 uid_property, 

45 url_property, 

46) 

47from icalendar.cal.component import Component 

48from icalendar.cal.examples import get_example 

49 

50if TYPE_CHECKING: 

51 from collections.abc import Sequence 

52 

53 from icalendar.alarms import Alarms 

54 from icalendar.enums import CLASS, STATUS 

55 from icalendar.prop import vCalAddress 

56 from icalendar.prop.conference import Conference 

57 

58 

59class Todo(Component): 

60 """ 

61 A "VTODO" calendar component is a grouping of component 

62 properties that represents an action item or assignment. For 

63 example, it can be used to represent an item of work assigned to 

64 an individual, such as "Prepare for the upcoming conference 

65 seminar on Internet Calendaring". 

66 

67 Examples: 

68 Create a new Todo: 

69 

70 >>> from icalendar import Todo 

71 >>> todo = Todo.new() 

72 >>> print(todo.to_ical()) 

73 BEGIN:VTODO 

74 DTSTAMP:20250517T080612Z 

75 UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 

76 END:VTODO 

77 

78 Complete the example Todo. 

79 

80 .. code-block:: pycon 

81 

82 >>> from datetime import datetime, timezone 

83 >>> from icalendar import Todo, STATUS 

84 >>> todo = Todo.example() 

85 >>> todo["PERCENT-COMPLETE"] = 100 

86 >>> todo["COMPLETED"] = datetime(2007, 5, 1, 12, tzinfo=timezone.utc) 

87 >>> todo.status = STATUS.COMPLETED 

88 >>> print(todo.to_ical().decode()) 

89 BEGIN:VTODO 

90 CATEGORIES:FAMILY,FINANCE 

91 CLASS:CONFIDENTIAL 

92 COMPLETED:2007-05-01 12:00:00+00:00 

93 DTSTAMP:20070313T123432Z 

94 DUE;VALUE=DATE:20070501 

95 PERCENT-COMPLETE:100 

96 STATUS:COMPLETED 

97 SUMMARY:Submit Quebec Income Tax Return for 2006 

98 UID:20070313T123432Z-456553@example.com 

99 END:VTODO 

100 

101 """ 

102 

103 name = "VTODO" 

104 

105 required = ( 

106 "UID", 

107 "DTSTAMP", 

108 ) 

109 singletons = ( 

110 "CLASS", 

111 "COLOR", 

112 "COMPLETED", 

113 "CREATED", 

114 "DESCRIPTION", 

115 "DTSTAMP", 

116 "DTSTART", 

117 "GEO", 

118 "LAST-MODIFIED", 

119 "LOCATION", 

120 "ORGANIZER", 

121 "PERCENT-COMPLETE", 

122 "PRIORITY", 

123 "RECURRENCE-ID", 

124 "SEQUENCE", 

125 "STATUS", 

126 "SUMMARY", 

127 "UID", 

128 "URL", 

129 "DUE", 

130 "DURATION", 

131 ) 

132 exclusive = ( 

133 "DUE", 

134 "DURATION", 

135 ) 

136 multiple = ( 

137 "ATTACH", 

138 "ATTENDEE", 

139 "CATEGORIES", 

140 "COMMENT", 

141 "CONTACT", 

142 "EXDATE", 

143 "RSTATUS", 

144 "RELATED", 

145 "RESOURCES", 

146 "RDATE", 

147 "RRULE", 

148 ) 

149 DTSTART = create_single_property( 

150 "DTSTART", 

151 "dt", 

152 (datetime, date), 

153 date, 

154 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.', 

155 ) 

156 DUE = create_single_property( 

157 "DUE", 

158 "dt", 

159 (datetime, date), 

160 date, 

161 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.', 

162 ) 

163 DURATION = property( 

164 property_get_duration, 

165 property_set_duration, 

166 property_del_duration, 

167 property_doc_duration_template.format(component="VTODO"), 

168 ) 

169 

170 def _get_start_end_duration(self): 

171 """Verify the calendar validity and return the right attributes.""" 

172 return get_start_end_duration_with_validation(self, "DTSTART", "DUE", "VTODO") 

173 

174 @property 

175 def start(self) -> date | datetime: 

176 """The start of the VTODO. 

177 

178 Invalid values raise an InvalidCalendar. 

179 If there is no start, we also raise an IncompleteComponent error. 

180 

181 You can get the start, end and duration of a Todo as follows: 

182 

183 >>> from datetime import datetime 

184 >>> from icalendar import Todo 

185 >>> todo = Todo() 

186 >>> todo.start = datetime(2021, 1, 1, 12) 

187 >>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes 

188 >>> todo.duration # 1800 seconds == 30 minutes 

189 datetime.timedelta(seconds=1800) 

190 >>> print(todo.to_ical()) 

191 BEGIN:VTODO 

192 DTSTART:20210101T120000 

193 DUE:20210101T123000 

194 END:VTODO 

195 """ 

196 return get_start_property(self) 

197 

198 @start.setter 

199 def start(self, start: date | datetime | None): 

200 """Set the start.""" 

201 self.DTSTART = start 

202 

203 @property 

204 def end(self) -> date | datetime: 

205 """The end of the todo. 

206 

207 Invalid values raise an InvalidCalendar error. 

208 If there is no end, we also raise an IncompleteComponent error. 

209 """ 

210 return get_end_property(self, "DUE") 

211 

212 @end.setter 

213 def end(self, end: date | datetime | None): 

214 """Set the end.""" 

215 self.DUE = end 

216 

217 @property 

218 def duration(self) -> timedelta: 

219 """The duration of the VTODO. 

220 

221 Returns the DURATION property if set, otherwise calculated from start and end. 

222 You can set the duration to automatically adjust the end time while keeping 

223 start locked. 

224 

225 Setting the duration will: 

226 

227 1. Keep the start time locked (unchanged) 

228 2. Adjust the end time to start + duration 

229 3. Remove any existing DUE property 

230 4. Set the DURATION property 

231 """ 

232 return get_duration_property(self) 

233 

234 @duration.setter 

235 def duration(self, value: timedelta): 

236 if not isinstance(value, timedelta): 

237 raise TypeError(f"Use timedelta, not {type(value).__name__}.") 

238 

239 # Use the set_duration method with default start-locked behavior 

240 self.set_duration(value, locked="start") 

241 

242 def set_duration( 

243 self, duration: timedelta | None, locked: Literal["start", "end"] = "start" 

244 ): 

245 """Set the duration of the event relative to either start or end. 

246 

247 Parameters: 

248 duration: The duration to set, or None to convert to DURATION property 

249 locked: Which property to keep unchanged ('start' or 'end') 

250 """ 

251 set_duration_with_locking(self, duration, locked, "DUE") 

252 

253 def set_start( 

254 self, start: date | datetime, locked: Literal["duration", "end"] | None = None 

255 ): 

256 """Set the start with explicit locking behavior. 

257 

258 Parameters: 

259 start: The start time to set 

260 locked: Which property to keep unchanged ('duration', 'end', or None 

261 for auto-detect) 

262 """ 

263 set_start_with_locking(self, start, locked, "DUE") 

264 

265 def set_end( 

266 self, end: date | datetime, locked: Literal["start", "duration"] = "start" 

267 ): 

268 """Set the end of the component, keeping either the start or the duration same. 

269 

270 Parameters: 

271 end: The end time to set 

272 locked: Which property to keep unchanged ('start' or 'duration') 

273 """ 

274 set_end_with_locking(self, end, locked, "DUE") 

275 

276 X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property 

277 X_MOZ_LASTACK = X_MOZ_LASTACK_property 

278 

279 @property 

280 def alarms(self) -> Alarms: 

281 """Compute the alarm times for this component. 

282 

283 >>> from datetime import datetime 

284 >>> from icalendar import Todo 

285 >>> todo = Todo() # empty without alarms 

286 >>> todo.start = datetime(2024, 10, 26, 10, 21) 

287 >>> len(todo.alarms.times) 

288 0 

289 

290 Note that this only uses DTSTART and DUE, but ignores 

291 RDATE, EXDATE, and RRULE properties. 

292 """ 

293 from icalendar.alarms import Alarms 

294 

295 return Alarms(self) 

296 

297 color = color_property 

298 sequence = sequence_property 

299 categories = categories_property 

300 rdates = rdates_property 

301 exdates = exdates_property 

302 rrules = rrules_property 

303 uid = uid_property 

304 summary = summary_property 

305 description = description_property 

306 classification = class_property 

307 url = url_property 

308 organizer = organizer_property 

309 location = location_property 

310 priority = priority_property 

311 contacts = contacts_property 

312 status = status_property 

313 attendees = attendees_property 

314 images = images_property 

315 conferences = conferences_property 

316 

317 @classmethod 

318 def new( 

319 cls, 

320 /, 

321 attendees: list[vCalAddress] | None = None, 

322 categories: Sequence[str] = (), 

323 classification: CLASS | None = None, 

324 color: str | None = None, 

325 comments: list[str] | str | None = None, 

326 concepts: CONCEPTS_TYPE_SETTER = None, 

327 contacts: list[str] | str | None = None, 

328 conferences: list[Conference] | None = None, 

329 created: date | None = None, 

330 description: str | None = None, 

331 end: date | datetime | None = None, 

332 last_modified: date | None = None, 

333 links: LINKS_TYPE_SETTER = None, 

334 location: str | None = None, 

335 organizer: vCalAddress | str | None = None, 

336 priority: int | None = None, 

337 refids: list[str] | str | None = None, 

338 related_to: RELATED_TO_TYPE_SETTER = None, 

339 sequence: int | None = None, 

340 stamp: date | None = None, 

341 start: date | datetime | None = None, 

342 status: STATUS | None = None, 

343 summary: str | None = None, 

344 uid: str | uuid.UUID | None = None, 

345 url: str | None = None, 

346 ): 

347 """Create a new TODO with all required properties. 

348 

349 This creates a new Todo in accordance with :rfc:`5545`. 

350 

351 Parameters: 

352 attendees: The :attr:`attendees` of the todo. 

353 categories: The :attr:`categories` of the todo. 

354 classification: The :attr:`classification` of the todo. 

355 color: The :attr:`color` of the todo. 

356 comments: The :attr:`~icalendar.Component.comments` of the todo. 

357 concepts: The :attr:`~icalendar.Component.concepts` of the todo. 

358 contacts: The :attr:`contacts` of the todo. 

359 conferences: The :attr:`conferences` of the todo. 

360 created: The :attr:`~icalendar.Component.created` of the todo. 

361 description: The :attr:`description` of the todo. 

362 end: The :attr:`end` of the todo. 

363 last_modified: The :attr:`~icalendar.Component.last_modified` of the todo. 

364 links: The :attr:`~icalendar.Component.links` of the todo. 

365 location: The :attr:`location` of the todo. 

366 organizer: The :attr:`organizer` of the todo. 

367 refids: :attr:`~icalendar.Component.refids` of the todo. 

368 related_to: :attr:`~icalendar.Component.related_to` of the todo. 

369 sequence: The :attr:`sequence` of the todo. 

370 stamp: The :attr:`~icalendar.Component.DTSTAMP` of the todo. 

371 If None, this is set to the current time. 

372 start: The :attr:`start` of the todo. 

373 status: The :attr:`status` of the todo. 

374 summary: The :attr:`summary` of the todo. 

375 uid: The :attr:`uid` of the todo. 

376 If None, this is set to a new :func:`uuid.uuid4`. 

377 url: The :attr:`url` of the todo. 

378 

379 Returns: 

380 :class:`Todo` 

381 

382 Raises: 

383 ~error.InvalidCalendar: If the content is not valid 

384 according to :rfc:`5545`. 

385 

386 .. warning:: As time progresses, we will be stricter with the validation. 

387 """ 

388 todo: Todo = super().new( 

389 stamp=stamp if stamp is not None else cls._utc_now(), 

390 created=created, 

391 last_modified=last_modified, 

392 comments=comments, 

393 links=links, 

394 related_to=related_to, 

395 refids=refids, 

396 concepts=concepts, 

397 ) 

398 todo.summary = summary 

399 todo.description = description 

400 todo.uid = uid if uid is not None else uuid.uuid4() 

401 todo.start = start 

402 todo.end = end 

403 todo.color = color 

404 todo.categories = categories 

405 todo.sequence = sequence 

406 todo.classification = classification 

407 todo.url = url 

408 todo.organizer = organizer 

409 todo.location = location 

410 todo.priority = priority 

411 todo.contacts = contacts 

412 todo.status = status 

413 todo.attendees = attendees 

414 todo.conferences = conferences 

415 

416 if cls._validate_new: 

417 cls._validate_start_and_end(start, end) 

418 return todo 

419 

420 @classmethod 

421 def example(cls, name: str = "example") -> Todo: 

422 """Return the todo example with the given name.""" 

423 return cls.from_ical(get_example("todos", name)) 

424 

425 

426__all__ = ["Todo"]