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