Skip to content

Commit

Permalink
Support for tasks (vtodo) elements. Closes #59
Browse files Browse the repository at this point in the history
  • Loading branch information
wetneb authored and aleksihakli committed Apr 4, 2023
1 parent 210efd1 commit 0c32c51
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 17 deletions.
30 changes: 20 additions & 10 deletions django_ical/feedgenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"),
Expand All @@ -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.
Expand Down Expand Up @@ -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
83 changes: 77 additions & 6 deletions django_ical/tests/test_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 29,7 @@ class TestItemsFeed(ICalFeed):
def items(self):
return [
{
"component_type": "event",
"title": "Title1",
"description": "Description1",
"link": "/event/1",
Expand Down Expand Up @@ -73,6 74,7 @@ def items(self):
"modified": datetime(2012, 5, 2, 10, 0),
},
{
"component_type": "event",
"title": "Title2",
"description": "Description2",
"link": "/event/2",
Expand Down Expand Up @@ -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": "[email protected]",
"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"]

Expand All @@ -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"]
Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:[email protected]",
)
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.
Expand Down
8 changes: 7 additions & 1 deletion django_ical/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
]


Expand Down
57 changes: 57 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------------------

Expand Down Expand Up @@ -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::
Expand Down

0 comments on commit 0c32c51

Please sign in to comment.