Skip to content

Commit

Permalink
Merge pull request #25 from txstate/master
Browse files Browse the repository at this point in the history
Support for getting conflicting events
  • Loading branch information
Rachel Sanders committed Nov 3, 2014
2 parents 894c569 + 0221724 commit aaf464d
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 12 deletions.
6 changes: 5 additions & 1 deletion docs/exchange2010.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Exchange2010CalendarEvent
=========================

.. autoclass:: Exchange2010CalendarEvent
:members: create, update, cancel, resend_invitations, move_to, get_occurrence, get_master
:members: create, update, cancel, resend_invitations, move_to, conflicting_events, get_occurrence, get_master

.. attribute:: id

Expand Down Expand Up @@ -141,6 +141,10 @@ Exchange2010CalendarEvent
Used in a weekly recurrence to specify which days of the week to schedule the event. This should be a
string of days separated by spaces. ex. "Monday Wednesday"

.. attribute:: conflicting_event_ids

**Read-only.** The internal id Exchange uses to refer to conflicting events.

.. method:: add_attendee(attendees, required=True)

Adds new attendees to the event.
Expand Down
15 changes: 10 additions & 5 deletions pyexchange/base/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@
"""
from collections import namedtuple

try:
import simplejson as json
except ImportError:
import json

ExchangeEventOrganizer = namedtuple('ExchangeEventOrganizer', ['name', 'email'])
ExchangeEventAttendee = namedtuple('ExchangeEventAttendee', ['name', 'email', 'required'])
ExchangeEventResponse = namedtuple('ExchangeEventResponse', ['name', 'email', 'response', 'last_response', 'required'])
Expand Down Expand Up @@ -69,6 +64,8 @@ class BaseExchangeCalendarEvent(object):
_attendees = {} # people attending
_resources = {} # conference rooms attending

_conflicting_event_ids = []

_track_dirty_attributes = False
_dirty_attributes = set() # any attributes that have changed, and we need to update in Exchange

Expand Down Expand Up @@ -111,6 +108,11 @@ def id(self):
""" **Read-only.** The internal id Exchange uses to refer to this event. """
return self._id

@property
def conflicting_event_ids(self):
""" **Read-only.** The internal id Exchange uses to refer to conflicting events. """
return self._conflicting_event_ids

@property
def change_key(self):
""" **Read-only.** When you change an event, Exchange makes you pass a change key to prevent overwriting a previous version. """
Expand Down Expand Up @@ -335,6 +337,9 @@ def get_master(self):
def get_occurrance(self, instance_index):
raise NotImplementedError

def conflicting_events(self):
raise NotImplementedError

def as_json(self):
""" Output ourselves as JSON """
raise NotImplementedError
Expand Down
35 changes: 35 additions & 0 deletions pyexchange/exchange2010/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,35 @@ def get_occurrence(self, instance_index):

return events

def conflicting_events(self):
"""
conflicting_events()
This will return a list of conflicting events.
**Example**::
event = service.calendar().get_event(id='<event_id>')
for conflict in event.conflicting_events():
print conflict.subject
"""

if not self.conflicting_event_ids:
return []

body = soap_request.get_item(exchange_id=self.conflicting_event_ids, format="AllProperties")
response_xml = self.service.send(body)

items = response_xml.xpath(u'//m:GetItemResponseMessage/m:Items', namespaces=soap_request.NAMESPACES)
events = []
for item in items:
event = Exchange2010CalendarEvent(service=self.service, xml=deepcopy(item))
if event.id:
events.append(event)

return events

def refresh_change_key(self):

body = soap_request.get_item(exchange_id=self._id, format=u"IdOnly")
Expand Down Expand Up @@ -474,6 +503,8 @@ def _parse_response_for_get_event(self, response):
resource_properties = self._parse_event_resources(response)
result[u'_resources'] = self._build_resource_dictionary([ExchangeEventResponse(**resource) for resource in resource_properties])

result['_conflicting_event_ids'] = self._parse_event_conflicts(response)

return result

def _parse_event_properties(self, response):
Expand Down Expand Up @@ -664,6 +695,10 @@ def _parse_event_attendees(self, response):

return result

def _parse_event_conflicts(self, response):
conflicting_ids = response.xpath(u'//m:Items/t:CalendarItem/t:ConflictingMeetings/t:CalendarItem/t:ItemId', namespaces=soap_request.NAMESPACES)
return [id_element.get(u"Id") for id_element in conflicting_ids]


class Exchange2010FolderService(BaseExchangeFolderService):

Expand Down
121 changes: 115 additions & 6 deletions tests/exchange2010/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@
end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
body=u'rärr ï äm ä dïnösäür')

TEST_CONFLICT_EVENT = EventFixture(
id=u'aabbccddeeff',
change_key=u'gghhiijjkkllmm',
calendar_id='calendar',
subject=u'mч cσnflíctíng єvєnt',
location=u'söüth päċïfïċ (40.1°S 123.7°W)',
start=datetime(year=2050, month=5, day=20, hour=20, minute=42, second=50, tzinfo=utc),
end=datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51, tzinfo=utc),
body=u'rärr ï äm ä dïnösäür',
)

TEST_EVENT_LIST_START = datetime(year=2050, month=4, day=20, hour=20, minute=42, second=50)
TEST_EVENT_LIST_END = datetime(year=2050, month=5, day=20, hour=21, minute=43, second=51)

Expand Down Expand Up @@ -330,12 +341,12 @@
<t:AdjacentMeetingCount>1</t:AdjacentMeetingCount>
<t:ConflictingMeetings>
<t:CalendarItem>
<t:ItemId Id="rarrrrr" ChangeKey="blarg"/>
<t:Subject>My other awesome event</t:Subject>
<t:Start>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:Start>
<t:End>{event.end:%Y-%m-%dT%H:%M:%SZ}</t:End>
<t:ItemId Id="{conflict_event.id}" ChangeKey="{conflict_event.change_key}"/>
<t:Subject>{conflict_event.subject}</t:Subject>
<t:Start>{conflict_event.start:%Y-%m-%dT%H:%M:%SZ}</t:Start>
<t:End>{conflict_event.end:%Y-%m-%dT%H:%M:%SZ}</t:End>
<t:LegacyFreeBusyStatus>Busy</t:LegacyFreeBusyStatus>
<t:Location>Nowhere special</t:Location>
<t:Location>{conflict_event.location}</t:Location>
</t:CalendarItem>
</t:ConflictingMeetings>
<t:AdjacentMeetings>
Expand Down Expand Up @@ -369,9 +380,107 @@
optional_tentative=PERSON_OPTIONAL_TENTATIVE,
optional_declined=PERSON_OPTIONAL_DECLINED,
optional_unknown=PERSON_OPTIONAL_UNKNOWN,
resource=RESOURCE
resource=RESOURCE,
conflict_event=TEST_CONFLICT_EVENT,
)

CONFLICTING_EVENTS_RESPONSE = u"""<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<h:ServerVersionInfo xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" MajorVersion="14" MinorVersion="2" MajorBuildNumber="328" MinorBuildNumber="11"/>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<m:GetItemResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<m:ResponseMessages>
<m:GetItemResponseMessage ResponseClass="Success">
<m:ResponseCode>NoError</m:ResponseCode>
<m:Items>
<t:CalendarItem>
<t:ItemId Id="{event.id}" ChangeKey="{event.change_key}"/>
<t:ParentFolderId Id="fooo" ChangeKey="bar"/>
<t:ItemClass>IPM.Appointment</t:ItemClass>
<t:Subject>{event.subject}</t:Subject>
<t:Sensitivity>Normal</t:Sensitivity>
<t:Body BodyType="HTML">{event.body}</t:Body>
<t:Body BodyType="Text">{event.body}</t:Body>
<t:DateTimeReceived>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:DateTimeReceived>
<t:Size>1935</t:Size>
<t:Importance>Normal</t:Importance>
<t:IsSubmitted>false</t:IsSubmitted>
<t:IsDraft>false</t:IsDraft>
<t:IsFromMe>false</t:IsFromMe>
<t:IsResend>false</t:IsResend>
<t:IsUnmodified>false</t:IsUnmodified>
<t:DateTimeSent>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:DateTimeSent>
<t:DateTimeCreated>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:DateTimeCreated>
<t:ResponseObjects>
<t:CancelCalendarItem/>
<t:ForwardItem/>
</t:ResponseObjects>
<t:ReminderDueBy>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:ReminderDueBy>
<t:ReminderIsSet>true</t:ReminderIsSet>
<t:ReminderMinutesBeforeStart>15</t:ReminderMinutesBeforeStart>
<t:DisplayCc/>
<t:DisplayTo/>
<t:HasAttachments>false</t:HasAttachments>
<t:Culture>en-US</t:Culture>
<t:Start>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:Start>
<t:End>{event.end:%Y-%m-%dT%H:%M:%SZ}</t:End>
<t:IsAllDayEvent>false</t:IsAllDayEvent>
<t:LegacyFreeBusyStatus>Busy</t:LegacyFreeBusyStatus>
<t:Location>{event.location}</t:Location>
<t:IsMeeting>true</t:IsMeeting>
<t:IsCancelled>false</t:IsCancelled>
<t:IsRecurring>false</t:IsRecurring>
<t:MeetingRequestWasSent>false</t:MeetingRequestWasSent>
<t:IsResponseRequested>true</t:IsResponseRequested>
<t:CalendarItemType>Single</t:CalendarItemType>
<t:MyResponseType>Organizer</t:MyResponseType>
<t:Organizer>
<t:Mailbox>
<t:Name>{organizer.name}</t:Name>
<t:EmailAddress>{organizer.email}</t:EmailAddress>
<t:RoutingType>SMTP</t:RoutingType>
</t:Mailbox>
</t:Organizer>
<t:ConflictingMeetingCount>1</t:ConflictingMeetingCount>
<t:AdjacentMeetingCount>1</t:AdjacentMeetingCount>
<t:ConflictingMeetings>
<t:CalendarItem>
<t:ItemId Id="{conflict_event.id}" ChangeKey="{conflict_event.change_key}"/>
<t:Subject>{conflict_event.subject}</t:Subject>
<t:Start>{conflict_event.start:%Y-%m-%dT%H:%M:%SZ}</t:Start>
<t:End>{conflict_event.end:%Y-%m-%dT%H:%M:%SZ}</t:End>
<t:LegacyFreeBusyStatus>Busy</t:LegacyFreeBusyStatus>
<t:Location>{conflict_event.location}</t:Location>
</t:CalendarItem>
</t:ConflictingMeetings>
<t:AdjacentMeetings>
<t:CalendarItem>
<t:ItemId Id="dinosaur" ChangeKey="goesrarrr"/>
<t:Subject>my other OTHER awesome event</t:Subject>
<t:Start>{event.start:%Y-%m-%dT%H:%M:%SZ}</t:Start>
<t:End>{event.end:%Y-%m-%dT%H:%M:%SZ}</t:End>
<t:LegacyFreeBusyStatus>Busy</t:LegacyFreeBusyStatus>
<t:Location>Outside</t:Location>
</t:CalendarItem>
</t:AdjacentMeetings>
<t:Duration>PT1H</t:Duration>
<t:TimeZone>(UTC-08:00) Pacific Time (US &amp; Canada)</t:TimeZone>
<t:AppointmentSequenceNumber>0</t:AppointmentSequenceNumber>
<t:AppointmentState>1</t:AppointmentState>
</t:CalendarItem>
</m:Items>
</m:GetItemResponseMessage>
</m:ResponseMessages>
</m:GetItemResponse>
</s:Body>
</s:Envelope>
""".format(
event=TEST_CONFLICT_EVENT,
organizer=ORGANIZER,
conflict_event=TEST_EVENT,
)

GET_ITEM_RESPONSE_ID_ONLY = u"""<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Expand Down
53 changes: 53 additions & 0 deletions tests/exchange2010/test_get_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ def test_required_attendees_are_required(self):
def test_optional_attendees_are_optional(self):
assert sorted(self.event.optional_attendees) == sorted(OPTIONAL_PEOPLE)

