示例#1
0
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()
示例#2
0
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
示例#3
0
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
示例#4
0
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
示例#5
0
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
示例#6
0
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()
示例#7
0
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")
示例#9
0
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")
示例#10
0
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 == '*****@*****.**'
示例#11
0
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 == '*****@*****.**'
示例#12
0
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 == ["*****@*****.**"]
示例#13
0
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
示例#14
0
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)
示例#15
0
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
示例#16
0
def fake_event():
    return Event(title="The fifth element",
                 participants=[{
                     "name": "Ronald Zubar",
                     "email": "*****@*****.**",
                     "status": "noreply",
                     "notes": "required"
                 }])
示例#17
0
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 == "*****@*****.**"
示例#18
0
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
示例#19
0
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
示例#20
0
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
示例#21
0
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
示例#22
0
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",
            },
        ],
    )
示例#23
0
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
示例#24
0
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)
示例#25
0
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
示例#26
0
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')
示例#27
0
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
示例#28
0
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
示例#29
0
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')
示例#30
0
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,
    )
示例#31
0
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'