diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 84214c2c..cd1c4ef9 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -711,6 +711,19 @@ class Todo(Component): class Journal(Component): + """A describtive text at a certain time or associated with a component. + + A "VJOURNAL" calendar component is a grouping of + component properties that represent one or more descriptive text + notes associated with a particular calendar date. The "DTSTART" + property is used to specify the calendar date with which the + journal entry is associated. Generally, it will have a DATE value + data type, but it can also be used to specify a DATE-TIME value + data type. Examples of a journal entry include a daily record of + a legislative body or a journal entry of individual telephone + contacts for the day or an ordered list of accomplishments for the + day. + """ name = 'VJOURNAL' @@ -724,6 +737,34 @@ class Journal(Component): 'RELATED', 'RDATE', 'RRULE', 'RSTATUS', 'DESCRIPTION', ) + DTSTART = create_single_property( + "DTSTART", "dt", (datetime, date), date, + 'The "DTSTART" property for a "VJOURNAL" that specifies the exact date at which the journal entry was made.') + + @property + def start(self) -> date: + """The start of the Journal. + + The "DTSTART" + property is used to specify the calendar date with which the + journal entry is associated. + """ + start = self.DTSTART + if start is None: + raise IncompleteComponent("No DTSTART given.") + return start + + @start.setter + def start(self, value: datetime|date) -> None: + """Set the start of the journal.""" + self.DTSTART = value + + end = start + + @property + def duration(self) -> timedelta: + """The journal does not last any time.""" + return timedelta(0) class FreeBusy(Component): diff --git a/src/icalendar/tests/test_issue_662_component_properties.py b/src/icalendar/tests/test_issue_662_component_properties.py index fb71d601..4a151e24 100644 --- a/src/icalendar/tests/test_issue_662_component_properties.py +++ b/src/icalendar/tests/test_issue_662_component_properties.py @@ -6,12 +6,13 @@ try: from zoneinfo import ZoneInfo except ImportError: - from backports.zoneinfo import ZoneInfo # type: ignore + from backports.zoneinfo import ZoneInfo # type: ignore PGH003 from icalendar import ( Event, IncompleteComponent, InvalidCalendar, + Journal, vDDDTypes, ) from icalendar.prop import vDuration @@ -263,14 +264,25 @@ def test_incomplete_event(incomplete_event_end, attr): (datetime(2024, 10, 11, 10, 20), timedelta(days=1)), ] ) -@pytest.mark.parametrize("attr", ["start", "end", "DTSTART", "DTEND"]) -def test_set_invalid_start(invalid_value, attr): +@pytest.mark.parametrize( + ("Component", "attr"), + [ + (Event,"start"), + (Event,"end"), + (Event,"DTSTART"), + (Event,"DTEND"), + (Journal,"start"), + (Journal,"end"), + (Journal,"DTSTART"), + ] +) +def test_set_invalid_start(invalid_value, attr, Component): """Check that we get the right error. - other types that vDDDTypes accepts - object """ - event = Event() + event = Component() with pytest.raises(TypeError) as e: setattr(event, attr, invalid_value) assert e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}." @@ -318,7 +330,7 @@ def test_setting_duration_deletes_the_end(): assert "DTEND" not in event assert event.DTEND is None assert event.DURATION == timedelta(days=1) - + valid_values = pytest.mark.parametrize( ("attr", "value"), [ @@ -353,4 +365,39 @@ def test_invalid_none(attr): event = Event() event[attr] = None with pytest.raises(InvalidCalendar): - getattr(event, attr) \ No newline at end of file + getattr(event, attr) + +@pytest.mark.parametrize("attr", ["DTSTART", "end", "start"]) +@pytest.mark.parametrize("start", [ + datetime(2024, 10, 11, 10, 20), + date(2024, 10, 11), + datetime(2024, 10, 11, 10, 20, tzinfo=ZoneInfo("Europe/Paris")), +]) +def test_journal_start(start, attr): + """Test that we can set the start of a journal.""" + j = Journal() + setattr(j, attr, start) + assert start == j.DTSTART + assert j.start == start + assert j.end == start + assert j.duration == timedelta(0) + +@pytest.mark.parametrize("attr", ["start", "end"]) +def test_delete_journal_start(attr): + """Delete the start of the journal.""" + j = Journal() + j.start = datetime(2010, 11, 12, 13, 14) + j.DTSTART = None + assert j.DTSTART is None + assert "DTSTART" not in j + with pytest.raises(IncompleteComponent): + getattr(j, attr) + +def setting_twice_does_not_duplicate_the_entry(): + j = Journal() + j.DTSTART = date(2024, 1,1 ) + j.DTSTART = date(2024, 1, 3) + assert date(2024, 1, 3) == j.DTSTART + assert j.start == date(2024, 1, 3) + assert j.end == date(2024, 1, 3) +