Fixed issue #1361 - Calendar: ICAL sources are not updated. Implemented issue #1362 - Calendar: ICAL ignores recurring appointments/events.

This commit is contained in:
Martin Edenhofer 2017-08-24 18:59:53 +02:00
parent ec94c72a6b
commit 0cd9fd7fcd
7 changed files with 398 additions and 22 deletions

View file

@ -74,6 +74,7 @@ gem 'argon2'
gem 'writeexcel' gem 'writeexcel'
gem 'icalendar' gem 'icalendar'
gem 'icalendar-recurrence'
gem 'browser' gem 'browser'
# integrations # integrations

View file

@ -185,6 +185,10 @@ GEM
httpclient (2.8.3) httpclient (2.8.3)
i18n (0.8.6) i18n (0.8.6)
icalendar (2.4.1) icalendar (2.4.1)
icalendar-recurrence (1.1.2)
icalendar (~> 2.0)
ice_cube (~> 0.13)
ice_cube (0.16.2)
inflection (1.0.0) inflection (1.0.0)
json (1.8.6) json (1.8.6)
jwt (1.5.6) jwt (1.5.6)
@ -481,6 +485,7 @@ DEPENDENCIES
guard-symlink guard-symlink
htmlentities htmlentities
icalendar icalendar
icalendar-recurrence
json json
koala koala
libv8 libv8

View file

@ -150,14 +150,16 @@ returns
return if !ical_url return if !ical_url
# only sync every 5 days # only sync every 5 days
cache_key = "CalendarIcal::#{id}" if id
cache = Cache.get(cache_key) cache_key = "CalendarIcal::#{id}"
return if !last_log && cache && cache[:ical_url] == ical_url cache = Cache.get(cache_key)
return if !last_log && cache && cache[:ical_url] == ical_url
end
begin begin
events = {} events = {}
if ical_url && !ical_url.empty? if ical_url.present?
events = Calendar.parse(ical_url) events = Calendar.fetch_parse(ical_url)
end end
# sync with public_holidays # sync with public_holidays
@ -189,11 +191,13 @@ returns
} }
} }
self.last_log = nil self.last_log = nil
cache = Cache.write( if id
cache_key, Cache.write(
{ public_holidays: public_holidays, ical_url: ical_url }, cache_key,
{ expires_in: 5.days }, { public_holidays: public_holidays, ical_url: ical_url },
) { expires_in: 1.day },
)
end
rescue => e rescue => e
self.last_log = e.inspect self.last_log = e.inspect
end end
@ -205,7 +209,7 @@ returns
true true
end end
def self.parse(location) def self.fetch_parse(location)
if location =~ /^http/i if location =~ /^http/i
result = UserAgent.get(location) result = UserAgent.get(location)
if !result.success? if !result.success?
@ -220,22 +224,43 @@ returns
cal = cals.first cal = cals.first
events = {} events = {}
cal.events.each { |event| cal.events.each { |event|
if event.rrule
# loop till days
interval_frame_start = Date.parse("#{Time.zone.now - 1.year}-01-01")
interval_frame_end = Date.parse("#{Time.zone.now + 3.years}-12-31")
occurrences = event.occurrences_between(interval_frame_start, interval_frame_end)
if occurrences.present?
occurrences.each { |occurrence|
result = Calendar.day_and_comment_by_event(event, occurrence.start_time)
next if !result
events[result[0]] = result[1]
}
end
end
next if event.dtstart < Time.zone.now - 1.year next if event.dtstart < Time.zone.now - 1.year
next if event.dtstart > Time.zone.now + 3.years next if event.dtstart > Time.zone.now + 3.years
day = "#{event.dtstart.year}-#{format('%02d', event.dtstart.month)}-#{format('%02d', event.dtstart.day)}" result = Calendar.day_and_comment_by_event(event, event.dtstart)
comment = event.summary || event.description next if !result
comment = Encode.conv( 'utf8', comment.to_s.force_encoding('utf-8') ) events[result[0]] = result[1]
if !comment.valid_encoding?
comment = comment.encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?')
end
# ignore daylight saving time entries
next if comment =~ /(daylight saving|sommerzeit|summertime)/i
events[day] = comment
} }
events.sort.to_h events.sort.to_h
end end
# get day and comment by event
def self.day_and_comment_by_event(event, start_time)
day = "#{start_time.year}-#{format('%02d', start_time.month)}-#{format('%02d', start_time.day)}"
comment = event.summary || event.description
comment = Encode.conv( 'utf8', comment.to_s.force_encoding('utf-8') )
if !comment.valid_encoding?
comment = comment.encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?')
end
# ignore daylight saving time entries
return if comment =~ /(daylight saving|sommerzeit|summertime)/i
[day, comment]
end
private private
# if changed calendar is default, set all others default to false # if changed calendar is default, set all others default to false
@ -254,6 +279,7 @@ returns
def min_one_check def min_one_check
if !Calendar.find_by(default: true) if !Calendar.find_by(default: true)
first = Calendar.order(:created_at, :id).limit(1).first first = Calendar.order(:created_at, :id).limit(1).first
return true if !first
first.default = true first.default = true
first.save first.save
end end

