def test_non_recurring_events_behave(db, default_account, calendar): event = Event(namespace_id=default_account.namespace.id, calendar=calendar, title='not recurring', description='', uid='non_recurring_uid', location='', busy=False, read_only=False, reminders='', recurrence=None, start=arrow.get(2014, 07, 07, 13, 30), end=arrow.get(2014, 07, 07, 13, 55), all_day=False, is_owner=False, participants=[], provider_name='inbox', raw_data='', original_start_tz='America/Los_Angeles', original_start_time=None, master_event_uid=None, source='local') assert isinstance(event, Event) with pytest.raises(AttributeError): event.inflate()
def test_rsvp_recipient(default_account, message): assert rsvp_recipient(None) is None event = Event.create() event.owner = "Georges Perec <*****@*****.**>" assert rsvp_recipient(event) == "*****@*****.**" event = Event.create() event.owner = "<*****@*****.**>" assert rsvp_recipient(event) == "*****@*****.**" event = Event.create() event.owner = "*****@*****.**" assert rsvp_recipient(event) == "*****@*****.**" event.owner = "None <None>" assert rsvp_recipient(event) is None message.from_addr = [("Georges Perec", "*****@*****.**")] event = Event.create() event.owner = None event.message = message assert rsvp_recipient(event) == message.from_addr[0][1] message.from_addr = None assert rsvp_recipient(event) is None message.from_addr = [] assert rsvp_recipient(event) is None message.from_addr = [("", "")] assert rsvp_recipient(event) is None message.from_addr = [("Georges Sans Addresse", "")] assert rsvp_recipient(event) is None
def test_unicode_event_truncation(db, default_account): emoji_str = u"".join([u"😁" for i in range(300)]) title = "".join(["a" for i in range(2000)]) e = Event(raw_data='', busy=True, all_day=False, read_only=False, uid='31418', start=datetime(2015, 2, 22, 11, 11), end=datetime(2015, 2, 22, 22, 22), is_owner=True, calendar=default_account.emailed_events_calendar, title=title, location=emoji_str, participants=[]) e.namespace = default_account.namespace db.session.add(e) db.session.commit() # Both location and title should be properly truncated to their max lengths. # It's ok to have N unicode characters in a VARCHAR(N) field because # the column is uft8-encoded. assert len(e.location) == 255 assert len(e.title) == 1024
def test_rsvp_recipient(default_account, message): assert rsvp_recipient(None) is None event = Event() event.owner = "Georges Perec <*****@*****.**>" assert rsvp_recipient(event) == "*****@*****.**" event = Event() event.owner = "<*****@*****.**>" assert rsvp_recipient(event) == "*****@*****.**" event = Event() event.owner = "*****@*****.**" assert rsvp_recipient(event) == "*****@*****.**" event.owner = "None <None>" assert rsvp_recipient(event) is None message.from_addr = [("Georges Perec", "*****@*****.**")] event = Event() event.owner = None event.message = message assert rsvp_recipient(event) == message.from_addr[0][1] message.from_addr = None assert rsvp_recipient(event) is None message.from_addr = [] assert rsvp_recipient(event) is None message.from_addr = [("", "")] assert rsvp_recipient(event) is None message.from_addr = [("Georges Sans Addresse", "")] assert rsvp_recipient(event) is None
def test_unicode_event_truncation(db, default_account): emoji_str = u"".join([u"😁" for i in range(256)]) title = "".join(["a" for i in range(2048)]) e = Event(raw_data='', busy=True, all_day=False, read_only=False, uid='31418', start=datetime(2015, 2, 22, 11, 11), end=datetime(2015, 2, 22, 22, 22), is_owner=True, calendar=default_account.emailed_events_calendar, title=title, location=emoji_str, participants=[]) e.namespace = default_account.namespace db.session.add(e) db.session.commit() # Original location had 256 emoji chars. Emoji in utf-8 are # 4 bytes in length. The field is at most 255 chars, so # 255 / 4 = 63. assert len(e.location) == 63 assert len(e.title) == 1024
def test_self_sent_update(db, default_account, message): # Create the calendars add_fake_calendar(db.session, default_account.namespace.id, name="Emailed events", read_only=True) default_calendar = add_fake_calendar(db.session, default_account.namespace.id, name="Calendar", read_only=False) # Import the self-sent event. with open(absolute_path(FIXTURES + "self_sent_v1.ics")) as fd: ics_data = fd.read() msg = add_fake_msg_with_calendar_part(db.session, default_account, ics_data) msg.from_addr = [(default_account.name, default_account.email_address)] import_attached_events(db.session, default_account, msg) db.session.commit() evs = db.session.query(Event).filter( Event.uid == "*****@*****.**").all() assert len(evs) == 1 ev = evs[0] assert ev.location == ("Olympia Hall, 28 Boulevard des Capucines, " "75009 Paris, France") # Create a copy of the event, and store it in the default calendar. event_copy = Event() event_copy.update(ev) event_copy.calendar = default_calendar db.session.add(event_copy) db.session.commit() with open(absolute_path(FIXTURES + "self_sent_v2.ics")) as fd: ics_data = fd.read() msg = add_fake_msg_with_calendar_part(db.session, default_account, ics_data) import_attached_events(db.session, default_account, msg) db.session.commit() evs = db.session.query(Event).filter( Event.uid == "*****@*****.**").all() # Check that the event in the default calendar didn't get updated. assert len(evs) == 2 for ev in evs: db.session.refresh(ev) if ev.calendar_id == default_calendar.id: assert ev.location == ("Olympia Hall, 28 Boulevard des Capucines, " "75009 Paris, France") else: assert ev.location == (u"Le Zenith, 211 Avenue Jean Jaures, " "75019 Paris, France")
def test_self_sent_update(db, default_account, message): # Create the calendars add_fake_calendar(db.session, default_account.namespace.id, name="Emailed events", read_only=True) default_calendar = add_fake_calendar(db.session, default_account.namespace.id, name="Calendar", read_only=False) # Import the self-sent event. with open(absolute_path(FIXTURES + 'self_sent_v1.ics')) as fd: ics_data = fd.read() msg = add_fake_msg_with_calendar_part(db.session, default_account, ics_data) msg.from_addr = [(default_account.name, default_account.email_address)] import_attached_events(db.session, default_account, msg) db.session.commit() evs = db.session.query(Event).filter( Event.uid == "*****@*****.**").all() assert len(evs) == 1 ev = evs[0] assert ev.location == ("Olympia Hall, 28 Boulevard des Capucines, " "75009 Paris, France") # Create a copy of the event, and store it in the default calendar. event_copy = Event() event_copy.update(ev) event_copy.calendar = default_calendar db.session.add(event_copy) db.session.commit() with open(absolute_path(FIXTURES + 'self_sent_v2.ics')) as fd: ics_data = fd.read() msg = add_fake_msg_with_calendar_part( db.session, default_account, ics_data) import_attached_events(db.session, default_account, msg) db.session.commit() evs = db.session.query(Event).filter( Event.uid == "*****@*****.**").all() # Check that the event in the default calendar didn't get updated. assert len(evs) == 2 for ev in evs: db.session.refresh(ev) if ev.calendar_id == default_calendar.id: assert ev.location == ("Olympia Hall, 28 Boulevard des Capucines, " "75009 Paris, France") else: assert ev.location == (u"Le Zenith, 211 Avenue Jean Jaures, " "75019 Paris, France")
def test_event_organizer_parsing(): from inbox.models.event import Event e = Event() e.owner = 'Jean Lecanuet <*****@*****.**>' assert e.organizer_email == '*****@*****.**' e.owner = u'Pierre Mendès-France <*****@*****.** >' assert e.organizer_email == '*****@*****.**' e.owner = u'Pierre Messmer < *****@*****.** >' assert e.organizer_email == '*****@*****.**'
def test_event_emails(): from inbox.models.event import Event e = Event() e.description = "Email: [email protected]." assert e.emails_from_description == ["*****@*****.**"] e.description = '<a href="mailto:[email protected]">[email protected]</a>' assert e.emails_from_description == [ "*****@*****.**", "*****@*****.**" ] e.title = "Email: [email protected]" assert e.emails_from_title == ["*****@*****.**"]
def recurring_event(db, account, calendar, rrule, start=arrow.get(2014, 8, 7, 20, 30, 00), end=arrow.get(2014, 8, 7, 21, 30, 00), all_day=False): ev = db.session.query(Event).filter_by(uid='myuid').first() if ev: db.session.delete(ev) ev = Event(namespace_id=account.namespace.id, calendar=calendar, title='recurring', description='', uid='myuid', location='', busy=False, read_only=False, reminders='', recurrence=rrule, start=start, end=end, all_day=all_day, is_owner=False, participants=[], provider_name='inbox', raw_data='', original_start_tz='America/Los_Angeles', original_start_time=None, master_event_uid=None, source='local') db.session.add(ev) db.session.commit() return ev
def test_when_delta(): # Test that the event length is calculated correctly ev = Event.create(namespace_id=0) # Time: minutes is 0 if start/end at same time ev.start = arrow.get(2015, 1, 1, 10, 0, 0) ev.end = arrow.get(2015, 1, 1, 10, 0, 0) when = ev.when assert isinstance(when, Time) assert ev.length == timedelta(minutes=0) # TimeSpan ev.start = arrow.get(2015, 1, 1, 10, 0, 0) ev.end = arrow.get(2015, 1, 1, 10, 30, 0) when = ev.when assert isinstance(when, TimeSpan) assert ev.length == timedelta(minutes=30) # Date: notice days is 0 if starts/ends on same day ev.all_day = True ev.start = arrow.get(2015, 1, 1, 0, 0, 0) ev.end = arrow.get(2015, 1, 1, 0, 0, 0) when = ev.when assert isinstance(when, Date) assert ev.length == timedelta(days=0) # DateSpan ev.all_day = True ev.start = arrow.get(2015, 1, 1, 10, 0, 0) ev.end = arrow.get(2015, 1, 2, 10, 0, 0) when = ev.when assert isinstance(when, DateSpan) assert ev.length == timedelta(days=1)
def test_new_instance_cancelled(db, default_account, calendar): # Test that if we receive a cancelled override from Google, we save it # as an override with cancelled status rather than deleting it. event = recurring_event(db, default_account, calendar, TEST_EXDATE_RULE) override_uid = event.uid + "_20140814T203000Z" override = Event(title='CANCELLED', description='', uid=override_uid, location='', busy=False, read_only=False, reminders='', recurrence=None, start=arrow.get(2014, 8, 14, 22, 15, 00), end=arrow.get(2014, 8, 14, 23, 15, 00), all_day=False, is_owner=False, participants=[], provider_name='inbox', raw_data='', original_start_tz='America/Los_Angeles', original_start_time=arrow.get(2014, 8, 14, 21, 30, 00), master_event_uid=event.uid, cancelled=True, source='local') handle_event_updates(default_account.namespace.id, calendar.id, [override], log, db.session) db.session.commit() # Check the event got saved with the cancelled flag find_override = db.session.query(Event).filter_by( uid=override_uid, namespace_id=default_account.namespace.id).one() assert find_override.cancelled is True
def fake_event(): return Event(title="The fifth element", participants=[{ "name": "Ronald Zubar", "email": "*****@*****.**", "status": "noreply", "notes": "required" }])
def test_event_organizer_parsing(): from inbox.models.event import Event e = Event.create() e.owner = "Jean Lecanuet <*****@*****.**>" assert e.organizer_email == "*****@*****.**" e.owner = u"Pierre Mendès-France <*****@*****.** >" assert e.organizer_email == "*****@*****.**" e.owner = u"Pierre Messmer < *****@*****.** >" assert e.organizer_email == "*****@*****.**"
def test_link_events_from_override(db, default_account, calendar): # Test that by creating a recurring event and override separately, we # can link them together based on UID and namespace_id when starting # from the override. master = recurring_event(db, default_account, calendar, TEST_EXDATE_RULE) original_start = parse_exdate(master)[0] override = Event(original_start_time=original_start, master_event_uid=master.uid, namespace_id=master.namespace_id, source='local') assert isinstance(override, RecurringEventOverride) link_events(db.session, override) assert override.master == master
def recurring_event( db, account, calendar, rrule, start=None, end=None, all_day=False, commit=True, ): start = start or arrow.get(2014, 8, 7, 20, 30, 0) end = end or arrow.get(2014, 8, 7, 21, 30, 0) # commit: are we returning a commited instance object? if commit: ev = db.session.query(Event).filter_by(uid="myuid").first() if ev: db.session.delete(ev) ev = Event.create( namespace_id=account.namespace.id, calendar=calendar, title="recurring", description="", uid="myuid", location="", busy=False, read_only=False, reminders="", recurrence=rrule, start=start, end=end, all_day=all_day, is_owner=False, participants=[], provider_name="inbox", raw_data="", original_start_tz="America/Los_Angeles", original_start_time=None, master_event_uid=None, source="local", ) if commit: db.session.add(ev) db.session.commit() return ev
def recurring_override_instance(db, master, original_start, start, end): # Returns an Override that has the master's UID, but is not linked yet override_uid = '{}_{}'.format(master.uid, original_start.strftime("%Y%m%dT%H%M%SZ")) ev = db.session.query(Event).filter_by(uid=override_uid).first() if ev: db.session.delete(ev) db.session.commit() ev = Event(original_start_time=original_start, master_event_uid=master.uid, namespace_id=master.namespace_id, calendar_id=master.calendar_id) ev.update(master) ev.uid = override_uid ev.start = start ev.end = end ev.master_event_uid = master.uid db.session.add(ev) return ev
def test_linking_events_from_different_calendars(db, default_account, calendar, other_calendar): # Test that two events with the same UID but in different calendars don't # get linked together. This is important because with the Google API, a # recurring events can be in two calendars and have the same UID. # In this case, we create two different recurring events. master = recurring_event(db, default_account, calendar, TEST_EXDATE_RULE) original_start = parse_exdate(master)[0] override = Event(original_start_time=original_start, master_event_uid=master.uid, namespace_id=master.namespace_id, calendar_id=other_calendar.id, uid='blah', source='local') assert isinstance(override, RecurringEventOverride) link_events(db.session, override) assert override.master == None
def fake_event2(): return Event.create( title="The fifth element", participants=[ { "name": "Ronald Zubar", "email": "*****@*****.**", "status": "noreply", "notes": "required", }, { "name": "Ronald McDonald", "email": "*****@*****.**", "status": "noreply", "notes": "required", }, ], )
def test_when_delta(): # Test that the event length is calculated correctly ev = Event(namespace_id=0) # Time: minutes is 0 if start/end at same time ev.start = arrow.get(2015, 01, 01, 10, 00, 00) ev.end = arrow.get(2015, 01, 01, 10, 00, 00) when = ev.when assert isinstance(when, Time) assert ev.length == timedelta(minutes=0) # TimeSpan ev.start = arrow.get(2015, 01, 01, 10, 00, 00) ev.end = arrow.get(2015, 01, 01, 10, 30, 00) when = ev.when assert isinstance(when, TimeSpan) assert ev.length == timedelta(minutes=30) # Date: notice days is 0 if starts/ends on same day ev.all_day = True ev.start = arrow.get(2015, 01, 01, 00, 00, 00) ev.end = arrow.get(2015, 01, 01, 00, 00, 00) when = ev.when assert isinstance(when, Date) assert ev.length == timedelta(days=0) # DateSpan ev.all_day = True ev.start = arrow.get(2015, 01, 01, 10, 00, 00) ev.end = arrow.get(2015, 01, 02, 10, 00, 00) when = ev.when assert isinstance(when, DateSpan) assert ev.length == timedelta(days=1)
def test_rsvp_recipient(default_account, message): assert rsvp_recipient(None) is None event = Event() event.owner = 'Georges Perec <*****@*****.**>' assert rsvp_recipient(event) == '*****@*****.**' event = Event() event.owner = '<*****@*****.**>' assert rsvp_recipient(event) == '*****@*****.**' event = Event() event.owner = '*****@*****.**' assert rsvp_recipient(event) == '*****@*****.**' event.owner = 'None <None>' assert rsvp_recipient(event) is None message.from_addr = [('Georges Perec', '*****@*****.**')] event = Event() event.owner = None event.message = message assert rsvp_recipient(event) == message.from_addr[0][1] message.from_addr = None assert rsvp_recipient(event) is None message.from_addr = [] assert rsvp_recipient(event) is None message.from_addr = [('', '')] assert rsvp_recipient(event) is None message.from_addr = [('Georges Sans Addresse', '')] assert rsvp_recipient(event) is None
def parse_event_response(event, read_only_calendar): """ Constructs an Event object from a Google event resource (a dictionary). See https://developers.google.com/google-apps/calendar/v3/reference/events Parameters ---------- event: dict Returns ------- A corresponding Event instance. This instance is not committed or added to a session. """ uid = str(event['id']) # The entirety of the raw event data in json representation. raw_data = json.dumps(event) title = event.get('summary', '') # Timing data _start = event['start'] _end = event['end'] _original = event.get('originalStartTime', {}) event_time = google_to_event_time(_start, _end) original_start = parse_google_time(_original) start_tz = _start.get('timeZone') last_modified = parse_datetime(event.get('updated')) description = event.get('description') location = event.get('location') busy = event.get('transparency') != 'transparent' sequence = event.get('sequence', 0) # We're lucky because event statuses follow the icalendar # spec. event_status = event.get('status', 'confirmed') assert event_status in EVENT_STATUSES # Ownership, read_only information creator = event.get('creator') if creator: owner = u'{} <{}>'.format(creator.get('displayName', ''), creator.get('email', '')) else: owner = '' participants = [] attendees = event.get('attendees', []) for attendee in attendees: status = STATUS_MAP[attendee.get('responseStatus')] participants.append({ 'email': attendee.get('email'), 'name': attendee.get('displayName'), 'status': status, 'notes': attendee.get('comment') }) organizer = event.get('organizer') is_owner = bool(organizer and organizer.get('self')) # FIXME @karim: The right thing here would be to use Google's ACL API. # There's some obscure cases, like an autoimported event which guests can # edit that can't be modified. read_only = True if not read_only_calendar: read_only = False # Recurring master or override info recurrence = event.get('recurrence') master_uid = event.get('recurringEventId') cancelled = (event.get('status') == 'cancelled') return Event(uid=uid, raw_data=raw_data, title=title, description=description, location=location, busy=busy, start=event_time.start, end=event_time.end, all_day=event_time.all_day, owner=owner, is_owner=is_owner, read_only=read_only, participants=participants, recurrence=recurrence, last_modified=last_modified, original_start_tz=start_tz, original_start_time=original_start, master_event_uid=master_uid, cancelled=cancelled, status=event_status, sequence_number=sequence, source='local')
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = dict(invites=[], rsvps=[]) # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get("method") if component.name == "VTIMEZONE": tzname = component.get("TZID") assert tzname in timezones_table, "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get("dtstart").dt original_end = component.get("dtend").dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None all_day = False if isinstance(start, datetime) and isinstance(end, datetime): tzid = str(original_start.tzinfo) if tzid in timezones_table: original_start_tz = timezones_table[tzid] if original_start.tzinfo is None: tzid = component.get("dtstart").params.get("TZID", None) assert (tzid in timezones_table ), "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) original_start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get("dtend").params.get("TZID", None) assert (tzid in timezones_table ), "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) original_end = local_timezone.localize(original_end) # Now that we have tz-aware datetimes, convert them to UTC start = original_start.astimezone(pytz.UTC) end = original_end.astimezone(pytz.UTC) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) assert isinstance(start, type(end)), ("Start and end should be of " "the same type") # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. component_dtstamp = component.get("dtstamp") component_last_modified = component.get("last-modified") last_modified = None if component_dtstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if component_dtstamp.dt.tzinfo is not None: last_modified = component_dtstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") elif component_last_modified is not None: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component_last_modified.dt title = None summaries = component.get("summary", []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = component.get("description") if description is not None: description = unicode(description) event_status = component.get("status") if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get("method") if method and method.lower() == "cancel": event_status = "cancelled" elif calendar_method and calendar_method.lower() == "cancel": # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = "cancelled" else: # Otherwise assume the event has been confirmed. event_status = "confirmed" assert event_status in EVENT_STATUSES recur = component.get("rrule") if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get("organizer") organizer_name = None organizer_email = None if organizer: organizer_email = unicode(organizer) if organizer_email.lower().startswith("mailto:"): organizer_email = organizer_email[7:] if "CN" in organizer.params: organizer_name = organizer.params["CN"] owner = formataddr([organizer_name, organizer_email.lower()]) else: owner = None is_owner = False if owner is not None and ( namespace.account.email_address == canonicalize_address(organizer_email)): is_owner = True attendees = component.get("attendee", []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith("mailto:"): email = email[7:] try: name = attendee.params["CN"] except KeyError: name = None status_map = { "NEEDS-ACTION": "noreply", "ACCEPTED": "yes", "DECLINED": "no", "TENTATIVE": "maybe", } status = "noreply" try: a_status = attendee.params["PARTSTAT"] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params["X-NUM-GUESTS"] notes = u"Guests: {}".format(guests) except KeyError: pass participants.append({ "email": email.lower(), "name": name, "status": status, "notes": notes, "guests": [], }) location = component.get("location") uid = str(component.get("uid")) sequence_number = int(component.get("sequence", 0)) # Some services (I'm looking at you, http://www.foogi.me/) # don't follow the spec and generate icalendar files with # ridiculously big sequence numbers. Truncate them to fit in # our db. if sequence_number > 2147483647: sequence_number = 2147483647 event = Event( namespace=namespace, calendar=calendar, uid=uid, provider_name="ics", raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, owner=owner, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source="local", status=event_status, sequence_number=sequence_number, participants=participants, ) # We need to distinguish between invites/updates/cancellations # and RSVPs. if calendar_method == "REQUEST" or calendar_method == "CANCEL": events["invites"].append(event) elif calendar_method == "REPLY": events["rsvps"].append(event) return events
def events_from_ics(namespace, calendar, ics_str): try: cal = iCalendar.from_ical(ics_str) except (ValueError, IndexError, KeyError): raise MalformedEventError() events = [] # See: https://tools.ietf.org/html/rfc5546#section-3.2 calendar_method = None for component in cal.walk(): if component.name == "VCALENDAR": calendar_method = component.get('method') if component.name == "VTIMEZONE": tzname = component.get('TZID') assert tzname in timezones_table,\ "Non-UTC timezone should be in table" if component.name == "VEVENT": # Make sure the times are in UTC. try: original_start = component.get('dtstart').dt original_end = component.get('dtend').dt except AttributeError: raise MalformedEventError("Event lacks start and/or end time") start = original_start end = original_end original_start_tz = None if isinstance(start, datetime) and isinstance(end, datetime): all_day = False original_start_tz = str(original_start.tzinfo) # icalendar doesn't parse Windows timezones yet # (see: https://github.com/collective/icalendar/issues/44) # so we look if the timezone isn't in our Windows-TZ # to Olson-TZ table. if original_start.tzinfo is None: tzid = component.get('dtstart').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] original_start_tz = corresponding_tz local_timezone = pytz.timezone(corresponding_tz) start = local_timezone.localize(original_start) if original_end.tzinfo is None: tzid = component.get('dtend').params.get('TZID', None) assert tzid in timezones_table,\ "Non-UTC timezone should be in table" corresponding_tz = timezones_table[tzid] local_timezone = pytz.timezone(corresponding_tz) end = local_timezone.localize(original_end) elif isinstance(start, date) and isinstance(end, date): all_day = True start = arrow.get(start) end = arrow.get(end) # Get the last modification date. # Exchange uses DtStamp, iCloud and Gmail LAST-MODIFIED. last_modified_tstamp = component.get('dtstamp') last_modified = None if last_modified_tstamp is not None: # This is one surprising instance of Exchange doing # the right thing by giving us an UTC timestamp. Also note that # Google calendar also include the DtStamp field, probably to # be a good citizen. if last_modified_tstamp.dt.tzinfo is not None: last_modified = last_modified_tstamp.dt else: raise NotImplementedError("We don't support arcane Windows" " timezones in timestamps yet") else: # Try to look for a LAST-MODIFIED element instead. # Note: LAST-MODIFIED is always in UTC. # http://www.kanzaki.com/docs/ical/lastModified.html last_modified = component.get('last-modified').dt assert last_modified is not None, \ "Event should have a DtStamp or LAST-MODIFIED timestamp" title = None summaries = component.get('summary', []) if not isinstance(summaries, list): summaries = [summaries] if summaries != []: title = " - ".join(summaries) description = unicode(component.get('description')) event_status = component.get('status') if event_status is not None: event_status = event_status.lower() else: # Some providers (e.g: iCloud) don't use the status field. # Instead they use the METHOD field to signal cancellations. method = component.get('method') if method and method.lower() == 'cancel': event_status = 'cancelled' elif calendar_method and calendar_method.lower() == 'cancel': # So, this particular event was not cancelled. Maybe the # whole calendar was. event_status = 'cancelled' else: # Otherwise assume the event has been confirmed. event_status = 'confirmed' assert event_status in EVENT_STATUSES recur = component.get('rrule') if recur: recur = "RRULE:{}".format(recur.to_ical()) participants = [] organizer = component.get('organizer') if organizer: # Here's the problem. Gmail and Exchange define the organizer # field like this: # # ORGANIZER;CN="User";EMAIL="*****@*****.**":mailto:[email protected] # but iCloud does it like this: # ORGANIZER;CN=User;[email protected]:mailto: # [email protected] # so what we first try to get the EMAIL field, and only if # it's not present we use the MAILTO: link. if 'EMAIL' in organizer.params: organizer = organizer.params['EMAIL'] else: organizer = unicode(organizer) if organizer.startswith('mailto:'): organizer = organizer[7:] if (namespace.account.email_address == canonicalize_address( organizer)): is_owner = True else: is_owner = False attendees = component.get('attendee', []) # the iCalendar python module doesn't return a list when # there's only one attendee. Go figure. if not isinstance(attendees, list): attendees = [attendees] for attendee in attendees: email = unicode(attendee) # strip mailto: if it exists if email.lower().startswith('mailto:'): email = email[7:] try: name = attendee.params['CN'] except KeyError: name = None status_map = { 'NEEDS-ACTION': 'noreply', 'ACCEPTED': 'yes', 'DECLINED': 'no', 'TENTATIVE': 'maybe' } status = 'noreply' try: a_status = attendee.params['PARTSTAT'] status = status_map[a_status] except KeyError: pass notes = None try: guests = attendee.params['X-NUM-GUESTS'] notes = "Guests: {}".format(guests) except KeyError: pass participants.append({ 'email': email, 'name': name, 'status': status, 'notes': notes, 'guests': [] }) location = component.get('location') uid = str(component.get('uid')) event = Event(namespace=namespace, calendar=calendar, uid=uid, provider_name='ics', raw_data=component.to_ical(), title=title, description=description, location=location, reminders=str([]), recurrence=recur, start=start, end=end, busy=True, all_day=all_day, read_only=True, is_owner=is_owner, last_modified=last_modified, original_start_tz=original_start_tz, source='local', status=event_status, participants=participants) events.append(event) return events
def parse_event_response(event): """ Constructs an Event object from a Google event resource (a dictionary). See https://developers.google.com/google-apps/calendar/v3/reference/events Parameters ---------- event: dict Returns ------- A corresponding Event instance. This instance is not committed or added to a session. """ uid = str(event['id']) # The entirety of the raw event data in json representation. raw_data = json.dumps(event) title = event.get('summary', '') # Timing data _start = event['start'] _end = event['end'] _original = event.get('originalStartTime', {}) event_time = google_to_event_time(_start, _end) original_start = parse_google_time(_original) start_tz = _start.get('timeZone') last_modified = parse_datetime(event.get('updated')) description = event.get('description') location = event.get('location') busy = event.get('transparency') != 'transparent' # We're lucky because an event statuses follow the icalendar # spec. event_status = event.get('status', 'confirmed') assert event_status in EVENT_STATUSES # Ownership, read_only information creator = event.get('creator') if creator: owner = u'{} <{}>'.format(creator.get('displayName', ''), creator.get('email', '')) else: owner = '' is_owner = bool(creator and creator.get('self')) read_only = not (is_owner or event.get('guestsCanModify')) participants = [] attendees = event.get('attendees', []) for attendee in attendees: status = STATUS_MAP[attendee.get('responseStatus')] participants.append({ 'email': attendee.get('email'), 'name': attendee.get('displayName'), 'status': status, 'notes': attendee.get('comment') }) # Recurring master or override info recurrence = event.get('recurrence') master_uid = event.get('recurringEventId') cancelled = (event.get('status') == 'cancelled') return Event( uid=uid, raw_data=raw_data, title=title, description=description, location=location, busy=busy, start=event_time.start, end=event_time.end, all_day=event_time.all_day, owner=owner, is_owner=is_owner, read_only=read_only, participants=participants, recurrence=recurrence, last_modified=last_modified, original_start_tz=start_tz, original_start_time=original_start, master_event_uid=master_uid, cancelled=cancelled, status=event_status, # TODO(emfree): remove after data cleanup source='local')
def parse_event_response(event, read_only_calendar): """ Constructs an Event object from a Google event resource (a dictionary). See https://developers.google.com/google-apps/calendar/v3/reference/events Parameters ---------- event: dict Returns ------- A corresponding Event instance. This instance is not committed or added to a session. """ uid = str(event["id"]) # The entirety of the raw event data in json representation. raw_data = json.dumps(event) title = event.get("summary", "") # Timing data _start = event["start"] _end = event["end"] _original = event.get("originalStartTime", {}) event_time = google_to_event_time(_start, _end) original_start = parse_google_time(_original) start_tz = _start.get("timeZone") last_modified = parse_datetime(event.get("updated")) description = event.get("description") location = event.get("location") busy = event.get("transparency") != "transparent" sequence = event.get("sequence", 0) # We're lucky because event statuses follow the icalendar # spec. event_status = event.get("status", "confirmed") assert event_status in EVENT_STATUSES # Ownership, read_only information creator = event.get("creator") if creator: owner = u"{} <{}>".format(creator.get("displayName", ""), creator.get("email", "")) else: owner = "" participants = [] attendees = event.get("attendees", []) for attendee in attendees: status = STATUS_MAP[attendee.get("responseStatus")] participants.append({ "email": attendee.get("email"), "name": attendee.get("displayName"), "status": status, "notes": attendee.get("comment"), }) organizer = event.get("organizer") is_owner = bool(organizer and organizer.get("self")) # FIXME @karim: The right thing here would be to use Google's ACL API. # There's some obscure cases, like an autoimported event which guests can # edit that can't be modified. read_only = True if not read_only_calendar: read_only = False # Recurring master or override info recurrence = event.get("recurrence") master_uid = event.get("recurringEventId") cancelled = event.get("status") == "cancelled" visibility = event.get("visibility") # Rewrite some values documented in # https://developers.google.com/calendar/v3/reference/events if visibility == "default": visibility = None elif visibility == "confidential": visibility = "private" return Event( uid=uid, raw_data=raw_data, title=title, description=description, location=location, busy=busy, start=event_time.start, end=event_time.end, all_day=event_time.all_day, owner=owner, is_owner=is_owner, read_only=read_only, participants=participants, recurrence=recurrence, last_modified=last_modified, original_start_tz=start_tz, original_start_time=original_start, master_event_uid=master_uid, cancelled=cancelled, status=event_status, sequence_number=sequence, source="local", visibility=visibility, )
def test_override_updated(db, default_account, calendar): # Test that when a recurring event override is created or updated # remotely, we update our override links appropriately. event = recurring_event(db, default_account, calendar, TEST_RRULE) assert event is not None # create a new Event, as if we just got it from Google master_uid = event.uid override_uid = master_uid + "_20140814T203000Z" override = Event(title='new override from google', description='', uid=override_uid, location='', busy=False, read_only=False, reminders='', recurrence=None, start=arrow.get(2014, 8, 14, 22, 30, 00), end=arrow.get(2014, 8, 14, 23, 30, 00), all_day=False, is_owner=False, participants=[], provider_name='inbox', raw_data='', original_start_tz='America/Los_Angeles', original_start_time=arrow.get(2014, 8, 14, 21, 30, 00), master_event_uid=master_uid, source='local') handle_event_updates(default_account.namespace.id, calendar.id, [override], log, db.session) db.session.commit() # Lets see if the event got saved with the right info find_override = db.session.query(Event).filter_by(uid=override_uid).one() assert find_override is not None assert find_override.master_event_id == event.id # Update the same override, making sure we don't create two override = Event(title='new override from google', description='', uid=override_uid, location='walk and talk', busy=False, read_only=False, reminders='', recurrence=None, start=arrow.get(2014, 8, 14, 22, 15, 00), end=arrow.get(2014, 8, 14, 23, 15, 00), all_day=False, is_owner=False, participants=[], provider_name='inbox', raw_data='', original_start_tz='America/Los_Angeles', original_start_time=arrow.get(2014, 8, 14, 21, 30, 00), master_event_uid=master_uid, source='local') handle_event_updates(default_account.namespace.id, calendar.id, [override], log, db.session) db.session.commit() # Let's see if the event got saved with the right info find_override = db.session.query(Event).filter_by(uid=override_uid).one() assert find_override is not None assert find_override.master_event_id == event.id assert find_override.location == 'walk and talk'