| 313 | | return set(map(convfunc, val.split(','))) |
|---|
| 314 | | |
|---|
| 315 | | bymonth = getSet('ByMonth') |
|---|
| 316 | | byweekno = getSet('ByWeekNo') |
|---|
| 317 | | byyearday = getSet('ByYearDay') |
|---|
| 318 | | bymonthday = getSet('ByMonthDay') |
|---|
| 319 | | byday = getSet('ByDay', lambda s: s.upper()) |
|---|
| 320 | | |
|---|
| 321 | | # fail the conversion if any of these are set |
|---|
| 322 | | if (getXMLField(rulenode, 'ByHour') or getXMLField(rulenode, 'ByMinute') |
|---|
| 323 | | or getXMLField(rulenode, 'BySecond') or getXMLField(rulenode, 'BySetPos') |
|---|
| 324 | | or (interval is not None and interval != 1)): |
|---|
| 325 | | return (MOTO_REPEAT_NONE, []) |
|---|
| 326 | | |
|---|
| 327 | | # compute the day and week number that the event falls in |
|---|
| 328 | | eventday = VCAL_DAYS[eventdt.weekday()] |
|---|
| 329 | | if eventdt.day % 7 == 0: |
|---|
| 330 | | eventweek = eventdt.day / 7 |
|---|
| 331 | | else: |
|---|
| 332 | | eventweek = eventdt.day / 7 + 1 |
|---|
| 333 | | |
|---|
| 334 | | # some convenience variables for the tests below |
|---|
| 335 | | byeventweekday = set(['%d%s' % (eventweek, eventday)]) |
|---|
| 336 | | byalldays = set(VCAL_DAYS) |
|---|
| 337 | | byallmonths = set(range(1, 12)) |
|---|
| 338 | | |
|---|
| 339 | | # now test if the rule matches what we can represent |
|---|
| 340 | | if (freq == rrule.DAILY and (not byday or byday == byalldays) |
|---|
| 341 | | and not bymonthday and not byyearday and not byweekno |
|---|
| 342 | | and not bymonth): |
|---|
| 343 | | moto_type = MOTO_REPEAT_DAILY |
|---|
| 344 | | elif (freq == rrule.WEEKLY and (not byday or byday == set([eventday])) |
|---|
| 345 | | and not bymonthday and not byyearday and not byweekno and |
|---|
| 346 | | not bymonth): |
|---|
| 347 | | moto_type = MOTO_REPEAT_WEEKLY |
|---|
| 348 | | elif (freq == rrule.MONTHLY and not byday |
|---|
| 349 | | and (not bymonthday or bymonthday == set([eventdt.day])) |
|---|
| 350 | | and not byyearday and not byweekno |
|---|
| 351 | | and (not bymonth or bymonth == byallmonths)): |
|---|
| 352 | | moto_type = MOTO_REPEAT_MONTHLY_DATE |
|---|
| 353 | | elif (freq == rrule.MONTHLY and byday == byeventweekday and not bymonthday |
|---|
| 354 | | and not byyearday and not byweekno |
|---|
| 355 | | and (not bymonth or bymonth == byallmonths)): |
|---|
| 356 | | moto_type = MOTO_REPEAT_MONTHLY_DAY |
|---|
| 357 | | elif (freq == rrule.YEARLY and not byday |
|---|
| 358 | | and (not bymonthday or bymonthday == set([eventdt.day])) |
|---|
| 359 | | and not byyearday and not byweekno |
|---|
| 360 | | and (not bymonth or bymonth == set([eventdt.month]))): |
|---|
| 361 | | moto_type = MOTO_REPEAT_YEARLY |
|---|
| 362 | | else: |
|---|
| 363 | | return (MOTO_REPEAT_NONE, []) |
|---|
| 364 | | |
|---|
| 365 | | # phew, looks like we have something the phone can represent |
|---|
| 366 | | # now we need to work out the exceptions and see if this event still occurs |
|---|
| 367 | | |
|---|
| 368 | | # convert byday strings to constants needed by rrule |
|---|
| | 326 | |
|---|
| | 327 | bymonth = getFieldAsList('ByMonth') |
|---|
| | 328 | byyearday = getFieldAsList('ByYearDay') |
|---|
| | 329 | bymonthday = getFieldAsList('ByMonthDay') |
|---|
| | 330 | byday = getFieldAsList('ByDay', lambda s: s.upper()) |
|---|
| | 331 | |
|---|
| | 332 | # convert byday strings to the constants needed by rrule |
|---|
| | 342 | # done! |
|---|
| | 343 | return rrule.rrule(freq, dtstart=eventdt, interval=interval, count=count, |
|---|
| | 344 | until=until, bymonth=bymonth, bymonthday=bymonthday, |
|---|
| | 345 | byweekday=byweekday, byyearday=byyearday) |
|---|
| | 346 | |
|---|
| | 347 | def moto_repeat_day_to_weekdays(repeat_day, eventday): |
|---|
| | 348 | """For a weekly repeat, return the list of days the event repeats on. |
|---|
| | 349 | |
|---|
| | 350 | repeat_day is field 19 (repeat on day) in the extended event format |
|---|
| | 351 | eventday is the 0-based weekday on which the event occurs |
|---|
| | 352 | |
|---|
| | 353 | This is based on the following description: |
|---|
| | 354 | |
|---|
| | 355 | The weekday of the event (the one that gets repeated of course) gets the |
|---|
| | 356 | value 2^6 = 64, the following weekday gets the value 2^5, the after that one |
|---|
| | 357 | gets 2^4, etc. |
|---|
| | 358 | Field 19 contains the sum of the binary encoded weekdays. |
|---|
| | 359 | |
|---|
| | 360 | For example: lets assume we have an event on Thursday, 04-12-2007 |
|---|
| | 361 | As the starting weekday is the day of the first event (Thursday), this day |
|---|
| | 362 | gets encoded with 2^6, so the encoding for the whole week is as follows: |
|---|
| | 363 | thursday 2^6 (1000000), friday 2^5 (0100000), saturday 2^4 (0010000), |
|---|
| | 364 | sunday 2^3 (0001000), monday 2^2 (0000100), tuesday 2^1 (0000010), |
|---|
| | 365 | wednesday 2^0 (0000001) |
|---|
| | 366 | lets say we want to repeat that event every thursday and monday on a weekly basis. |
|---|
| | 367 | Field 19 would then contain 2^6 + 2^2 = 68 or in binary form 1000100 |
|---|
| | 368 | """ |
|---|
| | 369 | |
|---|
| | 370 | if repeat_day == 0: |
|---|
| | 371 | return [] |
|---|
| | 372 | else: |
|---|
| | 373 | eventday = (eventday - 1) % 7 |
|---|
| | 374 | rest = moto_repeat_day_to_weekdays(repeat_day / 2, eventday) |
|---|
| | 375 | if repeat_day % 2: |
|---|
| | 376 | return rest + [eventday] |
|---|
| | 377 | else: |
|---|
| | 378 | return rest |
|---|
| | 379 | |
|---|
| | 380 | def moto_weekdays_to_repeat_day(weekdays, eventday): |
|---|
| | 381 | """Returns the repeat_day value given a set of days on which a weekly event repeats.""" |
|---|
| | 382 | |
|---|
| | 383 | return sum([2 ** (6 - (day - eventday) % 7) for day in weekdays]) |
|---|
| | 384 | |
|---|
| | 385 | def moto_repeat_day_to_monthday(repeat_day): |
|---|
| | 386 | """For a monthly or yearly by day repeat, return the day the event repeats on. |
|---|
| | 387 | |
|---|
| | 388 | repeat_day is field 19 (repeat on day) in the extended event format |
|---|
| | 389 | returns a tuple (nth, day) |
|---|
| | 390 | nth is the Nth time the day occurs in the month |
|---|
| | 391 | day is the weekday number (0=monday) |
|---|
| | 392 | |
|---|
| | 393 | This is based on the following description: |
|---|
| | 394 | |
|---|
| | 395 | The weekdays get the following basis encodings : |
|---|
| | 396 | sunday = 0 |
|---|
| | 397 | monday = 8 |
|---|
| | 398 | tuesday = 16 |
|---|
| | 399 | wednesday = 24 |
|---|
| | 400 | thursday = 32 |
|---|
| | 401 | friday = 40 |
|---|
| | 402 | saturday = 48 |
|---|
| | 403 | |
|---|
| | 404 | The above values are NEVER the value of field 19. Instead the field contains |
|---|
| | 405 | a value computed as follows: |
|---|
| | 406 | WEEKDAYCODE + 1 = first WEEKDAY of month |
|---|
| | 407 | WEEKDAYCODE + 2 = second WEEKDAY of month |
|---|
| | 408 | WEEKDAYCODE + 3 = third WEEKDAY of month |
|---|
| | 409 | WEEKDAYCODE + 4 = fourth WEEKDAY of month |
|---|
| | 410 | WEEKDAYCODE + 5 = last WEEKDAY of month |
|---|
| | 411 | """ |
|---|
| | 412 | |
|---|
| | 413 | daynum = (repeat_day / 8 - 1) % 7 |
|---|
| | 414 | nth = repeat_day % 8 |
|---|
| | 415 | if nth > 4: |
|---|
| | 416 | nth = 4 - nth |
|---|
| | 417 | return (nth, daynum) |
|---|
| | 418 | |
|---|
| | 419 | # reverse of the above function |
|---|
| | 420 | def moto_monthday_to_repeat_day(nth, daynum): |
|---|
| | 421 | """For a monthly or yearly by day repeat, returns the repeat_day value given |
|---|
| | 422 | the Nth time a day occurs in a montyh, and the day of the week number. |
|---|
| | 423 | """ |
|---|
| | 424 | if nth < 0: |
|---|
| | 425 | nth = 4 - nth |
|---|
| | 426 | return ((daynum + 1) % 7) * 8 + nth |
|---|
| | 427 | |
|---|
| | 428 | def xml_rrule_to_moto(rulenodes, exdates, exrules, eventdt): |
|---|
| | 429 | """Process XML recursion rules, converting them to the closest-possible |
|---|
| | 430 | matching recursion specifier supported by the phone. |
|---|
| | 431 | |
|---|
| | 432 | Arguments: |
|---|
| | 433 | rulenodes: list of RecurrenceRule nodes |
|---|
| | 434 | exdates: list of exception dates |
|---|
| | 435 | exrules: list of exception rules |
|---|
| | 436 | eventdt: event's date (and time if set) |
|---|
| | 437 | |
|---|
| | 438 | Returns a dict containing the following fields: |
|---|
| | 439 | repeat_type: Motorola repeat type (always present) |
|---|
| | 440 | repeat_every: repeat every Nth time (valid for the extended format only) |
|---|
| | 441 | repeat_day: bitfield showing which days/weeks to repeat on (extended) |
|---|
| | 442 | repeat_end: end date of the repeat (extended) |
|---|
| | 443 | exceptions: list of exceptions (always present) |
|---|
| | 444 | |
|---|
| | 445 | The general approach we take is: if the recursion can be represented |
|---|
| | 446 | by the phone's data structure, we convert it, otherwise we ignore the |
|---|
| | 447 | rule completely (to avoid the event showing up at incorrect times) by |
|---|
| | 448 | raising an UnsupportedDataError exception. |
|---|
| | 449 | """ |
|---|
| | 450 | |
|---|
| | 451 | # default return values |
|---|
| | 452 | ret = {} |
|---|
| | 453 | ret['repeat_type'] = MOTO_REPEAT_NONE |
|---|
| | 454 | ret['repeat_every'] = 0 |
|---|
| | 455 | ret['repeat_day'] = 0 |
|---|
| | 456 | ret['repeat_end'] = None |
|---|
| | 457 | ret['exceptions'] = [] |
|---|
| | 458 | |
|---|
| | 459 | # can't support multiple rules |
|---|
| | 460 | if len(rulenodes) != 1: |
|---|
| | 461 | raise UnsupportedDataError('Unhandled recursion: too many rules') |
|---|
| | 462 | rulenode = rulenodes[0] |
|---|
| | 463 | |
|---|
| | 464 | freq = getXMLField(rulenode, 'Frequency').upper() |
|---|
| | 465 | |
|---|
| | 466 | interval = getXMLField(rulenode, 'Interval') |
|---|
| | 467 | if interval: |
|---|
| | 468 | ret['repeat_every'] = int(interval) |
|---|
| | 469 | |
|---|
| | 470 | def getSet(name, convfunc=int): |
|---|
| | 471 | """Convenience function, get a comma-separated set of values from XML.""" |
|---|
| | 472 | val = getXMLField(rulenode, name) |
|---|
| | 473 | if not val: |
|---|
| | 474 | return None |
|---|
| | 475 | return set(map(convfunc, val.split(','))) |
|---|
| | 476 | |
|---|
| | 477 | bymonth = getSet('ByMonth') |
|---|
| | 478 | byyearday = getSet('ByYearDay') |
|---|
| | 479 | bymonthday = getSet('ByMonthDay') |
|---|
| | 480 | byday = getSet('ByDay', lambda s: s.upper()) |
|---|
| | 481 | |
|---|
| | 482 | # convert byday strings to a list of tuples |
|---|
| | 483 | # first tuple elem is the "Nth" part, second is the day number |
|---|
| | 484 | # if no int part is present, the "Nth" value is 0 |
|---|
| | 485 | if byday: |
|---|
| | 486 | byday_pairs = [(int(s[:-2] or "0"), VCAL_DAYS.index(s[-2:])) for s in byday] |
|---|
| | 487 | if len(byday) == 1: |
|---|
| | 488 | (byday_nth, byday_daynum) = byday_pairs[0] |
|---|
| | 489 | |
|---|
| | 490 | # compute the day and week number that the event falls in |
|---|
| | 491 | if eventdt.day % 7 == 0: |
|---|
| | 492 | eventweek = eventdt.day / 7 |
|---|
| | 493 | else: |
|---|
| | 494 | eventweek = eventdt.day / 7 + 1 |
|---|
| | 495 | |
|---|
| | 496 | # convenience variable for the tests below |
|---|
| | 497 | byallmonths = set(range(1, 12)) |
|---|
| | 498 | |
|---|
| | 499 | # now test if the rule matches what we can represent |
|---|
| | 500 | if (freq == 'DAILY' and not byday and not bymonthday and not byyearday and not bymonth): |
|---|
| | 501 | ret['repeat_type'] = MOTO_REPEAT_DAILY |
|---|
| | 502 | elif (freq == 'WEEKLY' and not bymonthday and not byyearday and not bymonth): |
|---|
| | 503 | ret['repeat_type'] = MOTO_REPEAT_WEEKLY |
|---|
| | 504 | if byday: |
|---|
| | 505 | # the Nth specifier doesn't make sense on a byday value in a weekly rule |
|---|
| | 506 | # so we can ignore them |
|---|
| | 507 | byday_nums = [daynum for (_, daynum) in byday_pairs] |
|---|
| | 508 | if byday_nums != [eventdt.day]: |
|---|
| | 509 | ret['repeat_day'] = moto_weekdays_to_repeat_day(byday_nums, eventdt.day) |
|---|
| | 510 | elif (freq == 'MONTHLY' and not byday and (not bymonthday or bymonthday == set([eventdt.day])) |
|---|
| | 511 | and not byyearday and (not bymonth or bymonth == byallmonths)): |
|---|
| | 512 | ret['repeat_type'] = MOTO_REPEAT_MONTHLY_DATE |
|---|
| | 513 | elif (freq == 'MONTHLY' and byday and len(byday) == 1 and not bymonthday |
|---|
| | 514 | and not byyearday and (not bymonth or bymonth == byallmonths)): |
|---|
| | 515 | ret['repeat_type'] = MOTO_REPEAT_MONTHLY_DAY |
|---|
| | 516 | if not (byday_daynum == eventdt.weekday() and byday_nth == eventweek): |
|---|
| | 517 | ret['repeat_day'] = moto_monthday_to_repeat_day(byday_nth, byday_daynum) |
|---|
| | 518 | elif (freq == 'YEARLY' and not byday and (not bymonthday or bymonthday == set([eventdt.day])) |
|---|
| | 519 | and not byyearday and (not bymonth or bymonth == set([eventdt.month]))): |
|---|
| | 520 | ret['repeat_type'] = MOTO_REPEAT_YEARLY_DATE |
|---|
| | 521 | elif (freq == 'YEARLY' and byday and len(byday == 1) |
|---|
| | 522 | and (not bymonthday or bymonthday == set([eventdt.day])) |
|---|
| | 523 | and not byyearday and (not bymonth or bymonth == set([eventdt.month]))): |
|---|
| | 524 | ret['repeat_type'] = MOTO_REPEAT_YEARLY_DAY |
|---|
| | 525 | if not (byday_daynum == eventdt.weekday() and byday_nth == eventweek): |
|---|
| | 526 | ret['repeat_day'] = moto_monthday_to_repeat_day(byday_nth, byday_daynum) |
|---|
| | 527 | else: |
|---|
| | 528 | raise UnsupportedDataError('Unhandled recursion: cannot represent rule') |
|---|
| | 529 | |
|---|
| | 530 | # phew, looks like we have something the phone can represent |
|---|
| | 531 | # now we need to work out the exceptions and see if this event still occurs |
|---|
| | 532 | |
|---|
| 414 | | exceptions.append(num) |
|---|
| 415 | | |
|---|
| 416 | | return (moto_type, exceptions) |
|---|
| | 563 | ret['exceptions'].append(num) |
|---|
| | 564 | |
|---|
| | 565 | return ret |
|---|
| | 566 | |
|---|
| | 567 | def moto_rrule_to_xml(doc, eventdt, repeat_type, exceptions, |
|---|
| | 568 | repeat_every=None, repeat_day=None, repeat_end=None): |
|---|
| | 569 | """Convert a Motorola-format recurrence to the corresponding XML description. |
|---|
| | 570 | |
|---|
| | 571 | Arguments correspond to the return values of xml_rrule_to_moto above. |
|---|
| | 572 | |
|---|
| | 573 | Returns a list of XML node objects. |
|---|
| | 574 | """ |
|---|
| | 575 | |
|---|
| | 576 | # compute the week number that the event falls in |
|---|
| | 577 | if eventdt.day % 7 == 0: |
|---|
| | 578 | weeknum = eventdt.day / 7 |
|---|
| | 579 | else: |
|---|
| | 580 | weeknum = eventdt.day / 7 + 1 |
|---|
| | 581 | |
|---|
| | 582 | e = doc.createElement('RecurrenceRule') |
|---|
| | 583 | if repeat_type == MOTO_REPEAT_DAILY: |
|---|
| | 584 | appendXMLTag(doc, e, 'Frequency', 'DAILY') |
|---|
| | 585 | rule = rrule.rrule(rrule.DAILY, |
|---|
| | 586 | dtstart=eventdt, interval=repeat_every, until=repeat_end) |
|---|
| | 587 | elif repeat_type == MOTO_REPEAT_WEEKLY: |
|---|
| | 588 | appendXMLTag(doc, e, 'Frequency', 'WEEKLY') |
|---|
| | 589 | if repeat_day: |
|---|
| | 590 | repeat_days = moto_repeat_day_to_weekdays(repeat_day, eventdt.weekday()).sort() |
|---|
| | 591 | appendXMLTag(doc, e, 'ByDay', ','.join([VCAL_DAYS[n] for n in repeat_days])) |
|---|
| | 592 | else: |
|---|
| | 593 | repeat_days = [] |
|---|
| | 594 | rule = rrule.rrule(rrule.WEEKLY, byweekday=repeat_days, |
|---|
| | 595 | dtstart=eventdt, interval=repeat_every, until=repeat_end) |
|---|
| | 596 | elif repeat_type == MOTO_REPEAT_MONTHLY_DATE: |
|---|
| | 597 | appendXMLTag(doc, e, 'Frequency', 'MONTHLY') |
|---|
| | 598 | appendXMLTag(doc, e, 'ByMonthDay', str(eventdt.day)) |
|---|
| | 599 | rule = rrule.rrule(rrule.MONTHLY, bymonthday=eventdt.day, |
|---|
| | 600 | dtstart=eventdt, interval=repeat_every, until=repeat_end) |
|---|
| | 601 | elif repeat_type == MOTO_REPEAT_MONTHLY_DAY: |
|---|
| | 602 | appendXMLTag(doc, e, 'Frequency', 'MONTHLY') |
|---|
| | 603 | if repeat_day: |
|---|
| | 604 | (repeat_nth, repeat_daynum) = moto_repeat_day_to_monthday(repeat_day) |
|---|
| | 605 | else: # default to repeating on the same week/day as the event |
|---|
| | 606 | repeat_nth = weeknum |
|---|
| | 607 | repeat_daynum = eventdt.weekday() |
|---|
| | 608 | appendXMLTag(doc, e, 'ByDay', '%d%s' % (repeat_nth, VCAL_DAYS[repeat_daynum])) |
|---|
| | 609 | rule = rrule.rrule(rrule.MONTHLY, byweekday=rrule.weekdays[repeat_daynum](repeat_nth), |
|---|
| | 610 | dtstart=eventdt, interval=repeat_every, until=repeat_end) |
|---|
| | 611 | elif repeat_type == MOTO_REPEAT_YEARLY_DATE: |
|---|
| | 612 | appendXMLTag(doc, e, 'Frequency', 'YEARLY') |
|---|
| | 613 | appendXMLTag(doc, e, 'ByMonth', str(eventdt.month)) |
|---|
| | 614 | appendXMLTag(doc, e, 'ByMonthDay', str(eventdt.day)) |
|---|
| | 615 | rule = rrule.rrule(rrule.YEARLY, bymonth=eventdt.month, bymonthday=eventdt.day, |
|---|
| | 616 | dtstart=eventdt, interval=repeat_every, until=repeat_end) |
|---|
| | 617 | elif repeat_type == MOTO_REPEAT_YEARLY_DAY: |
|---|
| | 618 | appendXMLTag(doc, e, 'Frequency', 'YEARLY') |
|---|
| | 619 | appendXMLTag(doc, e, 'ByMonth', str(eventdt.month)) |
|---|
| | 620 | if repeat_day: |
|---|
| | 621 | (repeat_nth, repeat_daynum) = moto_repeat_day_to_monthday(repeat_day) |
|---|
| | 622 | else: # default to repeating on the same week/day as the event |
|---|
| | 623 | repeat_nth = weeknum |
|---|
| | 624 | repeat_daynum = eventdt.weekday() |
|---|
| | 625 | appendXMLTag(doc, e, 'ByDay', '%d%s' % (repeat_nth, VCAL_DAYS[repeat_daynum])) |
|---|
| | 626 | rule = rrule.rrule(rrule.YEARLY, bymonth=eventdt.month, |
|---|
| | 627 | byweekday=rrule.weekdays[repeat_daynum](repeat_nth), |
|---|
| | 628 | dtstart=eventdt, interval=repeat_every, until=repeat_end) |
|---|
| | 629 | |
|---|
| | 630 | if repeat_every: |
|---|
| | 631 | appendXMLTag(doc, e, 'Interval', str(repeat_every)) |
|---|
| | 632 | |
|---|
| | 633 | if repeat_end: |
|---|
| | 634 | appendXMLTag(doc, e, 'Until', format_time(repeat_end, VCAL_DATETIME)) |
|---|
| | 635 | |
|---|
| | 636 | if e.hasChildNodes(): |
|---|
| | 637 | ret = [e] |
|---|
| | 638 | else: |
|---|
| | 639 | return [] |
|---|
| | 640 | |
|---|
| | 641 | if exceptions != []: |
|---|
| | 642 | # work out which dates the exceptions correspond to |
|---|
| | 643 | e = doc.createElement('ExceptionDateTime') |
|---|
| | 644 | e.setAttribute('Value', 'DATE') |
|---|
| | 645 | for exnum in exceptions: |
|---|
| | 646 | appendXMLTag(doc, e, 'Content', format_time(rule[exnum], VCAL_DATE)) |
|---|
| | 647 | if e.hasChildNodes(): |
|---|
| | 648 | ret.append(e) |
|---|
| | 649 | |
|---|
| | 650 | return ret |
|---|
| | 651 | |
|---|
| | 652 | def xmlevent_to_moto_simple(node, event): |
|---|
| | 653 | """Parse an XML event (node), setting the fields on a PhoneEvent object (event) |
|---|
| | 654 | |
|---|
| | 655 | Only the common fields between PhoneEventSimple and Extended are handled. |
|---|
| | 656 | Recursion-related fields are ignored. |
|---|
| | 657 | |
|---|
| | 658 | This function raises UnsupportedDataError for: |
|---|
| | 659 | * events before year 2000 (my phone doesn't allow them) |
|---|
| | 660 | """ |
|---|
| | 661 | |
|---|
| | 662 | def getField(tagname, subtag='Content'): |
|---|
| | 663 | """utility function for the XML processing below""" |
|---|
| | 664 | return getXMLField(node, tagname, subtag) |
|---|
| | 665 | |
|---|
| | 666 | event.name = getField('Summary') |
|---|
| | 667 | |
|---|
| | 668 | event.eventdt = parse_ical_time(getField('DateStarted')) |
|---|
| | 669 | if event.eventdt.year < 2000: |
|---|
| | 670 | raise UnsupportedDataError('Event is too old') |
|---|
| | 671 | |
|---|
| | 672 | if node.getElementsByTagName('Duration') != []: |
|---|
| | 673 | duration = node.getElementsByTagName('Duration')[0] |
|---|
| | 674 | def toint(numstr): |
|---|
| | 675 | """Convert a string to an integer, unless it's None, in which case return 0.""" |
|---|
| | 676 | if numstr is None: |
|---|
| | 677 | return 0 |
|---|
| | 678 | else: |
|---|
| | 679 | return int(numstr) |
|---|
| | 680 | weeks = toint(getXMLField(duration, 'Weeks')) |
|---|
| | 681 | days = toint(getXMLField(duration, 'Days')) |
|---|
| | 682 | hours = toint(getXMLField(duration, 'Hours')) |
|---|
| | 683 | mins = toint(getXMLField(duration, 'Minutes')) |
|---|
| | 684 | secs = toint(getXMLField(duration, 'Seconds')) |
|---|
| | 685 | event.duration = timedelta(weeks * 7 + days, (hours * 60 + mins) * 60 + secs) |
|---|
| | 686 | else: |
|---|
| | 687 | endstr = getField('DateEnd') |
|---|
| | 688 | if endstr is not None: |
|---|
| | 689 | event.duration = parse_ical_time(endstr) - event.eventdt |
|---|
| | 690 | else: |
|---|
| | 691 | # no duration or end specified, assume whole-day or no duration |
|---|
| | 692 | if isinstance(event.eventdt, date): |
|---|
| | 693 | event.duration = timedelta(1) |
|---|
| | 694 | else: |
|---|
| | 695 | event.duration = timedelta(0) |
|---|
| | 696 | |
|---|
| | 697 | # for some reason I don't understand, the phone only allows events |
|---|
| | 698 | # longer than a day if the time flag is set. pander to this by forcing |
|---|
| | 699 | # such events to start at midnight local time |
|---|
| | 700 | if isinstance(event.eventdt, date) and event.duration > timedelta(1): |
|---|
| | 701 | local_midnight = datetime_time(0, 0, 0, 0, dateutil.tz.tzlocal()) |
|---|
| | 702 | event.eventdt = datetime.combine(event.eventdt, local_midnight) |
|---|
| | 703 | |
|---|
| | 704 | triggerstr = getField('Alarm', 'AlarmTrigger') |
|---|
| | 705 | if triggerstr is None: |
|---|
| | 706 | event.alarmdt = None |
|---|
| | 707 | else: |
|---|
| | 708 | if triggerstr.startswith('-P') or triggerstr.startswith('P'): |
|---|
| | 709 | offset = parse_ical_duration(triggerstr) |
|---|
| | 710 | event.alarmdt = event.eventdt + offset |
|---|
| | 711 | else: |
|---|
| | 712 | event.alarmdt = parse_ical_time(triggerstr) |
|---|
| 992 | | if self.exceptions != []: |
|---|
| 993 | | assert(self.repeat_type != MOTO_REPEAT_NONE) |
|---|
| 994 | | |
|---|
| 995 | | # create an rrule object for this recurrence |
|---|
| 996 | | if self.repeat_type == MOTO_REPEAT_MONTHLY_DATE: |
|---|
| 997 | | rule = rrule.rrule(rrule.MONTHLY, bymonthday=self.eventdt.day, |
|---|
| 998 | | dtstart=self.eventdt) |
|---|
| 999 | | elif self.repeat_type == MOTO_REPEAT_MONTHLY_DAY: |
|---|
| 1000 | | weekday = rrule.weekdays[self.eventdt.weekday()] |
|---|
| 1001 | | rule = rrule.rrule(rrule.MONTHLY, byweekday=weekday(+weeknum), |
|---|
| 1002 | | dtstart=self.eventdt) |
|---|
| 1003 | | else: |
|---|
| 1004 | | if self.repeat_type == MOTO_REPEAT_DAILY: |
|---|
| 1005 | | freq = rrule.DAILY |
|---|
| 1006 | | elif self.repeat_type == MOTO_REPEAT_WEEKLY: |
|---|
| 1007 | | freq = rrule.WEEKLY |
|---|
| 1008 | | elif self.repeat_type == MOTO_REPEAT_YEARLY: |
|---|
| 1009 | | freq = rrule.YEARLY |
|---|
| 1010 | | rule = rrule.rrule(freq, dtstart=self.eventdt) |
|---|
| 1011 | | |
|---|
| 1012 | | # work out which dates the exceptions correspond to and |
|---|
| 1013 | | # generate ExceptionDate nodes for them |
|---|
| 1014 | | e = doc.createElement('ExceptionDateTime') |
|---|
| 1015 | | e.setAttribute('Value', 'DATE') |
|---|
| 1016 | | for exnum in self.exceptions: |
|---|
| 1017 | | appendXMLChild(doc, e, 'Content', format_time(rule[exnum], VCAL_DATE)) |
|---|
| 1018 | | if e.hasChildNodes(): |
|---|
| 1019 | | top.appendChild(e) |
|---|
| 1020 | | |
|---|
| 1021 | | e = doc.createElement('RecurrenceRule') |
|---|
| 1022 | | if self.repeat_type == MOTO_REPEAT_DAILY: |
|---|
| 1023 | | appendXMLChild(doc, e, 'Frequency', 'DAILY') |
|---|
| 1024 | | elif self.repeat_type == MOTO_REPEAT_WEEKLY: |
|---|
| 1025 | | appendXMLChild(doc, e, 'Frequency', 'WEEKLY') |
|---|
| 1026 | | elif self.repeat_type == MOTO_REPEAT_MONTHLY_DATE: |
|---|
| 1027 | | appendXMLChild(doc, e, 'Frequency', 'MONTHLY') |
|---|
| 1028 | | appendXMLChild(doc, e, 'ByMonthDay', str(self.eventdt.day)) |
|---|
| 1029 | | elif self.repeat_type == MOTO_REPEAT_MONTHLY_DAY: |
|---|
| 1030 | | appendXMLChild(doc, e, 'Frequency', 'MONTHLY') |
|---|
| 1031 | | day = VCAL_DAYS[self.eventdt.weekday()] |
|---|
| 1032 | | appendXMLChild(doc, e, 'ByDay', '%d%s' % (weeknum, day)) |
|---|
| 1033 | | elif self.repeat_type == MOTO_REPEAT_YEARLY: |
|---|
| 1034 | | appendXMLChild(doc, e, 'Frequency', 'YEARLY') |
|---|
| 1035 | | appendXMLChild(doc, e, 'ByMonth', str(self.eventdt.month)) |
|---|
| 1036 | | appendXMLChild(doc, e, 'ByMonthDay', str(self.eventdt.day)) |
|---|
| 1037 | | |
|---|
| 1038 | | if e.hasChildNodes(): |
|---|
| 1039 | | top.appendChild(e) |
|---|
| 1040 | | |
|---|
| 1078 | | class PhoneEventXML(PhoneEvent): |
|---|
| 1079 | | """Constructor for the PhoneEvent object with data in OpenSync XML format""" |
|---|
| 1080 | | def __init__(self, data): |
|---|
| 1081 | | """Parse XML event data. |
|---|
| 1082 | | |
|---|
| 1083 | | This function raises UnsupportedDataError for: |
|---|
| 1084 | | * events that recur but can't be represented on the phone |
|---|
| 1085 | | * events before year 2000 (my phone doesn't allow them) |
|---|
| | 1355 | class PhoneEventSimpleXML(PhoneEventSimple): |
|---|
| | 1356 | """Constructor for the PhoneEventSimple object with data in OpenSync XML format""" |
|---|
| | 1357 | def __init__(self, xmldata): |
|---|
| | 1358 | """Parse XML event data.""" |
|---|
| | 1359 | PhoneEventSimple.__init__(self) |
|---|
| | 1360 | doc = xml.dom.minidom.parseString(xmldata) |
|---|
| | 1361 | node = doc.getElementsByTagName('event')[0] |
|---|
| | 1362 | |
|---|
| | 1363 | xmlevent_to_moto_simple(node, self) |
|---|
| | 1364 | |
|---|
| | 1365 | rrules = node.getElementsByTagName('RecurrenceRule') |
|---|
| | 1366 | exdates = node.getElementsByTagName('ExceptionDateTime') |
|---|
| | 1367 | exrules = node.getElementsByTagName('ExceptionRule') |
|---|
| | 1368 | |
|---|
| | 1369 | rule = xml_rrule_to_moto(rrules, exdates, exrules, self.eventdt) |
|---|
| | 1370 | if (rule['repeat_type'] == MOTO_REPEAT_YEARLY_DAY or rule['repeat_every'] not in [0, 1] |
|---|
| | 1371 | or rule['repeat_day'] != 0 or rule['repeat_end']): |
|---|
| | 1372 | raise UnsupportedDataError("Recursion rule not supported by simple event format") |
|---|
| | 1373 | |
|---|
| | 1374 | self.repeat_type = rule['repeat_type'] |
|---|
| | 1375 | self.exceptions = rule['exceptions'] |
|---|
| | 1376 | |
|---|
| | 1377 | |
|---|
| | 1378 | class PhoneEventExtended(PhoneEventSimple): |
|---|
| | 1379 | """Class representing the extended version of the event data supported by |
|---|
| | 1380 | newer phone models. |
|---|
| | 1381 | |
|---|
| | 1382 | This class should not be instantiated directly, use one of |
|---|
| | 1383 | PhoneEventExtendedMoto or PhoneEventExtendedXML depending on the data format. |
|---|
| | 1384 | |
|---|
| | 1385 | class members (in addition to those defined by PhoneEventSimple): |
|---|
| | 1386 | event_type: integer 0-15 |
|---|
| | 1387 | location: string |
|---|
| | 1388 | note: string (stores XML description field) |
|---|
| | 1389 | repeat_every: integer |
|---|
| | 1390 | repeat_day: integer |
|---|
| | 1391 | repeat_end: date object or None, for repeat end date |
|---|
| | 1392 | """ |
|---|
| | 1393 | def __init__(self): |
|---|
| | 1394 | PhoneEventSimple.__init__(self) |
|---|
| | 1395 | self.event_type = MOTO_EVENT_TYPE_NONE |
|---|
| | 1396 | self.location = self.note = '' |
|---|
| | 1397 | self.repeat_every = self.repeat_day = 0 |
|---|
| | 1398 | self.repeat_end = None |
|---|
| | 1399 | |
|---|
| | 1400 | @staticmethod |
|---|
| | 1401 | def capabilities(): |
|---|
| | 1402 | """Return the OpenSync XML capabilities supported by this object.""" |
|---|
| | 1403 | caps = """ |
|---|
| | 1404 | <event> |
|---|
| | 1405 | <Alarm /> |
|---|
| | 1406 | <Categories /> |
|---|
| | 1407 | <DateEnd /> |
|---|
| | 1408 | <DateStarted /> |
|---|
| | 1409 | <Description /> |
|---|
| | 1410 | <Duration /> |
|---|
| | 1411 | <ExceptionDateTime /> |
|---|
| | 1412 | <Location /> |
|---|
| | 1413 | <RecurrenceRule /> |
|---|
| | 1414 | <Summary /> |
|---|
| | 1415 | </event> |
|---|
| 1087 | | PhoneEvent.__init__(self) |
|---|
| 1088 | | doc = xml.dom.minidom.parseString(data) |
|---|
| 1089 | | event = doc.getElementsByTagName('event')[0] |
|---|
| 1090 | | |
|---|
| 1091 | | def getField(tagname, subtag='Content'): |
|---|
| 1092 | | """utility function for the XML processing below""" |
|---|
| 1093 | | return getXMLField(event, tagname, subtag) |
|---|
| 1094 | | |
|---|
| 1095 | | self.pos = None |
|---|
| 1096 | | self.name = getField('Summary') |
|---|
| 1097 | | |
|---|
| 1098 | | self.eventdt = parse_ical_time(getField('DateStarted')) |
|---|
| 1099 | | if self.eventdt.year < 2000: |
|---|
| 1100 | | raise UnsupportedDataError('Event is too old') |
|---|
| 1101 | | |
|---|
| 1102 | | if event.getElementsByTagName('Duration') != []: |
|---|
| 1103 | | duration = event.getElementsByTagName('Duration')[0] |
|---|
| 1104 | | def toint(numstr): |
|---|
| 1105 | | """Convert a string to an integer, unless it's None, in which case return 0.""" |
|---|
| 1106 | | if numstr is None: |
|---|
| 1107 | | return 0 |
|---|
| 1108 | | else: |
|---|
| 1109 | | return int(numstr) |
|---|
| 1110 | | weeks = toint(getXMLField(duration, 'Weeks')) |
|---|
| 1111 | | days = toint(getXMLField(duration, 'Days')) |
|---|
| 1112 | | hours = toint(getXMLField(duration, 'Hours')) |
|---|
| 1113 | | mins = toint(getXMLField(duration, 'Minutes')) |
|---|
| 1114 | | secs = toint(getXMLField(duration, 'Seconds')) |
|---|
| 1115 | | self.duration = timedelta(weeks * 7 + days, (hours * 60 + mins) * 60 + secs) |
|---|
| 1116 | | else: |
|---|
| 1117 | | endstr = getField('DateEnd') |
|---|
| 1118 | | if endstr is not None: |
|---|
| 1119 | | self.duration = parse_ical_time(endstr) - self.eventdt |
|---|
| 1120 | | else: |
|---|
| 1121 | | # no duration or end specified, assume whole-day or no duration |
|---|
| 1122 | | if isinstance(self.eventdt, date): |
|---|
| 1123 | | self.duration = timedelta(1) |
|---|
| 1124 | | else: |
|---|
| 1125 | | self.duration = timedelta(0) |
|---|
| 1126 | | |
|---|
| 1127 | | # for some reason I don't understand, the phone only allows events |
|---|
| 1128 | |
|---|