30
test/fixtures/calendar1.ics vendored Normal file
View file

@ -0,0 +1,30 @@
BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:test2
PRODID:-//Apple Inc.//Mac OS X 10.12.6//EN
X-APPLE-CALENDAR-COLOR:#CC73E1
X-WR-TIMEZONE:Europe/Berlin
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20170824T123259Z
UID:D49093CD-2D9D-4B79-9DDA-79E1528B010E
RRULE:FREQ=YEARLY;INTERVAL=1
DTEND;VALUE=DATE:20121225
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:Christmas1
DTSTART;VALUE=DATE:20121224
DTSTAMP:20170824T123314Z
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
UID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
TRIGGER:-PT15H
ATTACH;VALUE=URI:Basso
X-APPLE-LOCAL-DEFAULT-ALARM:TRUE
ACTION:AUDIO
X-APPLE-DEFAULT-ALARM:TRUE
END:VALARM
END:VEVENT
END:VCALENDAR

51
test/fixtures/calendar2.ics vendored Normal file
View file

@ -0,0 +1,51 @@
BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:test2
PRODID:-//Apple Inc.//Mac OS X 10.12.6//EN
X-APPLE-CALENDAR-COLOR:#CC73E1
X-WR-TIMEZONE:Europe/Berlin
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20170824T123259Z
UID:D49093CD-2D9D-4B79-9DDA-79E1528B010E
RRULE:FREQ=YEARLY;INTERVAL=1
DTEND;VALUE=DATE:20121225
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:Christmas1
DTSTART;VALUE=DATE:20121224
DTSTAMP:20170824T123314Z
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
UID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
TRIGGER:-PT15H
ATTACH;VALUE=URI:Basso
X-APPLE-LOCAL-DEFAULT-ALARM:TRUE
ACTION:AUDIO
X-APPLE-DEFAULT-ALARM:TRUE
END:VALARM
END:VEVENT
BEGIN:VEVENT
CREATED:20170824T123259Z
UID:D49093CD-2D9D-4B79-9DDA-79E1528B010G
RRULE:FREQ=YEARLY;INTERVAL=1
DTEND;VALUE=DATE:20121225
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:Christmas2
DTSTART;VALUE=DATE:20121225
DTSTAMP:20170824T123314Z
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
UID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
TRIGGER:-PT15H
ATTACH;VALUE=URI:Basso
X-APPLE-LOCAL-DEFAULT-ALARM:TRUE
ACTION:AUDIO
X-APPLE-DEFAULT-ALARM:TRUE
END:VALARM
END:VEVENT
END:VCALENDAR

111
test/fixtures/calendar3.ics vendored Normal file
View file

