Skip to content

Commit

Permalink
Merge pull request #39 from nylas/all-day-event-handling
Browse files Browse the repository at this point in the history
Modify the CalendarItem logic to better account for all day events
  • Loading branch information
annielcook authored Dec 5, 2019
2 parents caa011c + 58e9ddf commit f3fb7df
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 16 deletions.
62 changes: 49 additions & 13 deletions exchangelib/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ def clean(self, value, version=None):
if hasattr(v, 'clean'):
v.clean(version=version)
else:
if isinstance(self, GenericDateField):
return value
if not isinstance(value, self.value_cls):
raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
if hasattr(value, 'clean'):
Expand Down Expand Up @@ -529,31 +531,51 @@ def to_xml(self, value, version):
return set_xml_value(field_elem, base64.b64encode(value).decode('ascii'), version=version)


class DateField(FieldURIField):
value_cls = EWSDate

class GenericDateField(FieldURIField):
"""
This class is a generic field to use when a field can be either a DateField or
a DateTimeField on a CalendarItem. Using this class allows either type to be used
and handles writing the appropriate XML based on whether the `is_all_day` field
on the CalendarItem is true, in which case it uses an EWSDate, else it uses an
EWSDateTime.
"""
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
is_all_day = elem.find('{%s}IsAllDayEvent' % TNS)
if val is not None:
try:
return self.value_cls.from_string(val)
except ValueError:
log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
if is_all_day is True:
try:
return EWSDate.from_string(val)
except ValueError:
log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
else:
try:
return EWSDateTime.from_string(val)
except ValueError as e:
if isinstance(e, NaiveDateTimeNotAllowed):
# We encountered a naive datetime. Convert to timezone-aware datetime using the default
# timezone of the account.
local_dt = e.args[0]
tz = account.default_timezone if account else UTC
log.info('Found naive datetime %s on field %s. Assuming timezone %s', local_dt, self.name, tz)
return tz.localize(local_dt)
log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
return self.default


class TimeField(FieldURIField):
value_cls = datetime.time
class DateField(FieldURIField):
value_cls = EWSDate

def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None:
try:
# Assume an integer in minutes since midnight
return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
return self.value_cls.from_string(val)
except ValueError:
pass
log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
return self.default


Expand Down Expand Up @@ -583,6 +605,20 @@ def from_xml(self, elem, account):
return self.default


class TimeField(FieldURIField):
value_cls = datetime.time

def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None:
try:
# Assume an integer in minutes since midnight
return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
except ValueError:
pass
return self.default


class TimeZoneField(FieldURIField):
value_cls = EWSTimeZone

Expand Down
11 changes: 8 additions & 3 deletions exchangelib/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
PhysicalAddressField, ExtendedPropertyField, AttachmentField, RecurrenceField, MailboxField, MailboxListField, \
AttendeesField, Choice, OccurrenceField, OccurrenceListField, MemberListField, EWSElementField, \
EffectiveRightsField, TimeZoneField, CultureField, IdField, CharField, TextListField, EnumAsIntField, \
EmailAddressField, FreeBusyStatusField, ReferenceItemIdField, AssociatedCalendarItemIdField, OccurrenceItemIdField
EmailAddressField, FreeBusyStatusField, ReferenceItemIdField, AssociatedCalendarItemIdField, OccurrenceItemIdField, \
GenericDateField
from .properties import EWSElement, ItemId, ConversationId, ParentFolderId, Attendee, ReferenceItemId, \
AssociatedCalendarItemId, PersonaId
from .recurrence import FirstOccurrence, LastOccurrence, Occurrence, DeletedOccurrence
Expand Down Expand Up @@ -520,8 +521,8 @@ class CalendarItem(Item):
FIELDS = Item.FIELDS + [
OccurrenceItemIdField('occurrence_item_id', field_uri='calendar:OccurrenceItemId', is_syncback_only=True),
TextField('uid', field_uri='calendar:UID', is_required_after_save=True, is_searchable=False),
DateTimeField('start', field_uri='calendar:Start', is_required=True),
DateTimeField('end', field_uri='calendar:End', is_required=True),
GenericDateField('start', field_uri='calendar:Start', is_required=True),
GenericDateField('end', field_uri='calendar:End', is_required=True),
DateTimeField('original_start', field_uri='calendar:OriginalStart', is_read_only=True),
BooleanField('is_all_day', field_uri='calendar:IsAllDayEvent', is_required=True, default=False),
FreeBusyStatusField('legacy_free_busy_status', field_uri='calendar:LegacyFreeBusyStatus', is_required=True,
Expand Down Expand Up @@ -579,12 +580,16 @@ class CalendarItem(Item):
URIField('net_show_url', field_uri='calendar:NetShowUrl'),
]


@classmethod
def timezone_fields(cls):
return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

def clean_timezone_fields(self, version):
# pylint: disable=access-member-before-definition
if self.is_all_day is True:
# No timezones for all day events!
return
# Sets proper values on the timezone fields if they are not already set
if version.build < EXCHANGE_2010:
if self._meeting_timezone is None and self.start is not None:
Expand Down

0 comments on commit f3fb7df

Please sign in to comment.