def test_conflicting_event_ids(self):
assert self.event.conflicting_event_ids[0] == TEST_CONFLICT_EVENT.id

@httprettified
def test_conflicting_events(self):
HTTPretty.register_uri(
HTTPretty.POST, FAKE_EXCHANGE_URL,
body=CONFLICTING_EVENTS_RESPONSE.encode('utf-8'),
content_type='text/xml; charset=utf-8',
)
conflicting_events = self.event.conflicting_events()
assert conflicting_events[0].id == TEST_CONFLICT_EVENT.id
assert conflicting_events[0].calendar_id == TEST_CONFLICT_EVENT.calendar_id
assert conflicting_events[0].subject == TEST_CONFLICT_EVENT.subject
assert conflicting_events[0].location == TEST_CONFLICT_EVENT.location
assert conflicting_events[0].start == TEST_CONFLICT_EVENT.start
assert conflicting_events[0].end == TEST_CONFLICT_EVENT.end
assert conflicting_events[0].body == TEST_CONFLICT_EVENT.body
assert conflicting_events[0].conflicting_event_ids[0] == TEST_EVENT.id


class Test_FailingToGetEvents(unittest.TestCase):

Expand Down Expand Up @@ -150,6 +170,7 @@ def test_requesting_an_event_and_getting_garbage_xml_throws_exception(self):
with raises(FailedExchangeException):
self.service.calendar().get_event(id=TEST_EVENT.id)


class Test_GetRecurringMasterEvents(unittest.TestCase):
service = None
event = None
Expand Down Expand Up @@ -402,3 +423,35 @@ def test_get_master_fail_from_master(self):
master = self.event.get_master()
with raises(InvalidEventType):
master.get_master()


class Test_GetConflictingEventsEmpty(unittest.TestCase):
event = None

@classmethod
def setUpClass(self):

@activate # this decorator doesn't play nice with @classmethod
def fake_event_request():

service = Exchange2010Service(
connection=ExchangeNTLMAuthConnection(
url=FAKE_EXCHANGE_URL, username=FAKE_EXCHANGE_USERNAME, password=FAKE_EXCHANGE_PASSWORD
)
)

HTTPretty.register_uri(
HTTPretty.POST, FAKE_EXCHANGE_URL,
body=GET_RECURRING_MASTER_DAILY_EVENT.encode('utf-8'),
content_type='text/xml; charset=utf-8',
)

return service.calendar().get_event(id=TEST_EVENT.id)

self.event = fake_event_request()

def test_conflicting_event_ids_empty(self):
assert len(self.event.conflicting_event_ids) == 0

def test_conflicting_events_empty(self):
assert len(self.event.conflicting_events()) == 0

0 comments on commit aaf464d

Please sign in to comment.