calendar.rb 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. class Calendar < ApplicationModel
  3. include ChecksClientNotification
  4. include CanUniqName
  5. store :business_hours
  6. store :public_holidays
  7. before_create :validate_public_holidays, :validate_hours, :fetch_ical
  8. before_update :validate_public_holidays, :validate_hours, :fetch_ical
  9. after_create :sync_default, :min_one_check
  10. after_update :sync_default, :min_one_check
  11. after_destroy :min_one_check
  12. =begin
  13. set initial default calendar
  14. calendar = Calendar.init_setup
  15. returns calendar object
  16. =end
  17. def self.init_setup(ip = nil)
  18. # ignore client ip if not public ip
  19. if ip && ip =~ /^(::1|127\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|192\.168\.)/
  20. ip = nil
  21. end
  22. # prevent multiple setups for same ip
  23. cache = Cache.get('Calendar.init_setup.done')
  24. return if cache && cache[:ip] == ip
  25. Cache.write('Calendar.init_setup.done', { ip: ip }, { expires_in: 1.hour })
  26. # call for calendar suggestion
  27. calendar_details = Service::GeoCalendar.location(ip)
  28. return if calendar_details.blank?
  29. return if calendar_details['name'].blank?
  30. return if calendar_details['business_hours'].blank?
  31. calendar_details['name'] = Calendar.generate_uniq_name(calendar_details['name'])
  32. calendar_details['default'] = true
  33. calendar_details['created_by_id'] = 1
  34. calendar_details['updated_by_id'] = 1
  35. # find if auto generated calendar exists
  36. calendar = Calendar.find_by(default: true, updated_by_id: 1, created_by_id: 1)
  37. if calendar
  38. calendar.update!(calendar_details)
  39. return calendar
  40. end
  41. create(calendar_details)
  42. end
  43. =begin
  44. get default calendar
  45. calendar = Calendar.default
  46. returns calendar object
  47. =end
  48. def self.default
  49. find_by(default: true)
  50. end
  51. =begin
  52. returns preset of ical feeds
  53. feeds = Calendar.ical_feeds
  54. returns
  55. {
  56. 'http://www.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics' => 'US',
  57. ...
  58. }
  59. =end
  60. def self.ical_feeds
  61. data = YAML.load_file(Rails.root.join('config/holiday_calendars.yml'))
  62. url = data['url']
  63. data['countries'].map do |country, domain|
  64. [format(url, domain: domain), country]
  65. end.to_h
  66. end
  67. =begin
  68. get list of available timezones and UTC offsets
  69. list = Calendar.timezones
  70. returns
  71. {
  72. 'America/Los_Angeles' => -7
  73. ...
  74. }
  75. =end
  76. def self.timezones
  77. list = {}
  78. TZInfo::Timezone.all_country_zone_identifiers.each do |timezone|
  79. t = TZInfo::Timezone.get(timezone)
  80. diff = t.current_period.utc_total_offset / 60 / 60
  81. list[ timezone ] = diff
  82. end
  83. list
  84. end
  85. =begin
  86. syn all calendars with ical feeds
  87. success = Calendar.sync
  88. returns
  89. true # or false
  90. =end
  91. def self.sync
  92. Calendar.find_each(&:sync)
  93. true
  94. end
  95. =begin
  96. syn one calendars with ical feed
  97. calendar = Calendar.find(4711)
  98. success = calendar.sync
  99. returns
  100. true # or false
  101. =end
  102. def sync(without_save = nil)
  103. return if !ical_url
  104. # only sync every 5 days
  105. if id
  106. cache_key = "CalendarIcal::#{id}"
  107. cache = Cache.get(cache_key)
  108. return if !last_log && cache && cache[:ical_url] == ical_url
  109. end
  110. begin
  111. events = {}
  112. if ical_url.present?
  113. events = Calendar.fetch_parse(ical_url)
  114. end
  115. # sync with public_holidays
  116. self.public_holidays ||= {}
  117. # remove old ical entries if feed has changed
  118. public_holidays.each do |day, meta|
  119. next if !public_holidays[day]['feed']
  120. next if meta['feed'] == Digest::MD5.hexdigest(ical_url)
  121. public_holidays.delete(day)
  122. end
  123. # sync new ical feed dates
  124. events.each do |day, summary|
  125. public_holidays[day] ||= {}
  126. # ignore if already added or changed
  127. next if public_holidays[day].key?('active')
  128. # entry already exists
  129. next if summary == public_holidays[day][:summary]
  130. # create new entry
  131. public_holidays[day] = {
  132. active: true,
  133. summary: summary,
  134. feed: Digest::MD5.hexdigest(ical_url)
  135. }
  136. end
  137. self.last_log = nil
  138. if id
  139. Cache.write(
  140. cache_key,
  141. { public_holidays: public_holidays, ical_url: ical_url },
  142. { expires_in: 1.day },
  143. )
  144. end
  145. rescue => e
  146. self.last_log = e.inspect
  147. end
  148. self.last_sync = Time.zone.now
  149. if !without_save
  150. save
  151. end
  152. true
  153. end
  154. def self.fetch_parse(location)
  155. if location.match?(/^http/i)
  156. result = UserAgent.get(location)
  157. if !result.success?
  158. raise result.error
  159. end
  160. cal_file = result.body
  161. else
  162. cal_file = File.open(location)
  163. end
  164. cals = Icalendar::Calendar.parse(cal_file)
  165. cal = cals.first
  166. events = {}
  167. cal.events.each do |event|
  168. if event.rrule
  169. # loop till days
  170. interval_frame_start = Date.parse("#{Time.zone.now - 1.year}-01-01")
  171. interval_frame_end = Date.parse("#{Time.zone.now + 3.years}-12-31")
  172. occurrences = event.occurrences_between(interval_frame_start, interval_frame_end)
  173. if occurrences.present?
  174. occurrences.each do |occurrence|
  175. result = Calendar.day_and_comment_by_event(event, occurrence.start_time)
  176. next if !result
  177. events[result[0]] = result[1]
  178. end
  179. end
  180. end
  181. next if event.dtstart < Time.zone.now - 1.year
  182. next if event.dtstart > Time.zone.now + 3.years
  183. result = Calendar.day_and_comment_by_event(event, event.dtstart)
  184. next if !result
  185. events[result[0]] = result[1]
  186. end
  187. events.sort.to_h
  188. end
  189. # get day and comment by event
  190. def self.day_and_comment_by_event(event, start_time)
  191. day = "#{start_time.year}-#{format('%<month>02d', month: start_time.month)}-#{format('%<day>02d', day: start_time.day)}"
  192. comment = event.summary || event.description
  193. comment = comment.to_utf8(fallback: :read_as_sanitized_binary)
  194. # ignore daylight saving time entries
  195. return if comment.match?(/(daylight saving|sommerzeit|summertime)/i)
  196. [day, comment]
  197. end
  198. =begin
  199. calendar = Calendar.find(123)
  200. calendar.business_hours_to_hash
  201. returns
  202. {
  203. mon: {'09:00' => '18:00'},
  204. tue: {'09:00' => '18:00'},
  205. wed: {'09:00' => '18:00'},
  206. thu: {'09:00' => '18:00'},
  207. sat: {'09:00' => '18:00'}
  208. }
  209. =end
  210. def business_hours_to_hash
  211. hours = {}
  212. business_hours.each do |day, meta|
  213. next if !meta[:active]
  214. next if !meta[:timeframes]
  215. hours[day.to_sym] = {}
  216. meta[:timeframes].each do |frame|
  217. next if !frame[0]
  218. next if !frame[1]
  219. hours[day.to_sym][frame[0]] = frame[1]
  220. end
  221. end
  222. hours
  223. end
  224. =begin
  225. calendar = Calendar.find(123)
  226. calendar.public_holidays_to_array
  227. returns
  228. [
  229. Thu, 08 Mar 2020,
  230. Sun, 25 Mar 2020,
  231. Thu, 29 Mar 2020,
  232. ]
  233. =end
  234. def public_holidays_to_array
  235. holidays = []
  236. public_holidays&.each do |day, meta|
  237. next if !meta
  238. next if !meta['active']
  239. next if meta['removed']
  240. holidays.push Date.parse(day)
  241. end
  242. holidays
  243. end
  244. def biz
  245. Biz::Schedule.new do |config|
  246. # get business hours
  247. hours = business_hours_to_hash
  248. raise "No configured hours found in calendar #{inspect}" if hours.blank?
  249. config.hours = hours
  250. # get holidays
  251. config.holidays = public_holidays_to_array
  252. config.time_zone = timezone
  253. end
  254. end
  255. private
  256. # if changed calendar is default, set all others default to false
  257. def sync_default
  258. return true if !default
  259. Calendar.find_each do |calendar|
  260. next if calendar.id == id
  261. next if !calendar.default
  262. calendar.default = false
  263. calendar.save
  264. end
  265. true
  266. end
  267. # check if min one is set to default true
  268. def min_one_check
  269. if !Calendar.find_by(default: true)
  270. first = Calendar.order(:created_at, :id).limit(1).first
  271. return true if !first
  272. first.default = true
  273. first.save
  274. end
  275. # check if sla's are refer to an existing calendar
  276. default_calendar = Calendar.find_by(default: true)
  277. Sla.find_each do |sla|
  278. if !sla.calendar_id
  279. sla.calendar_id = default_calendar.id
  280. sla.save!
  281. next
  282. end
  283. if !Calendar.find_by(id: sla.calendar_id)
  284. sla.calendar_id = default_calendar.id
  285. sla.save!
  286. end
  287. end
  288. true
  289. end
  290. # fetch ical feed
  291. def fetch_ical
  292. sync(true)
  293. true
  294. end
  295. # validate format of public holidays
  296. def validate_public_holidays
  297. # fillup feed info
  298. before = public_holidays_was
  299. public_holidays.each do |day, meta|
  300. if before && before[day] && before[day]['feed']
  301. meta['feed'] = before[day]['feed']
  302. end
  303. meta['active'] = if meta['active']
  304. true
  305. else
  306. false
  307. end
  308. end
  309. true
  310. end
  311. # validate business hours
  312. def validate_hours
  313. # get business hours
  314. hours = business_hours_to_hash
  315. raise Exceptions::UnprocessableEntity, 'No configured business hours found!' if hours.blank?
  316. # validate if business hours are usable by execute a try calculation
  317. begin
  318. Biz.configure do |config|
  319. config.hours = hours
  320. end
  321. Biz.time(10, :minutes).after(Time.zone.parse('Tue, 05 Feb 2019 21:40:28 UTC +00:00'))
  322. rescue => e
  323. raise Exceptions::UnprocessableEntity, e.message
  324. end
  325. true
  326. end
  327. end