concerto_calendar 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 %>