From 5509f61549c5637efe457a13ae7bb28d18963b50 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 9 Sep 2015 08:52:05 +0200 Subject: [PATCH] Init version of calendar management. --- .../app/controllers/calendar.js.coffee | 37 ++++ .../javascripts/app/models/calendar.js.coffee | 4 + .../javascripts/app/views/calendar.jst.eco | 59 ++++++ app/controllers/application_controller.rb | 14 +- app/controllers/calendars_controller.rb | 30 +++ app/models/calendar.rb | 197 ++++++++++++++++++ config/routes/calendar.rb | 11 + db/migrate/20150968000001_create_calendar.rb | 124 +++++++++++ 8 files changed, 463 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/calendar.js.coffee create mode 100644 app/assets/javascripts/app/models/calendar.js.coffee create mode 100644 app/assets/javascripts/app/views/calendar.jst.eco create mode 100644 app/controllers/calendars_controller.rb create mode 100644 app/models/calendar.rb create mode 100644 config/routes/calendar.rb create mode 100644 db/migrate/20150968000001_create_calendar.rb diff --git a/app/assets/javascripts/app/controllers/calendar.js.coffee b/app/assets/javascripts/app/controllers/calendar.js.coffee new file mode 100644 index 000000000..bac061ffa --- /dev/null +++ b/app/assets/javascripts/app/controllers/calendar.js.coffee @@ -0,0 +1,37 @@ +class Index extends App.ControllerContent + constructor: -> + super + + # check authentication + return if !@authenticate() + + @subscribeId = App.Calendar.subscribe(@render) + App.Calendar.fetch() + + render: => + calendars = App.Calendar.all() + for calendar in calendars + + # get preview public holidays + public_holidays_preview = {} + if calendar.public_holidays + from = new Date().setTime(new Date().getTime() - (5*24*60*60*1000)) + till = new Date().setTime(new Date().getTime() + (90*24*60*60*1000)) + keys = Object.keys(calendar.public_holidays).reverse() + #for day, comment of calendar.public_holidays + for day in keys + itemTime = new Date( Date.parse( "#{day}T00:00:00Z" ) ) + if itemTime < till && itemTime > from + public_holidays_preview[day] = calendar.public_holidays[day] + calendar.public_holidays_preview = public_holidays_preview + + @html App.view('calendar')( + calendars: calendars + ) + + release: => + if @subscribeId + App.Calendar.unsubscribe(@subscribeId) + + +App.Config.set( 'Calendars', { prio: 2400, name: 'Calendars', parent: '#manage', target: '#manage/calendars', controller: Index, role: ['Admin'] }, 'NavBarAdmin' ) \ No newline at end of file diff --git a/app/assets/javascripts/app/models/calendar.js.coffee b/app/assets/javascripts/app/models/calendar.js.coffee new file mode 100644 index 000000000..f38889002 --- /dev/null +++ b/app/assets/javascripts/app/models/calendar.js.coffee @@ -0,0 +1,4 @@ +class App.Calendar extends App.Model + @configure 'Calendar', 'name', 'timezone', 'default', 'business_hours', 'ical_url', 'public_holidays' + @extend Spine.Model.Ajax + @url: @apiPath + '/calendars' diff --git a/app/assets/javascripts/app/views/calendar.jst.eco b/app/assets/javascripts/app/views/calendar.jst.eco new file mode 100644 index 000000000..b461fa03a --- /dev/null +++ b/app/assets/javascripts/app/views/calendar.jst.eco @@ -0,0 +1,59 @@ + + +<% for calendar in @calendars: %> +
+
+
+

<%= calendar.name %>

