Add categories property

pull/808/head
Nicco Kunzmann 2025-04-23 19:36:46 +02:00
rodzic 33f554fb2f
commit cca55fc8c2
6 zmienionych plików z 156 dodań i 6 usunięć

Wyświetl plik

@ -17,6 +17,7 @@ New features:
- Add attributes to the calendar for properties ``NAME``, ``DESCRIPTION``, and ``COLOR``. See `Issue 655 <https://github.com/collective/icalendar/issues/655>`_.
- Add ``sequence`` attribute to ``Event``, ``Todo``, and ``Journal`` components. See `Issue 802 <https://github.com/collective/icalendar/issues/802>`_.
- Add ``categories`` attribute to ``Calendar``, ``Event``, ``Todo``, and ``Journal`` components. See `Issue 655 <https://github.com/collective/icalendar/issues/655>`_.
- Add compatibility to :rfc:`6868`. See `Issue 652 <https://github.com/collective/icalendar/issues/652>`_.
Bug fixes:

Wyświetl plik

@ -2,10 +2,11 @@
from __future__ import annotations
from datetime import date, datetime
import itertools
from typing import TYPE_CHECKING, Optional
from icalendar.error import InvalidCalendar
from icalendar.prop import vDDDTypes, vText
from icalendar.prop import vCategory, vDDDTypes, vText
from icalendar.timezone import tzp
if TYPE_CHECKING:
@ -179,9 +180,79 @@ Examples:
"""
)
def _get_categories(component: Component) -> list[str]:
"""Get all the categories."""
categories : Optional[vCategory|list[vCategory]] = component.get("CATEGORIES")
if isinstance(categories, list):
_set_categories(component, list(itertools.chain.from_iterable(cat.cats for cat in categories)))
return _get_categories(component)
if categories is None:
categories = vCategory([])
component.add("CATEGORIES", categories)
return categories.cats
def _set_categories(component: Component, cats: list[str]) -> None:
"""Set the categories."""
component["CATEGORIES"] = categories = vCategory(cats)
cats.clear()
cats.extend(categories.cats)
categories.cats = cats
def _del_categories(component: Component) -> None:
"""Delete the categories."""
component.pop("CATEGORIES", None)
categories_property = property(
_get_categories,
_set_categories,
_del_categories,
"""This property defines the categories for a component.
Property Parameters:
IANA, non-standard, and language property parameters can be specified on this
property.
Conformance:
The property can be specified within "VEVENT", "VTODO", or "VJOURNAL" calendar
components.
Since :rfc:`7986` it can also be defined on a "VCALENDAR" component.
Description:
This property is used to specify categories or subtypes
of the calendar component. The categories are useful in searching
for a calendar component of a particular type and category.
Within the "VEVENT", "VTODO", or "VJOURNAL" calendar components,
more than one category can be specified as a COMMA-separated list
of categories.
Example:
>>> from icalendar import Event
>>> event = Event()
>>> event.categories = ["Work", "Meeting"]
>>> print(event.to_ical())
BEGIN:VEVENT
CATEGORIES:Work,Meeting
END:VEVENT
>>> event.categories.append("Lecture")
>>> event.categories == ["Work", "Meeting", "Lecture"]
True
.. note::
At present, we do not take the LANGUAGE parameter into account.
"""
)
__all__ = [
"single_utc_property",
"multi_language_text_property",
"single_int_property",
"sequence_property"
"sequence_property",
"categories_property",
]

Wyświetl plik

@ -15,6 +15,7 @@ import dateutil.rrule
import dateutil.tz
from icalendar.attr import (
categories_property,
multi_language_text_property,
sequence_property,
single_int_property,
@ -894,6 +895,7 @@ class Event(Component):
X_MOZ_SNOOZE_TIME = _X_MOZ_SNOOZE_TIME
X_MOZ_LASTACK = _X_MOZ_LASTACK
sequence = sequence_property
categories = categories_property
class Todo(Component):
@ -1083,6 +1085,7 @@ class Todo(Component):
return Alarms(self)
sequence = sequence_property
categories = categories_property
class Journal(Component):
@ -1168,6 +1171,7 @@ class Journal(Component):
return timedelta(0)
sequence = sequence_property
categories = categories_property
class FreeBusy(Component):
@ -2101,6 +2105,7 @@ class Calendar(Component):
END:VCALENDAR
"""
)
categories = categories_property
# These are read only singleton, so one instance is enough for the module
types_factory = TypesFactory()

Wyświetl plik

@ -430,17 +430,20 @@ class vDDDLists:
class vCategory:
params: Parameters
def __init__(self, c_list, params={}):
def __init__(self, c_list:list[str] | str, params={}):
if not hasattr(c_list, "__iter__") or isinstance(c_list, str):
c_list = [c_list]
self.cats = [vText(c) for c in c_list]
self.cats : list[vText|str] = [vText(c) for c in c_list]
self.params = Parameters(params)
def __iter__(self):
return iter(vCategory.from_ical(self.to_ical()))
def to_ical(self):
return b",".join([c.to_ical() for c in self.cats])
return b",".join([
c.to_ical() if hasattr(c, "to_ical") else vText(c).to_ical()
for c in self.cats
])
@staticmethod
def from_ical(ical):

Wyświetl plik

@ -1,7 +1,7 @@
"""This tests the compatibility with RFC 7529.
See
- https://github.com/collective/icalendar/issues/653
- https://github.com/collective/icalendar/issues/655
- https://www.rfc-editor.org/rfc/rfc7529.html
"""

Wyświetl plik

@ -0,0 +1,70 @@
"""This tests the compatibility with RFC 7529.
CATEGORIES property
See
- https://github.com/collective/icalendar/issues/655
- https://www.rfc-editor.org/rfc/rfc7529.html
"""
from typing import Union
import pytest
from icalendar import Calendar, Event, Journal, Todo
CTJE = Union[Calendar, Event, Journal, Todo]
@pytest.fixture(params=[Event, Calendar, Todo, Journal])
def component(request):
"""An empty component with possible categories."""
return request.param()
def test_no_categories_at_creation(component: CTJE):
"""An empty component has no categories."""
assert "CATEGORIES" not in component
assert component.categories == []
def test_add_one_category(component: CTJE):
"""Add one category."""
component.add("categories", "Lecture")
assert component.categories == ["Lecture"]
def test_add_multiple_categories(component: CTJE):
"""Add categories."""
component.add("categories", ["Lecture", "Workshop"])
assert component.categories == ["Lecture", "Workshop"]
def test_set_categories(component: CTJE):
"""Set categories."""
component.categories = ["Lecture", "Workshop"]
assert component.categories == ["Lecture", "Workshop"]
def test_modify_list(component: CTJE):
"""Modify the list and it still works."""
component.categories = categories = ["cat1"]
categories.append("cat2")
assert component.categories == ["cat1", "cat2"]
def test_delete_categories(component: CTJE):
"""Delete categories."""
component.categories = ["Lecture", "Workshop"]
del component.categories
assert "CATEGORIES" not in component
assert component.categories == []
def test_deal_with_several_categories(component: CTJE):
"""If we have categories several times, we should all use them."""
component.add("categories", ["c1", "c2"])
component.add("categories", ["c3", "c4"])
assert component.categories == ["c1", "c2", "c3", "c4"]
component.categories.append("c5")
assert component.categories == ["c1", "c2", "c3", "c4", "c5"]
component.categories.remove("c2")
assert component.categories == ["c1", "c3", "c4", "c5"]
assert "c1,c3,c4,c5" in component.to_ical().decode()