concerto_calendar 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/LICENSE +13 -0
  2. data/README.md +8 -0
  3. data/Rakefile +40 -0
  4. data/app/assets/javascripts/concerto_calendar/application.js +1 -0
  5. data/app/assets/javascripts/concerto_calendar/concerto_calendar.js +24 -0
  6. data/app/assets/stylesheets/concerto_calendar/application.css +13 -0
  7. data/app/models/calendar.rb +305 -0
  8. data/app/views/contents/calendar/_form_top.html.erb +93 -0
  9. data/app/views/contents/calendar/_render_default.html.erb +30 -0
  10. data/app/views/contents/calendar/_render_grid.html.erb +3 -0
  11. data/app/views/contents/calendar/_render_tile.html.erb +5 -0
  12. data/app/views/contents/calendar/_tab_icon.html.erb +1 -0
  13. data/config/routes.rb +3 -0
  14. data/lib/concerto_calendar.rb +4 -0
  15. data/lib/concerto_calendar/engine.rb +9 -0
  16. data/lib/concerto_calendar/version.rb +3 -0
  17. data/test/concerto_calendar_test.rb +7 -0
  18. data/test/dummy/README.rdoc +261 -0
  19. data/test/dummy/Rakefile +7 -0
  20. data/test/dummy/app/assets/javascripts/application.js +13 -0
  21. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  22. data/test/dummy/app/controllers/application_controller.rb +3 -0
  23. data/test/dummy/app/helpers/application_helper.rb +2 -0
  24. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  25. data/test/dummy/config.ru +4 -0
  26. data/test/dummy/config/application.rb +59 -0
  27. data/test/dummy/config/boot.rb +10 -0
  28. data/test/dummy/config/database.yml +25 -0
  29. data/test/dummy/config/environment.rb +5 -0
  30. data/test/dummy/config/environments/development.rb +37 -0
  31. data/test/dummy/config/environments/production.rb +67 -0
  32. data/test/dummy/config/environments/test.rb +37 -0
  33. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  34. data/test/dummy/config/initializers/content_types.rb +1 -0
  35. data/test/dummy/config/initializers/inflections.rb +15 -0
  36. data/test/dummy/config/initializers/mime_types.rb +5 -0
  37. data/test/dummy/config/initializers/secret_token.rb +7 -0
  38. data/test/dummy/config/initializers/session_store.rb +8 -0
  39. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  40. data/test/dummy/config/locales/en.yml +5 -0
  41. data/test/dummy/config/routes.rb +4 -0
  42. data/test/dummy/public/404.html +26 -0
  43. data/test/dummy/public/422.html +26 -0
  44. data/test/dummy/public/500.html +25 -0
  45. data/test/dummy/public/favicon.ico +0 -0
  46. data/test/dummy/script/rails +6 -0
  47. data/test/integration/navigation_test.rb +10 -0
  48. data/test/test_helper.rb +15 -0
  49. metadata +190 -0
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Concerto Authors
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # Concerto 2 Calendar Plugin
2
+ This plugin provides support for displaying Google Calendar or iCal entries in Concerto 2.
3
+
4
+ 1. Add to your Gemfile: ```gem 'concerto_calendar'```
5
+ 2. ```bundle install```
6
+
7
+
8
+ Concerto 2 Calendar is licensed under the Apache License, Version 2.0.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'ConcertoCalendar'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
24
+ load 'rails/tasks/engine.rake'
25
+
26
+
27
+
28
+ Bundler::GemHelper.install_tasks
29
+
30
+ require 'rake/testtask'
31
+
32
+ Rake::TestTask.new(:test) do |t|
33
+ t.libs << 'lib'
34
+ t.libs << 'test'
35
+ t.pattern = 'test/**/*_test.rb'
36
+ t.verbose = false
37
+ end
38
+
39
+
40
+ task :default => :test
@@ -0,0 +1 @@
1
+ //= require_tree .
@@ -0,0 +1,24 @@
1
+ function attachHandlers() {
2
+ $('select#calendar_config_calendar_source').on('change', revealRelevantFields);
3
+
4
+ function revealRelevantFields() {
5
+ var vendor = $('select#calendar_config_calendar_source').val();
6
+ if (vendor == 'google') {
7
+ $('input#calendar_config_api_key').closest('div.clearfix').show();
8
+ $('input#calendar_config_calendar_id').closest('div.clearfix').show();
9
+ $('input#calendar_config_calendar_url').closest('div.clearfix').hide();
10
+ } else if (vendor == 'ical') {
11
+ $('input#calendar_config_api_key').closest('div.clearfix').hide();
12
+ $('input#calendar_config_calendar_id').closest('div.clearfix').hide();
13
+ $('input#calendar_config_calendar_url').closest('div.clearfix').show();
14
+ }
15
+ }
16
+
17
+ revealRelevantFields();
18
+
19
+ $("input#calendar_config_start_date.datefield").datepicker();
20
+ $("input#calendar_config_end_date.datefield").datepicker();
21
+ }
22
+
23
+ $(document).ready(attachHandlers);
24
+ $(document).on('page:change', attachHandlers);
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,305 @@
1
+ class Calendar < DynamicContent
2
+ after_initialize :set_defaults, :on => :new
3
+ validate :validate_config, :on => :create
4
+
5
+ # this is the common class used for holding the content to be rendered
6
+ # it is populated from the various calendar sources
7
+ class CalendarResults
8
+ class CalendarResultItem
9
+ attr_accessor :name, :description, :location, :start_time, :end_time
10
+
11
+ def initialize(name, description, location, start_time, end_time)
12
+ @name=name
13
+ @description = description
14
+ @location = location
15
+ @start_time = start_time
16
+ @end_time = end_time
17
+ end
18
+ end
19
+
20
+ attr_accessor :error_message, :name, :items
21
+
22
+ def initialize
23
+ self.items = []
24
+ self.name = ""
25
+ self.error_message = ""
26
+ end
27
+
28
+ def error?
29
+ !self.error_message.empty?
30
+ end
31
+
32
+ def add_item(name, description, location, start_time, end_time)
33
+ self.items << CalendarResultItem.new(name, description, location, start_time, end_time)
34
+ end
35
+ end
36
+
37
+ DISPLAY_NAME = 'Calendar'
38
+ DISPLAY_FORMATS = {
39
+ "List (Multiple)" => "headlines",
40
+ "Detailed (Single)" => "detailed"
41
+ }
42
+ CALENDAR_SOURCES = { # exclude RSS and ATOM since cant get individual fields
43
+ "Google" => "google",
44
+ "iCal" => "ical",
45
+ # "Bedework JSON" => "bedeworkjson"
46
+ }
47
+
48
+ def set_defaults
49
+ self.config['calendar_source'] ||= 'ical'
50
+ self.config['day_format'] ||= '%A %b %e'
51
+ self.config['time_format'] ||= '%l:%M %P'
52
+ self.config['max_results'] ||= 10
53
+ end
54
+
55
+ def build_content
56
+ contents = []
57
+ result = fetch_calendar
58
+
59
+ if result.error?
60
+ raise result.error_message
61
+ else
62
+ day_format = self.config['day_format']
63
+ time_format = self.config['time_format']
64
+
65
+ case self.config['output_format']
66
+ when 'headlines' # 5 items per entry, titles only
67
+ result.items.each_slice(5).with_index do |items, index|
68
+ htmltext = HtmlText.new()
69
+ htmltext.name = "#{result.name} (#{index+1})"
70
+ htmltext.data = "<h1>#{result.name}</h1>#{items_to_html(items, day_format, time_format)}"
71
+ contents << htmltext
72
+ end
73
+ when 'detailed' # each item is a separate entry, title and description
74
+ result.items.each_with_index do |item, index|
75
+ htmltext = HtmlText.new()
76
+ htmltext.name = "#{result.name} (#{index+1})"
77
+ htmltext.data = item_to_html(item, day_format, time_format)
78
+ contents << htmltext
79
+ end
80
+ else
81
+ raise ArgumentError, 'Unexpected output format for Calendar feed.'
82
+ end
83
+ end
84
+
85
+ return contents
86
+ end
87
+
88
+ def fetch_calendar
89
+ result = CalendarResults.new
90
+ client_key = self.config['api_key']
91
+ calendar_id = self.config['calendar_id']
92
+ calendar_source = self.config['calendar_source']
93
+ start_date = self.config['start_date'].strip.empty? ? Clock.time.beginning_of_day.iso8601 : self.config['start_date'].to_time.beginning_of_day.iso8601
94
+ end_date = self.config['end_date'].strip.empty? ? (start_date.to_time.beginning_of_day + self.config['days_ahead'].to_i.days).end_of_day.iso8601 : self.config['end_date'].to_time.beginning_of_day.iso8601
95
+
96
+ case calendar_source
97
+ when 'google'
98
+ if !client_key.empty?
99
+ # ---------------------------------- google calendar api v3 via client api
100
+ require 'google/api_client'
101
+
102
+ client = Google::APIClient.new
103
+ client.authorization = nil
104
+ client.key = client_key
105
+
106
+ cal = client.discovered_api('calendar', 'v3')
107
+
108
+ params = {}
109
+ params['calendarId'] = calendar_id
110
+ params['maxResults'] = self.config['max_results'] if !params['max_results'].blank?
111
+ params['singleEvents'] = true
112
+ params['orderBy'] = 'startTime'
113
+ params['fields'] = "description,items(description,end,endTimeUnspecified,location,organizer/displayName,source/title,start,status,summary,updated),summary,timeZone,updated"
114
+ params['timeMin'] = start_date
115
+ params['timeMax'] = end_date
116
+
117
+ tmp = client.execute(:api_method => cal.events.list, :parameters => params)
118
+
119
+ # convert to common data structure
120
+ result.error_message = tmp.error_message if tmp.error?
121
+ if !result.error?
122
+ result.name = tmp.data.summary
123
+ tmp.data.items.each do |item|
124
+ result.add_item(item.summary, item.description, item.location, item.start.dateTime, item.end.dateTime)
125
+ end
126
+ end
127
+ else
128
+ # ---------------------------------- public calendar via plain http
129
+ require 'net/http'
130
+ url = "http://www.google.com/calendar/feeds/#{calendar_id}/public/full?alt=json"
131
+ params = {}
132
+ params['max-results'] = self.config['max_results'] if !params['max_results'].blank?
133
+ params['singleevents'] = true
134
+ params['orderby'] = 'starttime'
135
+ params['start-min'] = start_date
136
+ params['start-max'] = end_date
137
+ url += params.collect { |k,v| "&#{k}=#{v}" }.join()
138
+
139
+ tmp = nil
140
+ begin
141
+ json_data = Net::HTTP.get_response(URI.parse(url)).body
142
+ tmp = JSON.load(json_data)
143
+ rescue => e
144
+ result.error_message = e.message
145
+ end
146
+
147
+ # convert to common data structure
148
+ if !result.error?
149
+ result.name = tmp["feed"]["title"]["$t"]
150
+ tmp["feed"]["entry"].each do |item|
151
+ location = item["gd$where"].first["valueString"]
152
+ start_time = item["gd$when"].first["startTime"].to_time
153
+ end_time = item["gd$when"].first["endTime"].to_time
154
+ result.add_item(item["title"]["$t"], item["content"]["$t"], location, start_time, end_time)
155
+ end
156
+ end
157
+ end
158
+ when 'ical'
159
+ # ---------------------------------- iCal calendar
160
+ # need to filter manually below because the url may not accommodate filtering
161
+ # so respect self.config[max_results] and start_date and end_date (which incorporates the days ahead)
162
+ require 'open-uri'
163
+ require 'icalendar'
164
+
165
+ begin
166
+ url = self.config['calendar_url']
167
+ calendars = nil
168
+ open(URI.parse(url)) do |cal|
169
+ calendars = Icalendar.parse(cal)
170
+ end
171
+
172
+ max_results = self.config['max_results'].to_i
173
+ result.name = self.name # iCal doesn't provide a calendar name, so use the user's provided name
174
+ calendars.first.events.each do |item|
175
+ title = item.summary
176
+ description = item.description
177
+ location = item.location
178
+ item_start_time = item.dtstart.to_time unless item.dtstart.nil?
179
+ item_end_time = item.dtend.to_time unless item.dtend.nil?
180
+ # make sure the item's start date is within the specified range
181
+ if item_start_time >= start_date && item_start_time < end_date
182
+ result.add_item(title, description, location, item_start_time, item_end_time)
183
+ end
184
+ end
185
+ result.items.sort! { |a, b| a.start_time <=> b.start_time }
186
+ result.items = result.items[0..max_results]
187
+ rescue => e
188
+ result.error_message = e.message
189
+ end
190
+ else
191
+ result.error_message = "unsupported calendar source #{calendar_source}"
192
+ end
193
+
194
+ return result
195
+ end
196
+
197
+ def item_to_html(item, day_format, time_format)
198
+ start_time = item.start_time.strftime(time_format)
199
+ end_time = item.end_time.strftime(time_format) unless item.end_time.nil?
200
+
201
+ html = []
202
+ html << "<h1>#{item.name}</h1>"
203
+ html << "<h2>#{item.start_time.strftime(day_format)}</h2>"
204
+ html << (end_time.nil? ? "<div class=\"cal-time\">#{start_time}</div>" : "<div class=\"cal-time\">#{start_time} - #{end_time}</div>") unless start_time == end_time
205
+ html << "<div class=\"cal-location\">#{item.location}</div>"
206
+ html << "<p>#{item.description}</p>"
207
+ return html.join("")
208
+ end
209
+
210
+ # display date (only when it changes) / times with title...
211
+ def items_to_html(items, day_format, time_format)
212
+ html = []
213
+ last_date = nil
214
+ items.each do |item|
215
+ # see if we need a date header
216
+ if last_date != item.start_time.to_date
217
+ if last_date.nil?
218
+ # dont need to close list
219
+ else
220
+ html << "</dl>"
221
+ end
222
+ html << "<h2>#{item.start_time.strftime(day_format)}</h2>"
223
+ html << "<dl>"
224
+ end
225
+ # todo: end time should include date if different
226
+ start_time = item.start_time.strftime(time_format)
227
+ end_time = item.end_time.strftime(time_format) unless item.end_time.nil?
228
+
229
+ html << (end_time.nil? ? "<dt>#{start_time}</dt>" : "<dt>#{start_time} - #{end_time}</dt>") unless start_time == end_time
230
+ html << "<dd>#{item.name}</dd>"
231
+ last_date = item.start_time.to_date
232
+ end
233
+ html << "</dl>" if !last_date.nil?
234
+ return html.join("")
235
+ end
236
+
237
+ # calendar api parameters and preferred view (output_format)
238
+ def self.form_attributes
239
+ attributes = super()
240
+ attributes.concat([:config => [
241
+ :calendar_source, # google or ical (or bedework JSON eventually)
242
+ :api_key, # google
243
+ :calendar_id, # google
244
+ :calendar_url, # iCal url (specify parms in url manually)
245
+ :max_results,
246
+ :days_ahead,
247
+ :start_date,
248
+ :end_date,
249
+ :output_format, # all cals
250
+ :day_format, # all cals
251
+ :time_format # all cals
252
+ ]])
253
+ end
254
+
255
+ def validate_config
256
+ # if self.config['api_key'].blank?
257
+ # errors.add(:config_api_key, "can't be blank")
258
+ # end
259
+
260
+ prerequisites_met = true
261
+ if self.config['calendar_id'].blank? && self.config['calendar_source'] == "google"
262
+ errors.add(:config_calendar_id, "can't be blank")
263
+ prerequisites_met = false
264
+ end
265
+ if self.config['calendar_url'].blank? && self.config['calendar_source'] != "google"
266
+ errors.add(:config_calendar_url, "can't be blank")
267
+ prerequisites_met = false
268
+ end
269
+ if self.config['max_results'].blank?
270
+ errors.add(:config_max_results, "can't be blank")
271
+ end
272
+ # if self.config['days_ahead'].blank? && self.config['end_date'].blank?
273
+ # errors.add(:config_days_ahead, "days ahead or end_date must be specified")
274
+ # end
275
+ if !self.config['start_date'].blank? && !self.config['end_date'].blank?
276
+ start_date = self.config['start_date'].to_date
277
+ end_date = self.config['end_date'].to_date
278
+ if start_date > end_date
279
+ errors.add(:config_start_date, "must precede end date")
280
+ end
281
+ end
282
+ if !CALENDAR_SOURCES.values.include?(self.config['calendar_source'])
283
+ errors.add(:config_calendar_source, "must be #{CALENDAR_SOURCES.keys.join(' or ')}")
284
+ end
285
+ if !DISPLAY_FORMATS.values.include?(self.config['output_format'])
286
+ errors.add(:config_output_format, "must be #{DISPLAY_FORMATS.keys.join(' or ')}")
287
+ end
288
+ # todo: validate strftime components in day_format and time_format?
289
+
290
+ begin
291
+ validate_request #if !self.config['api_key'].blank?
292
+ rescue => e
293
+ errors.add(:base, "Could not fetch calendar - #{e.message}")
294
+ end if prerequisites_met
295
+ end
296
+
297
+ # make sure the request is valid by fetching a result back
298
+ def validate_request
299
+ result = fetch_calendar
300
+
301
+ if result.error?
302
+ raise result.error_message
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,93 @@
1
+ <%= javascript_include_tag "concerto_calendar/application" %>
2
+ <%= stylesheet_link_tag "concerto_calendar/application" %>
3
+
4
+ <%= form.fields_for :config do |config| %>
5
+ <fieldset>
6
+ <legend><span>Calendar</span></legend>
7
+ <div class="clearfix">
8
+ <%= config.label :calendar_source %>
9
+ <div class="input">
10
+ <%= config.select :calendar_source, Calendar::CALENDAR_SOURCES, :selected => @content.config['calendar_source'] %>
11
+ </div>
12
+ </div>
13
+
14
+ <!-- google specific -->
15
+ <div class="clearfix">
16
+ <%= config.label :api_key %>
17
+ <div class="input">
18
+ <%= config.text_field :api_key, :placeholder => 'your secret API key from Google API Console', :class => "input-xxlarge", :value => @content.config['api_key'] %>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="clearfix">
23
+ <%= config.label :calendar_id %>
24
+ <div class="input">
25
+ <%= config.text_field :calendar_id, :placeholder => 'office@soldotnabiblechapel.com', :class => "input-xlarge", :value => @content.config['calendar_id'] %>
26
+ </div>
27
+ </div>
28
+ <!-- end of google specific -->
29
+
30
+ <!-- iCal specific -->
31
+ <div class="clearfix">
32
+ <%= config.label :calendar_url %>
33
+ <div class="input">
34
+ <%= config.text_field :calendar_url, :placeholder => 'http://events.rpi.edu/webcache/v1.0/icsDays/7/no--filter.ics', :class => "input-xxlarge", :value => @content.config['calendar_url'] %>
35
+ </div>
36
+ </div>
37
+ <!-- end of iCal specific -->
38
+ </fieldset>
39
+
40
+ <fieldset>
41
+ <legend><span>Filter and Format</span></legend>
42
+ <div class="clearfix">
43
+ <%= config.label :max_results %>
44
+ <div class="input">
45
+ <%= config.number_field :max_results, :class => "input-small", :value => @content.config['max_results'] %>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="clearfix">
50
+ <%= config.label :days_ahead %>
51
+ <div class="input">
52
+ <%= config.number_field :days_ahead, :class => "input-small", :value => @content.config['days_ahead'] %>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="clearfix">
57
+ <%= config.label :start_date %>
58
+ <div class="input">
59
+ <%= config.text_field :start_date, :class => "input-small datefield", :value => @content.config['start_date'] %>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="clearfix">
64
+ <%= config.label :end_date %>
65
+ <div class="input">
66
+ <%= config.text_field :end_date, :class => "input-small datefield", :value => @content.config['end_date'] %>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="clearfix">
71
+ <%= config.label :day_format %>
72
+ <div class="input">
73
+ <%= config.text_field :day_format, :placeholder => '%A %b %e', :class => "input-medium", :value => @content.config['day_format'] %>
74
+ <div><i>use <a href="http://www.ruby-doc.org/core-2.0/Time.html#method-i-strftime" target="_blank">strftime format</a> specifiers</i></div>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="clearfix">
79
+ <%= config.label :time_format %>
80
+ <div class="input">
81
+ <%= config.text_field :time_format, :placeholder => '%l:%M %P', :class => "input-medium", :value => @content.config['time_format'] %>
82
+ <div><i>use <a href="http://www.ruby-doc.org/core-2.0/Time.html#method-i-strftime" target="_blank">strftime format</a> specifiers</i></div>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="clearfix">
87
+ <%= config.label :output_format, 'Display Format' %>
88
+ <div class="input">
89
+ <%= config.select :output_format, Calendar::DISPLAY_FORMATS, :selected => @content.config['output_format'] %>
90
+ </div>
91
+ </div>
92
+ </fieldset>
93
+ <% end %>