+ <%- @T('Timezone') %>: <%= calendar.timezone %> +
+
+ <%- @T('Business Hours') %>:
+ + + + + + + + + + + + + + + + + + + + + + +
<%- @T('Monday') %><% if _.isEmpty(calendar.business_hours['mon']): %>-<% else: %><% for from, till of calendar.business_hours['mon']: %><%= from %>:<%= till %> <% end %><% end %>
<%- @T('Tuesday') %><% if _.isEmpty(calendar.business_hours['tue']): %>-<% else: %><% for from, till of calendar.business_hours['tue']: %><%= from %>:<%= till %> <% end %><% end %>
<%- @T('Wednesday') %><% if _.isEmpty(calendar.business_hours['wed']): %>-<% else: %><% for from, till of calendar.business_hours['wed']: %><%= from %>:<%= till %> <% end %><% end %>
<%- @T('Thursday') %><% if _.isEmpty(calendar.business_hours['thu']): %>-<% else: %><% for from, till of calendar.business_hours['thu']: %><%= from %>:<%= till %> <% end %><% end %>
<%- @T('Friday') %><% if _.isEmpty(calendar.business_hours['fri']): %>-<% else: %><% for from, till of calendar.business_hours['fri']: %><%= from %>:<%= till %> <% end %><% end %>
<%- @T('Saturday') %><% if _.isEmpty(calendar.business_hours['sat']): %>-<% else: %><% for from, till of calendar.business_hours['sat']: %><%= from %>:<%= till %> <% end %><% end %>
<%- @T('Sunday') %><% if _.isEmpty(calendar.business_hours['sun']): %>-<% else: %><% for from, till of calendar.business_hours['sun']: %><%= from %>:<%= till %> <% end %><% end %>
+
+
+ <%- @T('Public Holidays') %>:
+ <% for holiday, meta of calendar.public_holidays_preview: %> + <%= holiday %>: <%= meta.summary %>
+ <% end %> +
+
+
+ <% if !calendar.default: %> +
<%- @T('Delete') %>
+
<%- @T('Set as Default') %>
+ <% end %> +
<%- @T('Edit') %>
+
+
+<% end %> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a3f6c214f..a31493ce0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -287,19 +287,7 @@ class ApplicationController < ActionController::Base } # get all time zones - config['timezones'] = {} - TZInfo::Timezone.all.each { |t| - - # ignore the following time zones - next if t.name =~ /^GMT/ - next if t.name =~ /^Etc/ - next if t.name =~ /^MET/ - next if t.name =~ /^MST/ - next if t.name =~ /^ROC/ - next if t.name =~ /^ROK/ - diff = t.current_period.utc_total_offset / 60 / 60 - config['timezones'][ t.name ] = diff - } + config['timezones'] = Calendar.timezones # remember if we can to swich back to user if session[:switched_from_user_id] diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb new file mode 100644 index 000000000..e62d497ea --- /dev/null +++ b/app/controllers/calendars_controller.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class CalendarsController < ApplicationController + before_action :authentication_check + + def index + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_index_render(Calendar, params) + end + + def show + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_show_render(Calendar, params) + end + + def create + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_create_render(Calendar, params) + end + + def update + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_update_render(Calendar, params) + end + + def destroy + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_destory_render(Calendar, params) + end +end diff --git a/app/models/calendar.rb b/app/models/calendar.rb new file mode 100644 index 000000000..73e52401f --- /dev/null +++ b/app/models/calendar.rb @@ -0,0 +1,197 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class Calendar < ApplicationModel + store :business_hours + store :public_holidays + +=begin + +get default calendar + + calendar = Calendar.default + +returns calendar object + +=end + + def self.default + find_by(default: true) + end + +=begin + +returnes preset of ical feeds + + feeds = Calendar.ical_feeds + +returns + + { + 'US Holidays' => 'http://www.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics', + ... + } + +=end + + def self.ical_feeds + gfeeds = { + 'Australian Holidays' => 'en.australian', + 'Austrian Holidays' => 'de.austrian', + 'Brazilian Holidays' => 'en.brazilian', + 'Canadian Holidays' => 'en.canadian', + 'China Holidays' => 'en.china', + 'Switzerland Holidays' => 'de.ch', + 'Christian Holidays' => 'en.christian', + 'Danish Holidays' => 'da.danish', + 'Dutch Holidays' => 'nl.dutch', + 'Finnish Holidays' => 'en.finnish', + 'French Holidays' => 'fe.french', + 'German Holidays' => 'de.german', + 'Greek Holidays' => 'en.greek', + 'Hong Kong Holidays' => 'en.hong_kong', + 'Indian Holidays' => 'en.indian', + 'Indonesian Holidays' => 'en.indonesian', + 'Iranian Holidays' => 'en.iranian', + 'Irish Holidays' => 'en.irish', + 'Islamic Holidays' => 'en.islamic', + 'Italian Holidays' => 'it.italian', + 'Japanese Holidays' => 'en.japanese', + 'Jewish Holidays' => 'en.jewish', + 'Malaysian Holidays' => 'en.malaysia', + 'Mexican Holidays' => 'en.mexican', + 'New Zealand Holidays' => 'en.new_zealand', + 'Norwegian Holidays' => 'en.norwegian', + 'Philippines Holidays' => 'en.philippines', + 'Polish Holidays' => 'en.polish', + 'Portuguese Holidays' => 'en.portuguese', + 'Russian Holidays' => 'en.russian', + 'Singapore Holidays' => 'en.singapore', + 'South Africa Holidays' => 'en.sa', + 'South Korean Holidays' => 'en.south_korea', + 'Spain Holidays' => 'en.spain', + 'Swedish Holidays' => 'en.swedish', + 'Taiwan Holidays' => 'en.taiwan', + 'Thai Holidays' => 'en.thai', + 'UK Holidays' => 'en.uk', + 'US Holidays' => 'en.usa', + 'Vietnamese Holidays' => 'en.vietnamese', + } + gfeeds.each {|key, name| + gfeeds[key] = "http://www.google.com/calendar/ical/#{name}%23holiday%40group.v.calendar.google.com/public/basic.ics" + } + gfeeds + 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 { |timezone| + + # ignore the following time zones + #next if t.name =~ /^GMT/ + #next if t.name =~ /^Etc/ + #next if t.name !~ /\// + t = TZInfo::Timezone.get(timezone) + diff = t.current_period.utc_total_offset / 60 / 60 + list[ timezone ] = diff + } + list + end + +=begin + +syn all calendars with ical feeds + + success = Calendar.sync + +returns + + true # or false + +=end + + def self.sync + Calendar.all.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 + return if !ical_url + events = Calendar.parse(ical_url) + + # sync with public_holidays + if !public_holidays + self.public_holidays = {} + end + events.each {|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, + } + } + self.last_log = '' + self.last_sync = Time.zone.now + save + true + end + + def self.parse(location) + if location =~ /^http/i + result = UserAgent.get(location) + cal_file = result.body + else + cal_file = File.open(location) + end + + cals = Icalendar.parse(cal_file) + cal = cals.first + events = {} + cal.events.each {|event| + next if event.dtstart < Time.zone.now - 1.year + next if event.dtstart > Time.zone.now + 3.year + day = "#{event.dtstart.year}-#{format('%02d', event.dtstart.month)}-#{format('%02d', event.dtstart.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 + events[day] = comment + } + events.sort.to_h + end +end diff --git a/config/routes/calendar.rb b/config/routes/calendar.rb new file mode 100644 index 000000000..b0d2a39b3 --- /dev/null +++ b/config/routes/calendar.rb @@ -0,0 +1,11 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + # calendars + match api_path + '/calendars', to: 'calendars#index', via: :get + match api_path + '/calendars/:id', to: 'calendars#show', via: :get + match api_path + '/calendars', to: 'calendars#create', via: :post + match api_path + '/calendars/:id', to: 'calendars#update', via: :put + match api_path + '/calendars/:id', to: 'calendars#destroy', via: :delete + +end diff --git a/db/migrate/20150968000001_create_calendar.rb b/db/migrate/20150968000001_create_calendar.rb new file mode 100644 index 000000000..3e8fa1d85 --- /dev/null +++ b/db/migrate/20150968000001_create_calendar.rb @@ -0,0 +1,124 @@ +class CreateCalendar < ActiveRecord::Migration + def up + create_table :calendars do |t| + t.string :name, limit: 250, null: true + t.string :timezone, limit: 250, null: true + t.string :business_hours, limit: 1200, null: true + t.boolean :default, null: false, default: false + t.string :ical_url, limit: 500, null: true + t.text :public_holidays, limit: 500.kilobytes + 1, null: true + t.text :last_log, limit: 500.kilobytes + 1, null: true + t.timestamp :last_sync, null: true + t.integer :updated_by_id, null: false + t.integer :created_by_id, null: false + t.timestamps + end + add_index :calendars, [:name], unique: true + + Calendar.create_or_update( + name: 'US', + 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: 'http://www.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics', + updated_by_id: 1, + created_by_id: 1, + ) + Calendar.create_or_update( + name: 'Germany', + timezone: 'Europe/Berlin', + 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: false, + ical_url: 'http://www.google.com/calendar/ical/de.german%23holiday%40group.v.calendar.google.com/public/basic.ics', + updated_by_id: 1, + created_by_id: 1, + ) +=begin + Calendar.create_or_update( + name: 'French', + timezone: 'Europe/Paris', + business_hours: { + mon: { '09:00' => '17:00' }, + tue: { '09:00' => '17:00' }, + wed: { '09:00' => '17:00' }, + thu: { '09:00' => '12:00', '13:00' => '17:00' }, + fri: { '09:00' => '17:00' } + }, + default: false, + ical_url: 'http://www.google.com/calendar/ical/fr.french%23holiday%40group.v.calendar.google.com/public/basic.ics', + updated_by_id: 1, + created_by_id: 1, + ) + Calendar.create_or_update( + name: 'Switzerland', + timezone: 'Europe/Zurich', + business_hours: { + mon: { '09:00' => '17:00' }, + tue: { '09:00' => '17:00' }, + wed: { '09:00' => '17:00' }, + thu: { '09:00' => '12:00', '13:00' => '17:00' }, + fri: { '09:00' => '17:00' } + }, + default: false, + ical_url: 'http://www.google.com/calendar/ical/de.ch%23holiday%40group.v.calendar.google.com/public/basic.ics', + updated_by_id: 1, + created_by_id: 1, + ) + Calendar.create_or_update( + name: 'Austria', + timezone: 'Europe/Vienna', + 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: false, + ical_url: 'http://www.google.com/calendar/ical/de.austrian%23holiday%40group.v.calendar.google.com/public/basic.ics', + updated_by_id: 1, + created_by_id: 1, + ) + Calendar.create_or_update( + name: 'Italian', + timezone: 'Europe/Roma', + 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: false, + ical_url: 'http://www.google.com/calendar/ical/it.italian%23holiday%40group.v.calendar.google.com/public/basic.ics', + updated_by_id: 1, + created_by_id: 1, + ) +=end + Scheduler.create_or_update( + name: 'Sync calendars with ical feeds.', + method: 'Calendar.sync', + period: 1.day, + prio: 2, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + end + + def down + drop_table :calendars + end +end