Skip to content

Commit

Permalink
Merge pull request #15 from ematthews/master
Browse files Browse the repository at this point in the history
Add list_events capability to search between two dates
  • Loading branch information
Rachel Sanders committed Oct 15, 2014
2 parents f9eb9f0 + d8bdf1d commit 890e7b8
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 17 deletions.
25 changes: 25 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,31 @@ If the id doesn't match anything in Exchange, a ``pyexchange.exceptions.Exchange

For all other errors, we throw a ``pyexchange.exceptions.FailedExchangeException``.

Listing events
``````````````

To list events between two dates, simply do:

events = my_calendar.list_events(
start=datetime(2014, 10, 1, 11, 0, 0, tzinfo=timezone("US/Eastern")),
end=datetime(2014, 10, 29, 11, 0, 0, tzinfo=timezone("US/Eastern")),
details=True
)

This will return a list of Event objects that are between start and end. If no results are found, it will return an empty list (it intentionally will not throw an Exception.)

for event in calendar_list.events:
print "{start} {stop} - {subject}".format(
start=event.start,
stop=event.end,
subject=event.subject
)

The third argument, 'details', is optional. By default (if details is not specified, or details=False), it will return most of the fields within an event. The full details for the Organizer or Attendees field are not populated by default by Exchange. If these fields are required in your usage, then pass details=True with the request to make a second lookup for these values. The further details can also be loaded after the fact using the load_all_details() function, as below:

events = my_calendar.list_events(start, end)
events.load_all_details()

