From 58e9ddf52191cf6fe72fc12460ce8fa862fdda1a Mon Sep 17 00:00:00 2001 From: Annie Cook Date: Tue, 3 Dec 2019 13:38:20 -0800 Subject: [PATCH] Modify the CalendarItem logic to better account for all day events --- exchangelib/fields.py | 62 ++++++++++++++++++++++++++++++++++--------- exchangelib/items.py | 11 +++++--- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/exchangelib/fields.py b/exchangelib/fields.py index 83ed92c7..fa6ed7d5 100644 --- a/exchangelib/fields.py +++ b/exchangelib/fields.py @@ -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'): @@ -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 @@ -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 diff --git a/exchangelib/items.py b/exchangelib/items.py index e229c429..9018339c 100644 --- a/exchangelib/items.py +++ b/exchangelib/items.py @@ -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 @@ -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, @@ -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: