Module exchangelib.services.update_item
Expand source code
from collections import OrderedDict
from .common import EWSAccountService, to_item_id
from ..ewsdatetime import EWSDate
from ..fields import FieldPath, IndexedField
from ..properties import ItemId
from ..util import create_element, set_xml_value, MNS
from ..version import EXCHANGE_2013_SP1
class UpdateItem(EWSAccountService):
"""MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation"""
SERVICE_NAME = 'UpdateItem'
element_container_name = '{%s}Items' % MNS
def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
suppress_read_receipts):
from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
raise ValueError("'conflict_resolution' %s must be one of %s" % (
conflict_resolution, CONFLICT_RESOLUTION_CHOICES
))
if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
raise ValueError("'message_disposition' %s must be one of %s" % (
message_disposition, MESSAGE_DISPOSITION_CHOICES
))
if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
))
if suppress_read_receipts not in (True, False):
raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
if message_disposition == SEND_ONLY:
raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
return self._elems_to_objs(self._chunked_get_elements(
self.get_payload,
items=items,
conflict_resolution=conflict_resolution,
message_disposition=message_disposition,
send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations,
suppress_read_receipts=suppress_read_receipts,
))
def _elems_to_objs(self, elems):
from ..items import Item
for elem in elems:
if isinstance(elem, (Exception, type(None))):
yield elem
continue
yield Item.id_from_xml(elem)
def _delete_item_elem(self, field_path):
deleteitemfield = create_element('t:DeleteItemField')
return set_xml_value(deleteitemfield, field_path, version=self.account.version)
def _set_item_elem(self, item_model, field_path, value):
setitemfield = create_element('t:SetItemField')
set_xml_value(setitemfield, field_path, version=self.account.version)
item_elem = create_element(item_model.request_tag())
field_elem = field_path.field.to_xml(value, version=self.account.version)
set_xml_value(item_elem, field_elem, version=self.account.version)
setitemfield.append(item_elem)
return setitemfield
@staticmethod
def _sorted_fields(item_model, fieldnames):
# Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS.
# Checks that all fieldnames are valid.
unique_fieldnames = list(OrderedDict.fromkeys(fieldnames)) # Make field names unique ,but keep ordering
# Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
for f in item_model.FIELDS:
if f.name in unique_fieldnames:
unique_fieldnames.remove(f.name)
yield f
if unique_fieldnames:
raise ValueError("Field name(s) %s are not valid for a '%s' item" % (
', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__))
def _get_item_update_elems(self, item, fieldnames):
from ..items import CalendarItem
fieldnames_copy = list(fieldnames)
if item.__class__ == CalendarItem:
# For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields
item.clean_timezone_fields(version=self.account.version) # Possibly also sets timezone values
for field_name in ('start', 'end'):
if field_name in fieldnames_copy:
tz_field_name = item.tz_field_for_field_name(field_name).name
if tz_field_name not in fieldnames_copy:
fieldnames_copy.append(tz_field_name)
for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy):
if field.is_read_only:
raise ValueError('%s is a read-only field' % field.name)
value = self._get_item_value(item, field)
if value is None or (field.is_list and not value):
# A value of None or [] means we want to remove this field from the item
yield from self._get_delete_item_elems(field=field)
else:
yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value)
def _get_item_value(self, item, field):
from ..items import CalendarItem
value = field.clean(getattr(item, field.name), version=self.account.version) # Make sure the value is OK
if item.__class__ == CalendarItem:
# For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone
if field.name in ('start', 'end'):
if type(value) is EWSDate:
# EWS always expects a datetime
return item.date_to_datetime(field_name=field.name)
tz_field_name = item.tz_field_for_field_name(field.name).name
return value.astimezone(getattr(item, tz_field_name))
return value
def _get_delete_item_elems(self, field):
if field.is_required or field.is_required_after_save:
raise ValueError('%s is a required field and may not be deleted' % field.name)
for field_path in FieldPath(field=field).expand(version=self.account.version):
yield self._delete_item_elem(field_path=field_path)
def _get_set_item_elems(self, item_model, field, value):
if isinstance(field, IndexedField):
# Generate either set or delete elements for all combinations of labels and subfields
supported_labels = field.value_cls.get_field_by_fieldname('label')\
.supported_choices(version=self.account.version)
seen_labels = set()
subfields = field.value_cls.supported_fields(version=self.account.version)
for v in value:
seen_labels.add(v.label)
for subfield in subfields:
field_path = FieldPath(field=field, label=v.label, subfield=subfield)
subfield_value = getattr(v, subfield.name)
if not subfield_value:
# Generate delete elements for blank subfield values
yield self._delete_item_elem(field_path=field_path)
else:
# Generate set elements for non-null subfield values
yield self._set_item_elem(
item_model=item_model,
field_path=field_path,
value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}),
)
# Generate delete elements for all subfields of all labels not mentioned in the list of values
for label in (label for label in supported_labels if label not in seen_labels):
for subfield in subfields:
yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield))
else:
yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value)
def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
suppress_read_receipts):
# Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
# are the attribute names that were updated. Returns the XML for an UpdateItem call.
# an UpdateItem request.
if self.account.version.build >= EXCHANGE_2013_SP1:
updateitem = create_element(
'm:%s' % self.SERVICE_NAME,
attrs=OrderedDict([
('ConflictResolution', conflict_resolution),
('MessageDisposition', message_disposition),
('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
])
)
else:
updateitem = create_element(
'm:%s' % self.SERVICE_NAME,
attrs=OrderedDict([
('ConflictResolution', conflict_resolution),
('MessageDisposition', message_disposition),
('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
])
)
itemchanges = create_element('m:ItemChanges')
version = self.account.version
for item, fieldnames in items:
if not item.account:
item.account = self.account
if not fieldnames:
raise ValueError('"fieldnames" must not be empty')
itemchange = create_element('t:ItemChange')
set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
updates = create_element('t:Updates')
for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
updates.append(elem)
itemchange.append(updates)
itemchanges.append(itemchange)
if not len(itemchanges):
raise ValueError('"items" must not be empty')
updateitem.append(itemchanges)
return updateitem
Classes
class UpdateItem (*args, **kwargs)
-
Expand source code
class UpdateItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation""" SERVICE_NAME = 'UpdateItem' element_container_name = '{%s}Items' % MNS def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts): from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \ SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES: raise ValueError("'conflict_resolution' %s must be one of %s" % ( conflict_resolution, CONFLICT_RESOLUTION_CHOICES )) if message_disposition not in MESSAGE_DISPOSITION_CHOICES: raise ValueError("'message_disposition' %s must be one of %s" % ( message_disposition, MESSAGE_DISPOSITION_CHOICES )) if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES: raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % ( send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES )) if suppress_read_receipts not in (True, False): raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts) if message_disposition == SEND_ONLY: raise ValueError('Cannot send-only existing objects. Use SendItem service instead') return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=items, conflict_resolution=conflict_resolution, message_disposition=message_disposition, send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations, suppress_read_receipts=suppress_read_receipts, )) def _elems_to_objs(self, elems): from ..items import Item for elem in elems: if isinstance(elem, (Exception, type(None))): yield elem continue yield Item.id_from_xml(elem) def _delete_item_elem(self, field_path): deleteitemfield = create_element('t:DeleteItemField') return set_xml_value(deleteitemfield, field_path, version=self.account.version) def _set_item_elem(self, item_model, field_path, value): setitemfield = create_element('t:SetItemField') set_xml_value(setitemfield, field_path, version=self.account.version) item_elem = create_element(item_model.request_tag()) field_elem = field_path.field.to_xml(value, version=self.account.version) set_xml_value(item_elem, field_elem, version=self.account.version) setitemfield.append(item_elem) return setitemfield @staticmethod def _sorted_fields(item_model, fieldnames): # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS. # Checks that all fieldnames are valid. unique_fieldnames = list(OrderedDict.fromkeys(fieldnames)) # Make field names unique ,but keep ordering # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field. for f in item_model.FIELDS: if f.name in unique_fieldnames: unique_fieldnames.remove(f.name) yield f if unique_fieldnames: raise ValueError("Field name(s) %s are not valid for a '%s' item" % ( ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__)) def _get_item_update_elems(self, item, fieldnames): from ..items import CalendarItem fieldnames_copy = list(fieldnames) if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields item.clean_timezone_fields(version=self.account.version) # Possibly also sets timezone values for field_name in ('start', 'end'): if field_name in fieldnames_copy: tz_field_name = item.tz_field_for_field_name(field_name).name if tz_field_name not in fieldnames_copy: fieldnames_copy.append(tz_field_name) for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy): if field.is_read_only: raise ValueError('%s is a read-only field' % field.name) value = self._get_item_value(item, field) if value is None or (field.is_list and not value): # A value of None or [] means we want to remove this field from the item yield from self._get_delete_item_elems(field=field) else: yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value) def _get_item_value(self, item, field): from ..items import CalendarItem value = field.clean(getattr(item, field.name), version=self.account.version) # Make sure the value is OK if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone if field.name in ('start', 'end'): if type(value) is EWSDate: # EWS always expects a datetime return item.date_to_datetime(field_name=field.name) tz_field_name = item.tz_field_for_field_name(field.name).name return value.astimezone(getattr(item, tz_field_name)) return value def _get_delete_item_elems(self, field): if field.is_required or field.is_required_after_save: raise ValueError('%s is a required field and may not be deleted' % field.name) for field_path in FieldPath(field=field).expand(version=self.account.version): yield self._delete_item_elem(field_path=field_path) def _get_set_item_elems(self, item_model, field, value): if isinstance(field, IndexedField): # Generate either set or delete elements for all combinations of labels and subfields supported_labels = field.value_cls.get_field_by_fieldname('label')\ .supported_choices(version=self.account.version) seen_labels = set() subfields = field.value_cls.supported_fields(version=self.account.version) for v in value: seen_labels.add(v.label) for subfield in subfields: field_path = FieldPath(field=field, label=v.label, subfield=subfield) subfield_value = getattr(v, subfield.name) if not subfield_value: # Generate delete elements for blank subfield values yield self._delete_item_elem(field_path=field_path) else: # Generate set elements for non-null subfield values yield self._set_item_elem( item_model=item_model, field_path=field_path, value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}), ) # Generate delete elements for all subfields of all labels not mentioned in the list of values for label in (label for label in supported_labels if label not in seen_labels): for subfield in subfields: yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield)) else: yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value) def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts): # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames' # are the attribute names that were updated. Returns the XML for an UpdateItem call. # an UpdateItem request. if self.account.version.build >= EXCHANGE_2013_SP1: updateitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('ConflictResolution', conflict_resolution), ('MessageDisposition', message_disposition), ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations), ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'), ]) ) else: updateitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('ConflictResolution', conflict_resolution), ('MessageDisposition', message_disposition), ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations), ]) ) itemchanges = create_element('m:ItemChanges') version = self.account.version for item, fieldnames in items: if not item.account: item.account = self.account if not fieldnames: raise ValueError('"fieldnames" must not be empty') itemchange = create_element('t:ItemChange') set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version) updates = create_element('t:Updates') for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames): updates.append(elem) itemchange.append(updates) itemchanges.append(itemchange) if not len(itemchanges): raise ValueError('"items" must not be empty') updateitem.append(itemchanges) return updateitem
Ancestors
Class variables
var SERVICE_NAME
var element_container_name
Methods
def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)
-
Expand source code
def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts): from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \ SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES: raise ValueError("'conflict_resolution' %s must be one of %s" % ( conflict_resolution, CONFLICT_RESOLUTION_CHOICES )) if message_disposition not in MESSAGE_DISPOSITION_CHOICES: raise ValueError("'message_disposition' %s must be one of %s" % ( message_disposition, MESSAGE_DISPOSITION_CHOICES )) if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES: raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % ( send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES )) if suppress_read_receipts not in (True, False): raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts) if message_disposition == SEND_ONLY: raise ValueError('Cannot send-only existing objects. Use SendItem service instead') return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=items, conflict_resolution=conflict_resolution, message_disposition=message_disposition, send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations, suppress_read_receipts=suppress_read_receipts, ))
def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)
-
Expand source code
def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts): # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames' # are the attribute names that were updated. Returns the XML for an UpdateItem call. # an UpdateItem request. if self.account.version.build >= EXCHANGE_2013_SP1: updateitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('ConflictResolution', conflict_resolution), ('MessageDisposition', message_disposition), ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations), ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'), ]) ) else: updateitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('ConflictResolution', conflict_resolution), ('MessageDisposition', message_disposition), ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations), ]) ) itemchanges = create_element('m:ItemChanges') version = self.account.version for item, fieldnames in items: if not item.account: item.account = self.account if not fieldnames: raise ValueError('"fieldnames" must not be empty') itemchange = create_element('t:ItemChange') set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version) updates = create_element('t:Updates') for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames): updates.append(elem) itemchange.append(updates) itemchanges.append(itemchange) if not len(itemchanges): raise ValueError('"items" must not be empty') updateitem.append(itemchanges) return updateitem
Inherited members