1""":rfc:`5545` VJOURNAL component."""
2
3from __future__ import annotations
4
5import uuid
6from datetime import date, datetime, timedelta
7from typing import TYPE_CHECKING
8
9from icalendar.attr import (
10 CONCEPTS_TYPE_SETTER,
11 LINKS_TYPE_SETTER,
12 RELATED_TO_TYPE_SETTER,
13 attendees_property,
14 categories_property,
15 class_property,
16 color_property,
17 contacts_property,
18 create_single_property,
19 descriptions_property,
20 exdates_property,
21 images_property,
22 organizer_property,
23 rdates_property,
24 rrules_property,
25 sequence_property,
26 status_property,
27 summary_property,
28 uid_property,
29 url_property,
30)
31from icalendar.cal.component import Component
32from icalendar.cal.examples import get_example
33from icalendar.error import IncompleteComponent
34
35if TYPE_CHECKING:
36 from collections.abc import Sequence
37
38 from icalendar.enums import CLASS, STATUS
39 from icalendar.prop import vCalAddress
40
41
42class Journal(Component):
43 """A descriptive text at a certain time or associated with a component.
44
45 Description:
46 A "VJOURNAL" calendar component is a grouping of
47 component properties that represent one or more descriptive text
48 notes associated with a particular calendar date. The "DTSTART"
49 property is used to specify the calendar date with which the
50 journal entry is associated. Generally, it will have a DATE value
51 data type, but it can also be used to specify a DATE-TIME value
52 data type. Examples of a journal entry include a daily record of
53 a legislative body or a journal entry of individual telephone
54 contacts for the day or an ordered list of accomplishments for the
55 day.
56
57 Examples:
58 Create a new Journal:
59
60 >>> from icalendar import Journal
61 >>> journal = Journal.new()
62 >>> print(journal.to_ical())
63 BEGIN:VJOURNAL
64 DTSTAMP:20250517T080612Z
65 UID:d755cef5-2311-46ed-a0e1-6733c9e15c63
66 END:VJOURNAL
67
68 Get the example Journal.
69
70 .. code-block:: pycon
71
72 >>> from icalendar import Journal
73 >>> journal = Journal.example()
74 >>> print(journal.to_ical().decode())
75 BEGIN:VJOURNAL
76 DESCRIPTION:Aurora project plans were reviewed.
77 DTSTAMP:19970901T130000Z
78 DTSTART;VALUE=DATE:19970317
79 SUMMARY:Staff meeting minutes
80 UID:19970901T130000Z-123405@example.com
81 END:VJOURNAL
82
83 """
84
85 name = "VJOURNAL"
86
87 required = (
88 "UID",
89 "DTSTAMP",
90 )
91 singletons = (
92 "CLASS",
93 "COLOR",
94 "CREATED",
95 "DTSTART",
96 "DTSTAMP",
97 "LAST-MODIFIED",
98 "ORGANIZER",
99 "RECURRENCE-ID",
100 "SEQUENCE",
101 "STATUS",
102 "SUMMARY",
103 "UID",
104 "URL",
105 )
106 multiple = (
107 "ATTACH",
108 "ATTENDEE",
109 "CATEGORIES",
110 "COMMENT",
111 "CONTACT",
112 "EXDATE",
113 "RELATED",
114 "RDATE",
115 "RRULE",
116 "RSTATUS",
117 "DESCRIPTION",
118 )
119
120 DTSTART = create_single_property(
121 "DTSTART",
122 "dt",
123 (datetime, date),
124 date,
125 'The "DTSTART" property for a "VJOURNAL" that specifies the exact date at which the journal entry was made.',
126 )
127
128 @property
129 def start(self) -> date:
130 """The start of the Journal.
131
132 The "DTSTART"
133 property is used to specify the calendar date with which the
134 journal entry is associated.
135 """
136 start = self.DTSTART
137 if start is None:
138 raise IncompleteComponent("No DTSTART given.")
139 return start
140
141 @start.setter
142 def start(self, value: datetime | date) -> None:
143 """Set the start of the journal."""
144 self.DTSTART = value
145
146 end = start
147
148 @property
149 def duration(self) -> timedelta:
150 """The journal has no duration: timedelta(0)."""
151 return timedelta(0)
152
153 color = color_property
154 sequence = sequence_property
155 categories = categories_property
156 rdates = rdates_property
157 exdates = exdates_property
158 rrules = rrules_property
159 uid = uid_property
160
161 summary = summary_property
162 descriptions = descriptions_property
163 classification = class_property
164 url = url_property
165 organizer = organizer_property
166 contacts = contacts_property
167 status = status_property
168 attendees = attendees_property
169 from icalendar.attr import RECURRENCE_ID
170
171 @property
172 def description(self) -> str:
173 """The concatenated descriptions of the journal.
174
175 A Journal can have several descriptions.
176 This is a compatibility method.
177 """
178 descriptions = self.descriptions
179 if not descriptions:
180 return None
181 return "\r\n\r\n".join(descriptions)
182
183 @description.setter
184 def description(self, description: str | None):
185 """Set the description"""
186 self.descriptions = description
187
188 @description.deleter
189 def description(self):
190 """Delete all descriptions."""
191 del self.descriptions
192
193 images = images_property
194
195 @classmethod
196 def new(
197 cls,
198 /,
199 attendees: list[vCalAddress] | None = None,
200 categories: Sequence[str] = (),
201 classification: CLASS | None = None,
202 color: str | None = None,
203 comments: list[str] | str | None = None,
204 concepts: CONCEPTS_TYPE_SETTER = None,
205 contacts: list[str] | str | None = None,
206 created: date | None = None,
207 description: str | Sequence[str] | None = None,
208 last_modified: date | None = None,
209 links: LINKS_TYPE_SETTER = None,
210 organizer: vCalAddress | str | None = None,
211 recurrence_id: date | datetime | None = None,
212 refids: list[str] | str | None = None,
213 related_to: RELATED_TO_TYPE_SETTER = None,
214 sequence: int | None = None,
215 stamp: date | None = None,
216 start: date | datetime | None = None,
217 status: STATUS | None = None,
218 summary: str | None = None,
219 uid: str | uuid.UUID | None = None,
220 url: str | None = None,
221 ):
222 """Create a new journal entry with all required properties.
223
224 This creates a new Journal in accordance with :rfc:`5545`.
225
226 Parameters:
227 attendees: The :attr:`attendees` of the journal.
228 categories: The :attr:`categories` of the journal.
229 classification: The :attr:`classification` of the journal.
230 color: The :attr:`color` of the journal.
231 comments: The :attr:`~icalendar.Component.comments` of the journal.
232 concepts: The :attr:`~icalendar.Component.concepts` of the journal.
233 contacts: The :attr:`contacts` of the journal.
234 created: The :attr:`~icalendar.Component.created` of the journal.
235 description: The :attr:`description` of the journal.
236 last_modified: The :attr:`~icalendar.Component.last_modified` of
237 the journal.
238 links: The :attr:`~icalendar.Component.links` of the journal.
239 organizer: The :attr:`organizer` of the journal.
240 recurrence_id: The :attr:`RECURRENCE_ID` of the journal.
241 refids: :attr:`~icalendar.Component.refids` of the journal.
242 related_to: :attr:`~icalendar.Component.related_to` of the journal.
243 sequence: The :attr:`sequence` of the journal.
244 stamp: The :attr:`~icalendar.Component.stamp` of the journal.
245 If None, this is set to the current time.
246 start: The :attr:`start` of the journal.
247 status: The :attr:`status` of the journal.
248 summary: The :attr:`summary` of the journal.
249 uid: The :attr:`uid` of the journal.
250 If None, this is set to a new :func:`uuid.uuid4`.
251 url: The :attr:`url` of the journal.
252
253 Returns:
254 :class:`Journal`
255
256 Raises:
257 ~error.InvalidCalendar: If the content is not valid
258 according to :rfc:`5545`.
259
260 .. warning:: As time progresses, we will be stricter with the validation.
261 """
262 journal: Journal = super().new(
263 stamp=stamp if stamp is not None else cls._utc_now(),
264 created=created,
265 last_modified=last_modified,
266 comments=comments,
267 links=links,
268 related_to=related_to,
269 refids=refids,
270 concepts=concepts,
271 )
272 journal.summary = summary
273 journal.descriptions = description
274 journal.uid = uid if uid is not None else uuid.uuid4()
275 journal.start = start
276 journal.color = color
277 journal.categories = categories
278 journal.sequence = sequence
279 journal.classification = classification
280 journal.url = url
281 journal.organizer = organizer
282 journal.contacts = contacts
283 journal.start = start
284 journal.status = status
285 journal.attendees = attendees
286 journal.RECURRENCE_ID = recurrence_id
287
288 return journal
289
290 @classmethod
291 def example(cls, name: str = "example") -> Journal:
292 """Return the journal example with the given name."""
293 return cls.from_ical(get_example("journals", name))
294
295
296__all__ = ["Journal"]