Cancelling an event
```````````````````

Expand Down
10 changes: 8 additions & 2 deletions pyexchange/base/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,13 @@ class BaseExchangeCalendarEvent(object):
DATA_ATTRIBUTES = [u'_id', u'subject', u'start', u'end', u'location', u'html_body', u'text_body', u'organizer',
u'_attendees', u'_resources', u'reminder_minutes_before_start', u'is_all_day']

def __init__(self, service, id=None, calendar_id=u'calendar', **kwargs):
def __init__(self, service, id=None, calendar_id=u'calendar', xml=None, **kwargs):
self.service = service
self.calendar_id = calendar_id

if id is None:
if xml is not None:
self._init_from_xml(xml)
elif id is None:
self._update_properties(kwargs)
else:
self._init_from_service(id)
Expand All @@ -84,6 +86,10 @@ def _init_from_service(self, id):
""" Connect to the Exchange service and grab all the properties out of it. """
raise NotImplementedError

def _init_from_xml(self, xml):
""" Using already retrieved XML from Exchange, extract properties out of it. """
raise NotImplementedError

@property
def id(self):
""" **Read-only.** The internal id Exchange uses to refer to this event. """
Expand Down
1 change: 1 addition & 0 deletions pyexchange/base/soap.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def _check_for_SOAP_fault(self, xml_tree):

if fault_nodes:
fault = fault_nodes[0]
log.debug(etree.tostring(fault, pretty_print=True))
raise FailedExchangeException(u"SOAP Fault from Exchange server", fault.text)

def _send_soap_request(self, xml, headers=None, retries=2, timeout=30, encoding="utf-8"):
Expand Down
127 changes: 113 additions & 14 deletions pyexchange/exchange2010/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,117 @@ def get_event(self, id):
def new_event(self, **properties):
return Exchange2010CalendarEvent(service=self.service, calendar_id=self.calendar_id, **properties)

def list_events(self, start=None, end=None, details=False):
return Exchange2010CalendarEventList(service=self.service, start=start, end=end, details=details)


class Exchange2010CalendarEventList(object):
"""
Creates & Stores a list of Exchange2010CalendarEvent items in the "self.events" variable.
"""
def __init__(self, service=None, start=None, end=None, details=False):
self.service = service
self.count = 0
self.start = start
self.end = end
self.events = list()
self.event_ids = list()
self.details = details

# This request uses a Calendar-specific query between two dates.
body = soap_request.get_calendar_items(format=u'AllProperties', start=self.start, end=self.end)
response_xml = self.service.send(body)
self._parse_response_for_all_events(response_xml)

# Populate the event ID list, for convenience reasons.
for event in self.events:
self.event_ids.append(event._id)

# If we have requested all the details, basically repeat the previous 3 steps,
# but instead of start/stop, we have a list of ID fields.
if self.details:
log.debug(u'Received request for all details, retrieving now!')
self.load_all_details()
return

def _parse_response_for_all_events(self, response):
"""
This function will retrieve *most* of the event data, excluding Organizer & Attendee details
"""
calendar_items = response.xpath(u'//t:CalendarItem', namespaces=soap_request.NAMESPACES)
counter = 0
if calendar_items:
self.count = len(calendar_items)
log.debug(u'Found %s items' % self.count)

for item in calendar_items:
# Skip matches of CalendarItem that are contained within other CalenderItems, particularly
# the fact that "<CalendarItem>" tags are located within "<ConflictingMeetings>"
if item.getparent().tag.endswith('ConflictingMeetings'):
continue

self._add_event(xml=item)
else:
log.debug(u'No calendar items found with search parameters.')

return self

def _add_event(self, xml=None):
log.debug(u'Adding new event to all events list.')
event = Exchange2010CalendarEvent(service=self.service, xml=xml)
log.debug(u'Subject of new event is %s' % event.subject)
self.events.append(event)
return self

def load_all_details(self):
"""
This function will execute all the event lookups for known events.
This is intended for use when you want to have a completely populated event entry, including
Organizer & Attendee details.
"""
log.debug(u"Loading all details")
if self.count > 0:
# Now, empty out the events to prevent duplicates!
del(self.events[:])

# Send the SOAP request with the list of exchange ID values.
log.debug(u"Requesting all event details for events: {event_list}".format(event_list=str(self.event_ids)))
body = soap_request.get_item(exchange_id=self.event_ids, format=u'AllProperties')
response_xml = self.service.send(body)

# Re-parse the results for all the details!
self._parse_response_for_all_events(response_xml)

return self


class Exchange2010CalendarEvent(BaseExchangeCalendarEvent):

def _init_from_service(self, id):

log.debug(u'Creating new Exchange2010CalendarEvent object from ID')
self.xpath_root = u'//m:Items/t:CalendarItem'
body = soap_request.get_item(exchange_id=id, format=u'AllProperties')
response_xml = self.service.send(body)
properties = self._parse_response_for_get_event(response_xml)

self._update_properties(properties)
self._id = id
log.debug(u'Created new event object with ID: %s' % self._id)

self._reset_dirty_attributes()

return self

def _init_from_xml(self, xml=None):
log.debug(u'Creating new Exchange2010CalendarEvent object from XML')
self.xpath_root = u'.'
properties = self._parse_event_properties(xml)

self._update_properties(properties)
self._id = xml.xpath(u'./t:ItemId/@Id', namespaces=soap_request.NAMESPACES)[0]

log.debug(u'Created new event object with ID: %s' % self._id)
self._reset_dirty_attributes()

return self
Expand Down Expand Up @@ -278,22 +377,22 @@ def _parse_response_for_get_event(self, response):
def _parse_event_properties(self, response):

property_map = {
u'subject' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Subject'}, # noqa
u'location' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Location'}, # noqa
u'availability' : { u'xpath' : u'//m:Items/t:CalendarItem/t:LegacyFreeBusyStatus'}, # noqa
u'start' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Start', u'cast': u'datetime'}, # noqa
u'end' : { u'xpath' : u'//m:Items/t:CalendarItem/t:End', u'cast': u'datetime'}, # noqa
u'html_body' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Body[@BodyType="HTML"]'}, # noqa
u'text_body' : { u'xpath' : u'//m:Items/t:CalendarItem/t:Body[@BodyType="Text"]'}, # noqa
u'reminder_minutes_before_start' : { u'xpath' : u'//m:Items/t:CalendarItem/t:ReminderMinutesBeforeStart', u'cast': u'int'}, # noqa
u'is_all_day' : { u'xpath' : u'//m:Items/t:CalendarItem/t:IsAllDayEvent', u'cast': u'bool'}, # noqa
u'subject' : { u'xpath' : self.xpath_root + u'/t:Subject'}, # noqa
u'location' : { u'xpath' : self.xpath_root + u'/t:Location'}, # noqa
u'availability' : { u'xpath' : self.xpath_root + u'/t:LegacyFreeBusyStatus'}, # noqa
u'start' : { u'xpath' : self.xpath_root + u'/t:Start', u'cast': u'datetime'}, # noqa
u'end' : { u'xpath' : self.xpath_root + u'/t:End', u'cast': u'datetime'}, # noqa
u'html_body' : { u'xpath' : self.xpath_root + u'/t:Body[@BodyType="HTML"]'}, # noqa
u'text_body' : { u'xpath' : self.xpath_root + u'/t:Body[@BodyType="Text"]'}, # noqa
u'reminder_minutes_before_start' : { u'xpath' : self.xpath_root + u'/t:ReminderMinutesBeforeStart', u'cast': u'int'}, # noqa
u'is_all_day' : { u'xpath' : self.xpath_root + u'/t:IsAllDayEvent', u'cast': u'bool'}, # noqa
}

return self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)

def _parse_event_organizer(self, response):

organizer = response.xpath(u'//m:Items/t:CalendarItem/t:Organizer/t:Mailbox', namespaces=soap_request.NAMESPACES)
organizer = response.xpath(self.xpath_root + u'/t:Organizer/t:Mailbox', namespaces=soap_request.NAMESPACES)

property_map = {
u'name' : { u'xpath' : u't:Name'}, # noqa
Expand All @@ -315,7 +414,7 @@ def _parse_event_resources(self, response):

result = []

resources = response.xpath(u'//m:Items/t:CalendarItem/t:Resources/t:Attendee', namespaces=soap_request.NAMESPACES)
resources = response.xpath(self.xpath_root + u'/t:Resources/t:Attendee', namespaces=soap_request.NAMESPACES)

for attendee in resources:
attendee_properties = self.service._xpath_to_dict(element=attendee, property_map=property_map, namespace_map=soap_request.NAMESPACES)
Expand All @@ -339,7 +438,7 @@ def _parse_event_attendees(self, response):

result = []

required_attendees = response.xpath(u'//m:Items/t:CalendarItem/t:RequiredAttendees/t:Attendee', namespaces=soap_request.NAMESPACES)
required_attendees = response.xpath(self.xpath_root + u'/t:RequiredAttendees/t:Attendee', namespaces=soap_request.NAMESPACES)
for attendee in required_attendees:
attendee_properties = self.service._xpath_to_dict(element=attendee, property_map=property_map, namespace_map=soap_request.NAMESPACES)
attendee_properties[u'required'] = True
Expand All @@ -349,7 +448,7 @@ def _parse_event_attendees(self, response):

result.append(attendee_properties)

optional_attendees = response.xpath(u'//m:Items/t:CalendarItem/t:OptionalAttendees/t:Attendee', namespaces=soap_request.NAMESPACES)
optional_attendees = response.xpath(self.xpath_root + u'/t:OptionalAttendees/t:Attendee', namespaces=soap_request.NAMESPACES)

for attendee in optional_attendees:
attendee_properties = self.service._xpath_to_dict(element=attendee, property_map=property_map, namespace_map=soap_request.NAMESPACES)
Expand Down
28 changes: 27 additions & 1 deletion pyexchange/exchange2010/soap_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,42 @@ def get_item(exchange_id, format=u"Default"):
"""

elements = list()
if type(exchange_id) == list:
for item in exchange_id:
elements.append(T.ItemId(Id=item))
else:
elements = [T.ItemId(Id=exchange_id)]

root = M.GetItem(
M.ItemShape(
T.BaseShape(format)
),
M.ItemIds(
T.ItemId(Id=exchange_id)
*elements
)
)
return root

def get_calendar_items(format=u"Default", start=None, end=None, max_entries=999999):
start = start.strftime(EXCHANGE_DATE_FORMAT)
end = end.strftime(EXCHANGE_DATE_FORMAT)

root = M.FindItem(
{u'Traversal': u'Shallow'},
M.ItemShape(
T.BaseShape(format)
),
M.CalendarView({
u'MaxEntriesReturned': unicode(max_entries),
u'StartDate': start,
u'EndDate': end,
}),
M.ParentFolderIds(T.DistinguishedFolderId(Id=u"calendar")),
)

return root


def get_folder(folder_id, format=u"Default"):

Expand Down
Loading

0 comments on commit 890e7b8

Please sign in to comment.