@ -0,0 +1,111 @@
BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:test2
PRODID:-//Apple Inc.//Mac OS X 10.12.6//EN
X-APPLE-CALENDAR-COLOR:#CC73E1
X-WR-TIMEZONE:Europe/Berlin
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20170824T123259Z
UID:D49093CD-2D9D-4B79-9DDA-79E1528B010E
RRULE:FREQ=YEARLY;INTERVAL=1
DTEND;VALUE=DATE:20121225
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:Christmas1
DTSTART;VALUE=DATE:20121224
DTSTAMP:20170824T123314Z
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
UID:D8B0F2C4-B45E-4D1B-9DC5-80DCDF5F06C1
TRIGGER:-PT15H
ATTACH;VALUE=URI:Basso
X-APPLE-LOCAL-DEFAULT-ALARM:TRUE
ACTION:AUDIO
X-APPLE-DEFAULT-ALARM:TRUE
END:VALARM
END:VEVENT
BEGIN:VEVENT
CREATED:20170824T123409Z
UID:B461611E-DF33-42BA-B40E-DF32FAD3BA92
RRULE:FREQ=MONTHLY;INTERVAL=1;COUNT=10
DTEND;TZID=Europe/Berlin:20121228T100000
TRANSP:OPAQUE
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:day4
DTSTART;TZID=Europe/Berlin:20121228T090000
DTSTAMP:20170824T134117Z
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
CREATED:20170824T123419Z
UID:4F1F0444-9F71-4FDD-9D4A-EBA6BF1EF72E
RRULE:FREQ=MONTHLY;INTERVAL=1;COUNT=5
DTEND;VALUE=DATE:20121227
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:day3
DTSTART;VALUE=DATE:20161226
DTSTAMP:20170824T134106Z
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:118EB330-738F-4956-9FAD-D548462CC26A
UID:118EB330-738F-4956-9FAD-D548462CC26A
TRIGGER:-PT15H
ATTACH;VALUE=URI:Basso
X-APPLE-LOCAL-DEFAULT-ALARM:TRUE
ACTION:AUDIO
X-APPLE-DEFAULT-ALARM:TRUE
END:VALARM
END:VEVENT
BEGIN:VEVENT
CREATED:20170824T134043Z
UID:3606DA64-D734-434A-A430-E2AC7ADECACF
DTEND;TZID=Europe/Berlin:20121226T100000
TRANSP:OPAQUE
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:day2
DTSTART;TZID=Europe/Berlin:20121226T090000
DTSTAMP:20170824T134043Z
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
CREATED:20170824T134127Z
UID:7B5AB905-39EA-42B6-AD31-1E63D4C5C2C7
DTEND;VALUE=DATE:20121229
TRANSP:TRANSPARENT
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:day5
DTSTART;VALUE=DATE:20161228
DTSTAMP:20170824T134134Z
SEQUENCE:0
BEGIN:VALARM
X-WR-ALARMUID:45BF1D7B-3D2C-48DC-AB5C-BB391AFA7837
UID:45BF1D7B-3D2C-48DC-AB5C-BB391AFA7837
TRIGGER:-PT15H
ATTACH;VALUE=URI:Basso
X-APPLE-LOCAL-DEFAULT-ALARM:TRUE
ACTION:AUDIO
X-APPLE-DEFAULT-ALARM:TRUE
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -3,7 +3,7 @@ require 'test_helper'
class CalendarTest < ActiveSupport::TestCase class CalendarTest < ActiveSupport::TestCase
test 'default test' do test 'default test' do
Calendar.delete_all Calendar.destroy_all
calendar1 = Calendar.create_or_update( calendar1 = Calendar.create_or_update(
name: 'US 1', name: 'US 1',
timezone: 'America/Los_Angeles', timezone: 'America/Los_Angeles',
@ -91,4 +91,156 @@ class CalendarTest < ActiveSupport::TestCase
travel_back travel_back
end end
test 'sync test' do
Calendar.destroy_all
travel_to Time.zone.parse('2017-08-24T01:04:44Z0')
calendar1 = Calendar.create_or_update(
name: 'Sync 1',
timezone: 'America/Los_Angeles',
business_hours: {
mon: { '09:00' => '17:00' },
tue: { '09:00' => '17:00' },
wed: { '09:00' => '17:00' },
thu: { '09:00' => '17:00' },
fri: { '09:00' => '17:00' }
},
default: true,
ical_url: 'test/fixtures/calendar1.ics',
updated_by_id: 1,
created_by_id: 1,
)
assert_equal(true, calendar1.public_holidays['2016-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2016-12-24']['summary'])
assert_nil(calendar1.public_holidays['2016-12-25'])
assert_equal(true, calendar1.public_holidays['2017-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2017-12-24']['summary'])
assert_nil(calendar1.public_holidays['2017-12-25'])
assert_equal(true, calendar1.public_holidays['2018-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2018-12-24']['summary'])
assert_nil(calendar1.public_holidays['2018-12-25'])
assert_equal(true, calendar1.public_holidays['2019-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2019-12-24']['summary'])
assert_nil(calendar1.public_holidays['2019-12-25'])
assert_nil(calendar1.public_holidays['2020-12-24'])
Calendar.sync
assert_equal(true, calendar1.public_holidays['2016-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2016-12-24']['summary'])
assert_nil(calendar1.public_holidays['2016-12-25'])
assert_equal(true, calendar1.public_holidays['2017-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2017-12-24']['summary'])
assert_nil(calendar1.public_holidays['2017-12-25'])
assert_equal(true, calendar1.public_holidays['2018-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2018-12-24']['summary'])
assert_nil(calendar1.public_holidays['2018-12-25'])
assert_equal(true, calendar1.public_holidays['2019-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2019-12-24']['summary'])
assert_nil(calendar1.public_holidays['2019-12-25'])
assert_nil(calendar1.public_holidays['2020-12-24'])
cache_key = "CalendarIcal::#{calendar1.id}"
cache = Cache.get(cache_key)
calendar1.update_columns(ical_url: 'test/fixtures/calendar2.ics')
cache_key = "CalendarIcal::#{calendar1.id}"
cache = Cache.get(cache_key)
cache[:ical_url] = 'test/fixtures/calendar2.ics'
Cache.write(
cache_key,
cache,
{ expires_in: 1.day },
)
Calendar.sync
calendar1.reload
assert_equal(true, calendar1.public_holidays['2016-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2016-12-24']['summary'])
assert_nil(calendar1.public_holidays['2016-12-25'])
assert_equal(true, calendar1.public_holidays['2017-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2017-12-24']['summary'])
assert_nil(calendar1.public_holidays['2017-12-25'])
assert_equal(true, calendar1.public_holidays['2018-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2018-12-24']['summary'])
assert_nil(calendar1.public_holidays['2018-12-25'])
assert_equal(true, calendar1.public_holidays['2019-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2019-12-24']['summary'])
assert_nil(calendar1.public_holidays['2019-12-25'])
assert_nil(calendar1.public_holidays['2020-12-24'])
travel 2.days
Calendar.sync
calendar1.reload
assert_equal(true, calendar1.public_holidays['2016-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2016-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2016-12-25']['active'])
assert_equal('Christmas2', calendar1.public_holidays['2016-12-25']['summary'])
assert_equal(true, calendar1.public_holidays['2017-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2017-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2017-12-25']['active'])
assert_equal('Christmas2', calendar1.public_holidays['2017-12-25']['summary'])
assert_equal(true, calendar1.public_holidays['2018-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2018-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2018-12-25']['active'])
assert_equal('Christmas2', calendar1.public_holidays['2018-12-25']['summary'])
assert_equal(true, calendar1.public_holidays['2019-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2019-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2019-12-25']['active'])
assert_equal('Christmas2', calendar1.public_holidays['2019-12-25']['summary'])
assert_nil(calendar1.public_holidays['2020-12-24'])
assert_nil(calendar1.public_holidays['2020-12-25'])
Calendar.destroy_all
calendar1 = Calendar.create_or_update(
name: 'Sync 2',
timezone: 'America/Los_Angeles',
business_hours: {
mon: { '09:00' => '17:00' },
tue: { '09:00' => '17:00' },
wed: { '09:00' => '17:00' },
thu: { '09:00' => '17:00' },
fri: { '09:00' => '17:00' }
},
default: true,
ical_url: 'test/fixtures/calendar3.ics',
updated_by_id: 1,
created_by_id: 1,
)
assert_equal(true, calendar1.public_holidays['2016-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2016-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2016-12-26']['active'])
assert_equal('day3', calendar1.public_holidays['2016-12-26']['summary'])
assert_equal(true, calendar1.public_holidays['2016-12-28']['active'])
assert_equal('day5', calendar1.public_holidays['2016-12-28']['summary'])
assert_equal(true, calendar1.public_holidays['2017-01-26']['active'])
assert_equal('day3', calendar1.public_holidays['2017-01-26']['summary'])
assert_equal(true, calendar1.public_holidays['2017-02-26']['active'])
assert_equal('day3', calendar1.public_holidays['2017-02-26']['summary'])
assert_equal(true, calendar1.public_holidays['2017-03-26']['active'])
assert_equal('day3', calendar1.public_holidays['2017-03-26']['summary'])
assert_equal(true, calendar1.public_holidays['2017-04-26']['active'])
assert_equal('day3', calendar1.public_holidays['2017-04-26']['summary'])
assert_nil(calendar1.public_holidays['2017-05-26'])
assert_equal(true, calendar1.public_holidays['2017-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2017-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2018-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2018-12-24']['summary'])
assert_equal(true, calendar1.public_holidays['2019-12-24']['active'])
assert_equal('Christmas1', calendar1.public_holidays['2019-12-24']['summary'])
assert_nil(calendar1.public_holidays['2020-12-24'])
Calendar.destroy_all
travel_back
end
end end