diff --git a/django_ical/feedgenerator.py b/django_ical/feedgenerator.py index 8682666..984b082 100644 --- a/django_ical/feedgenerator.py +++ b/django_ical/feedgenerator.py @@ -27,7 +27,7 @@ http://www.ietf.org/rfc/rfc2445.txt """ -from icalendar import Calendar, Event +from icalendar import Calendar, Event, Todo from django.utils.feedgenerator import SyndicationFeed @@ -45,7 +45,7 @@ ), # See format here: http://www.rfc-editor.org/rfc/rfc2445.txt (sec 4.3.6) ) -ITEM_EVENT_FIELD_MAP = ( +ITEM_ELEMENT_FIELD_MAP = ( # 'item_guid' becomes 'unique_id' when passed to the SyndicationFeed ("unique_id", "uid"), ("title", "summary"), @@ -68,9 +68,15 @@ ("status", "status"), ("attendee", "attendee"), ("valarm", None), + # additional properties supported by the Todo class (VTODO calendar component). + # see https://icalendar.readthedocs.io/en/latest/_modules/icalendar/cal.html#Todo + ("completed", "completed"), + ("percent_complete", "percent-complete"), + ("priority", "priority"), + ("due", "due"), + ("categories", "categories"), ) - class ICal20Feed(SyndicationFeed): """ iCalendar 2.0 Feed implementation. @@ -101,22 +107,26 @@ def write(self, outfile, encoding): def write_items(self, calendar): """ - Write all events to the calendar + Write all elements to the calendar """ for item in self.items: - event = Event() - for ifield, efield in ITEM_EVENT_FIELD_MAP: + component_type = item.get("component_type") + if component_type == "todo": + element = Todo() + else: + element = Event() + for ifield, efield in ITEM_ELEMENT_FIELD_MAP: val = item.get(ifield) if val is not None: if ifield == "attendee": for list_item in val: - event.add(efield, list_item) + element.add(efield, list_item) elif ifield == "valarm": for list_item in val: - event.add_component(list_item) + element.add_component(list_item) else: - event.add(efield, val) - calendar.add_component(event) + element.add(efield, val) + calendar.add_component(element) DefaultFeed = ICal20Feed diff --git a/django_ical/tests/test_feed.py b/django_ical/tests/test_feed.py index 578aa53..b2e111d 100644 --- a/django_ical/tests/test_feed.py +++ b/django_ical/tests/test_feed.py @@ -29,6 +29,7 @@ class TestItemsFeed(ICalFeed): def items(self): return [ { + "component_type": "event", "title": "Title1", "description": "Description1", "link": "/event/1", @@ -73,6 +74,7 @@ def items(self): "modified": datetime(2012, 5, 2, 10, 0), }, { + "component_type": "event", "title": "Title2", "description": "Description2", "link": "/event/2", @@ -108,8 +110,31 @@ def items(self): }, ], }, + { + "component_type": "todo", + "title": "Submit Revised Internet-Draft", + "description": "an important test", + "link": "/event/3", + "start": datetime(2007, 5, 14, 0), + "due": datetime(2007, 5, 16, 0), + "completed": datetime(2007, 3, 20), + "priority": 1, + "status": "NEEDS-ACTION", + "organizer": { + "cn": "Bossy Martin", + "email": "bossy.martin@example.com", + "role": "CHAIR" + }, + "modified": datetime(2012, 5, 2, 10, 0), + "geolocation": (37.386013, 2.238985), + "categories": ['CLEANING'], + "percent_complete": 89, + }, ] + def item_component_type(self, obj): + return obj.get("component_type", None) + def item_title(self, obj): return obj["title"] @@ -120,19 +145,22 @@ def item_start_datetime(self, obj): return obj["start"] def item_end_datetime(self, obj): - return obj["end"] + return obj.get("end", None) + + def item_due_datetime(self, obj): + return obj.get("due", None) def item_rrule(self, obj): - return obj["recurrences"]["rrules"] + return obj.get("recurrences", {}).get("rrules", None) def item_exrule(self, obj): - return obj["recurrences"]["xrules"] + return obj.get("recurrences", {}).get("xrules", None) def item_rdate(self, obj): - return obj["recurrences"]["rdates"] + return obj.get("recurrences", {}).get("rdates", None) def item_exdate(self, obj): - return obj["recurrences"]["xdates"] + return obj.get("recurrences", {}).get("xdates", None) def item_link(self, obj): return obj["link"] @@ -146,6 +174,21 @@ def item_updateddate(self, obj): def item_pubdate(self, obj): return obj.get("modified", None) + def item_completed(self, obj): + return obj.get("completed", None) + + def item_percent_complete(self, obj): + return obj.get("percent_complete", None) + + def item_priority(self, obj): + return obj.get("priority", None) + + def item_due(self, obj): + return obj.get("due", None) + + def item_categories(self, obj): + return obj.get("categories") or [] + def item_organizer(self, obj): organizer_dic = obj.get("organizer", None) if organizer_dic: @@ -230,7 +273,7 @@ def test_items(self): response = view(request) calendar = icalendar.Calendar.from_ical(response.content) - self.assertEqual(len(calendar.subcomponents), 2) + self.assertEqual(len(calendar.subcomponents), 3) self.assertEqual(calendar.subcomponents[0]["SUMMARY"], "Title1") self.assertEqual(calendar.subcomponents[0]["DESCRIPTION"], "Description1") @@ -342,6 +385,34 @@ def test_items(self): [comp.to_ical() for comp in calendar.subcomponents[1].subcomponents], ) + self.assertEqual(calendar.subcomponents[2]["SUMMARY"], "Submit Revised Internet-Draft") + self.assertTrue(calendar.subcomponents[2]["URL"].endswith("/event/3")) + self.assertEqual( + calendar.subcomponents[2]["DTSTART"].to_ical(), b"20070514T000000" + ) + self.assertEqual( + calendar.subcomponents[2]["DUE"].to_ical(), b"20070516T000000" + ) + self.assertEqual( + calendar.subcomponents[2]["GEO"].to_ical(), "37.386013;2.238985" + ) + self.assertEqual( + calendar.subcomponents[2]["LAST-MODIFIED"].to_ical(), b"20120502T100000Z" + ) + self.assertEqual( + calendar.subcomponents[2]["ORGANIZER"].to_ical(), + b"MAILTO:bossy.martin@example.com", + ) + self.assertEqual( + calendar.subcomponents[2]["PRIORITY"].to_ical(), b"1" + ) + self.assertEqual( + calendar.subcomponents[2]["CATEGORIES"][0].to_ical(), b"CLEANING" + ) + self.assertEqual( + calendar.subcomponents[2]["PERCENT-COMPLETE"].to_ical(), b"89" + ) + def test_wr_timezone(self): """ Test for the x-wr-timezone property. diff --git a/django_ical/views.py b/django_ical/views.py index 7078531..1d2f8b9 100644 --- a/django_ical/views.py +++ b/django_ical/views.py @@ -22,6 +22,7 @@ # Extra fields added to items (events) to # support ical ICAL_EXTRA_FIELDS = [ + "component_type", # type of calendar component (event, todo) "timestamp", # dtstamp "created", # created "start_datetime", # dtstart @@ -36,7 +37,12 @@ "exdate", # exdate "status", # CONFIRMED|TENTATIVE|CANCELLED "attendee", # list of attendees - "valarm", # list of icalendar.Alarm objects + "valarm", # list of icalendar.Alarm objects, + # additional fields for tasks + "completed", # completed + "percent_complete", # percent-complete + "priority", # priority + "due", # due ] diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 357196d..b0c2f09 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -160,6 +160,46 @@ Alarms must be `icalendar.Alarm` objects, a list is expected as the return value return [valarm] +Tasks (Todos) +------------- + +It is also possible to generate representations of tasks (or deadlines, todos) +which are represented in iCal with the dedicated `VTODO` component instead of the usual `VEVENT`. + +To do so, you can use a specific method to determine which type of component a given item +should be translated as: + +.. code-block:: python + + from django_ical.views import ICalFeed + from examplecom.models import Deadline + + class EventFeed(ICalFeed): + """ + A simple event calender with tasks + """ + product_id = '-//example.com//Example//EN' + timezone = 'UTC' + file_name = "event.ics" + + def items(self): + return Deadline.objects.all().order_by('-due_datetime') + + def item_component_type(self): + return 'todo' # could also be 'event', which is the default + + def item_title(self, item): + return item.title + + def item_description(self, item): + return item.description + + def item_due_datetime(self, item): + return item.due_datetime + + + + Property Reference and Extensions --------------------------------- @@ -264,6 +304,23 @@ Here is a table of all of the fields that django-ical supports. | | | Can be CONFIRMED, CANCELLED | | | | or TENTATIVE. | +-----------------------+-----------------------+-----------------------------+ +| item_completed | `COMPLETED`_ | The date a task was | +| | | completed. | ++-----------------------+-----------------------+-----------------------------+ +| item_percent_complete | `PERCENT-COMPLETE`_ | A number from 0 to 100 | +| | | indication the completion | +| | | of the task. | ++-----------------------+-----------------------+-----------------------------+ +| item_priority | `PRIORITY`_ | An integer from 0 to 9. | +| | | 0 means undefined. | +| | | 1 means highest priority. | ++-----------------------+-----------------------+-----------------------------+ +| item_due | `DUE` _ | The date a task is due. | ++-----------------------+-----------------------+-----------------------------+ +| item_categories | `CATEGORIES`_ | A list of strings, each | +| | | being a category of the | +| | | task. | ++-----------------------+-----------------------+-----------------------------+ .. note::