# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Calendar < ApplicationModel include ChecksClientNotification include CanUniqName store :business_hours store :public_holidays before_create :validate_public_holidays, :fetch_ical before_update :validate_public_holidays, :fetch_ical after_create :sync_default, :min_one_check after_update :sync_default, :min_one_check after_destroy :min_one_check =begin set inital default calendar calendar = Calendar.init_setup returns calendar object =end def self.init_setup(ip = nil) # ignore client ip if not public ip if ip && ip =~ /^(::1|127\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|192\.168\.)/ ip = nil end # prevent multible setups for same ip cache = Cache.get('Calendar.init_setup.done') return if cache && cache[:ip] == ip Cache.write('Calendar.init_setup.done', { ip: ip }, { expires_in: 1.hour }) # call for calendar suggestion calendar_details = Service::GeoCalendar.location(ip) return if !calendar_details calendar_details['name'] = Calendar.generate_uniq_name(calendar_details['name']) calendar_details['default'] = true calendar_details['created_by_id'] = 1 calendar_details['updated_by_id'] = 1 # find if auto generated calendar exists calendar = Calendar.find_by(default: true, updated_by_id: 1, created_by_id: 1) if calendar calendar.update!(calendar_details) return calendar end create(calendar_details) end =begin get default calendar calendar = Calendar.default returns calendar object =end def self.default find_by(default: true) end =begin returns preset of ical feeds feeds = Calendar.ical_feeds returns { 'http://www.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics' => 'US', ... } =end def self.ical_feeds data = YAML.load_file(Rails.root.join('config/holiday_calendars.yml')) url = data['url'] data['countries'].map do |country, domain| [(url % { domain: domain }), country] end.to_h end =begin get list of available timezones and UTC offsets list = Calendar.timezones returns { 'America/Los_Angeles' => -7 ... } =end def self.timezones list = {} TZInfo::Timezone.all_country_zone_identifiers.each do |timezone| t = TZInfo::Timezone.get(timezone) diff = t.current_period.utc_total_offset / 60 / 60 list[ timezone ] = diff end list end =begin syn all calendars with ical feeds success = Calendar.sync returns true # or false =end def self.sync Calendar.find_each(&:sync) true end =begin syn one calendars with ical feed calendar = Calendar.find(4711) success = calendar.sync returns true # or false =end def sync(without_save = nil) return if !ical_url # only sync every 5 days if id cache_key = "CalendarIcal::#{id}" cache = Cache.get(cache_key) return if !last_log && cache && cache[:ical_url] == ical_url end begin events = {} if ical_url.present? events = Calendar.fetch_parse(ical_url) end # sync with public_holidays if !public_holidays self.public_holidays = {} end # remove old ical entries if feed has changed public_holidays.each do |day, meta| next if !public_holidays[day]['feed'] next if meta['feed'] == Digest::MD5.hexdigest(ical_url) public_holidays.delete(day) end # sync new ical feed dates events.each do |day, summary| if !public_holidays[day] public_holidays[day] = {} end # ignore if already added or changed next if public_holidays[day].key?('active') # create new entry public_holidays[day] = { active: true, summary: summary, feed: Digest::MD5.hexdigest(ical_url) } end self.last_log = nil if id Cache.write( cache_key, { public_holidays: public_holidays, ical_url: ical_url }, { expires_in: 1.day }, ) end rescue => e self.last_log = e.inspect end self.last_sync = Time.zone.now if !without_save save end true end def self.fetch_parse(location) if location =~ /^http/i result = UserAgent.get(location) if !result.success? raise result.error end cal_file = result.body else cal_file = File.open(location) end cals = Icalendar::Calendar.parse(cal_file) cal = cals.first events = {} cal.events.each do |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 do |occurrence| result = Calendar.day_and_comment_by_event(event, occurrence.start_time) next if !result events[result[0]] = result[1] end end end next if event.dtstart < Time.zone.now - 1.year next if event.dtstart > Time.zone.now + 3.years result = Calendar.day_and_comment_by_event(event, event.dtstart) next if !result events[result[0]] = result[1] end events.sort.to_h 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 # if changed calendar is default, set all others default to false def sync_default return true if !default Calendar.find_each do |calendar| next if calendar.id == id next if !calendar.default calendar.default = false calendar.save end true end # check if min one is set to default true def min_one_check if !Calendar.find_by(default: true) first = Calendar.order(:created_at, :id).limit(1).first return true if !first first.default = true first.save end # check if sla's are refer to an existing calendar default_calendar = Calendar.find_by(default: true) Sla.find_each do |sla| if !sla.calendar_id sla.calendar_id = default_calendar.id sla.save! next end if !Calendar.find_by(id: sla.calendar_id) sla.calendar_id = default_calendar.id sla.save! end end true end # fetch ical feed def fetch_ical sync(true) true end # validate format of public holidays def validate_public_holidays # fillup feed info before = public_holidays_was public_holidays.each do |day, meta| if before && before[day] && before[day]['feed'] meta['feed'] = before[day]['feed'] end meta['active'] = if meta['active'] true else false end end true end end