radiant-event_calendar-extension 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. data/.gitmodules +0 -0
  2. data/README.md +198 -0
  3. data/Rakefile +139 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/admin/calendars_controller.rb +18 -0
  6. data/app/controllers/admin/event_venues_controller.rb +3 -0
  7. data/app/controllers/admin/events_controller.rb +9 -0
  8. data/app/controllers/admin/icals_controller.rb +26 -0
  9. data/app/controllers/events_controller.rb +201 -0
  10. data/app/models/calendar.rb +57 -0
  11. data/app/models/event.rb +409 -0
  12. data/app/models/event_calendar_page.rb +138 -0
  13. data/app/models/event_occurrence.rb +89 -0
  14. data/app/models/event_recurrence_rule.rb +85 -0
  15. data/app/models/event_venue.rb +17 -0
  16. data/app/models/ical.rb +132 -0
  17. data/app/views/admin/calendars/_actions.html.haml +6 -0
  18. data/app/views/admin/calendars/_form.html.haml +56 -0
  19. data/app/views/admin/calendars/edit.html.haml +11 -0
  20. data/app/views/admin/calendars/help.html.erb +22 -0
  21. data/app/views/admin/calendars/index.html.haml +60 -0
  22. data/app/views/admin/calendars/new.html.haml +10 -0
  23. data/app/views/admin/calendars/show.html.haml +36 -0
  24. data/app/views/admin/event_venues/_event_venue.html.haml +18 -0
  25. data/app/views/admin/event_venues/_form.html.haml +44 -0
  26. data/app/views/admin/event_venues/edit.html.haml +32 -0
  27. data/app/views/admin/event_venues/index.html.haml +33 -0
  28. data/app/views/admin/event_venues/new.html.haml +32 -0
  29. data/app/views/admin/event_venues/remove.html.haml +17 -0
  30. data/app/views/admin/events/_event.html.haml +47 -0
  31. data/app/views/admin/events/_form.html.haml +116 -0
  32. data/app/views/admin/events/_list_head.html.haml +18 -0
  33. data/app/views/admin/events/edit.html.haml +6 -0
  34. data/app/views/admin/events/index.html.haml +23 -0
  35. data/app/views/admin/events/new.html.haml +6 -0
  36. data/app/views/admin/events/remove.html.haml +17 -0
  37. data/app/views/admin/icals/refresh.html.haml +11 -0
  38. data/app/views/admin/icals/refresh_all.html.haml +0 -0
  39. data/app/views/events/_defacet.html.haml +2 -0
  40. data/app/views/events/_event.html.haml +29 -0
  41. data/app/views/events/_event_postscript.html.haml +0 -0
  42. data/app/views/events/_faceting.html.haml +27 -0
  43. data/app/views/events/_minicalendar.html.haml +49 -0
  44. data/app/views/events/_other_page_parts.html.haml +0 -0
  45. data/app/views/events/_views.html.haml +6 -0
  46. data/app/views/events/index.html.haml +56 -0
  47. data/app/views/events/index.ics.erb +7 -0
  48. data/app/views/events/index.rss.builder +20 -0
  49. data/config/routes.rb +13 -0
  50. data/db/migrate/001_create_calendar_and_events.rb +21 -0
  51. data/db/migrate/002_calendar_add_ical_url.rb +9 -0
  52. data/db/migrate/003_add_calendar_category.rb +9 -0
  53. data/db/migrate/004_add_slug.rb +9 -0
  54. data/db/migrate/005_add_subscription_refresh_history.rb +11 -0
  55. data/db/migrate/006_create_icals.rb +13 -0
  56. data/db/migrate/007_move_subscriptions_to_ical.rb +15 -0
  57. data/db/migrate/008_clean_out_calendar.rb +12 -0
  58. data/db/migrate/009_basic_authentication.rb +12 -0
  59. data/db/migrate/010_refresh_interval.rb +9 -0
  60. data/db/migrate/011_more_properties.rb +8 -0
  61. data/db/migrate/20090818133511_simpler_ical_columns.rb +17 -0
  62. data/db/migrate/20090819130919_ownership.rb +17 -0
  63. data/db/migrate/20090820073805_site_scope.rb +13 -0
  64. data/db/migrate/20091118100725_event_status.rb +9 -0
  65. data/db/migrate/20100216080944_more_event_data.rb +31 -0
  66. data/db/migrate/20100218131410_recurrence_parts.rb +19 -0
  67. data/db/migrate/20100219102227_venues_and_categories.rb +23 -0
  68. data/db/migrate/20100221180539_recurrence_rules.rb +34 -0
  69. data/db/migrate/20100222182112_occurrences.rb +13 -0
  70. data/event_calendar_extension.rb +32 -0
  71. data/lib/calendar_period.rb +151 -0
  72. data/lib/event_calendar_admin_ui.rb +78 -0
  73. data/lib/event_calendar_tags.rb +1021 -0
  74. data/lib/event_search.rb +22 -0
  75. data/lib/event_statuses.rb +24 -0
  76. data/lib/tasks/event_calendar_extension_tasks.rake +29 -0
  77. data/pkg/radiant-event_calendar-extension-1.0.0.gem +0 -0
  78. data/public/icals/blank +0 -0
  79. data/public/images/admin/calendar.png +0 -0
  80. data/public/images/event_calendar/calendarlinkbg.png +0 -0
  81. data/public/images/event_calendar/event_shadow.png +0 -0
  82. data/public/images/event_calendar/ical16.png +0 -0
  83. data/public/images/event_calendar/maplinkbg.png +0 -0
  84. data/public/images/event_calendar/one_event.png +0 -0
  85. data/public/images/event_calendar/several_events.png +0 -0
  86. data/public/javascripts/admin/event_calendar.js +240 -0
  87. data/public/stylesheets/sass/admin/event_calendar.sass +266 -0
  88. data/public/stylesheets/sass/admin/modules/_grid.sass +56 -0
  89. data/public/stylesheets/sass/constants.sass +80 -0
  90. data/public/stylesheets/sass/event_calendar.sass +203 -0
  91. data/radiant-event_calendar-extension.gemspec +166 -0
  92. data/spec/datasets/calendar_events_dataset.rb +43 -0
  93. data/spec/datasets/calendar_pages_dataset.rb +8 -0
  94. data/spec/datasets/calendar_sites_dataset.rb +6 -0
  95. data/spec/datasets/calendars_dataset.rb +34 -0
  96. data/spec/datasets/recurrence_dataset.rb +7 -0
  97. data/spec/files/dummy.ics +59 -0
  98. data/spec/files/ny.ics +36 -0
  99. data/spec/lib/event_calendar_page_spec.rb +24 -0
  100. data/spec/models/calendar_spec.rb +11 -0
  101. data/spec/models/event_spec.rb +98 -0
  102. data/spec/models/ical_spec.rb +63 -0
  103. data/spec/models/recurrence_rule_spec.rb +82 -0
  104. data/spec/spec.opts +6 -0
  105. data/spec/spec_helper.rb +36 -0
  106. metadata +238 -0
@@ -0,0 +1,78 @@
1
+ module EventCalendarAdminUI
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+
6
+ attr_accessor :calendar, :event, :event_venue
7
+ alias_method :calendars, :calendar
8
+ alias_method :events, :event
9
+ alias_method :event_venues, :event_venue
10
+
11
+ def load_default_regions_with_event_calendar
12
+ load_default_regions_without_event_calendar
13
+ @calendar = load_default_calendar_regions
14
+ @event = load_default_event_regions
15
+ @event_venue = load_default_event_venue_regions
16
+ end
17
+
18
+ alias_method_chain :load_default_regions, :event_calendar
19
+
20
+ protected
21
+
22
+ def load_default_calendar_regions
23
+ returning OpenStruct.new do |calendar|
24
+ calendar.edit = Radiant::AdminUI::RegionSet.new do |edit|
25
+ edit.main.concat %w{edit_header edit_form}
26
+ edit.form.concat %w{edit_name edit_ical}
27
+ edit.form_bottom.concat %w{edit_timestamp edit_buttons}
28
+ end
29
+ calendar.index = Radiant::AdminUI::RegionSet.new do |index|
30
+ index.thead.concat %w{name_header url_header refresh_header action_header}
31
+ index.tbody.concat %w{name_cell url_cell refresh_cell action_cell}
32
+ index.bottom.concat %w{buttons}
33
+ end
34
+ calendar.remove = calendar.index
35
+ calendar.new = calendar.edit
36
+ end
37
+ end
38
+
39
+ def load_default_event_regions
40
+ returning OpenStruct.new do |event|
41
+ event.edit = Radiant::AdminUI::RegionSet.new do |edit|
42
+ edit.main.concat %w{edit_header edit_form}
43
+ edit.form.concat %w{edit_event edit_date edit_venue}
44
+ edit.form_bottom.concat %w{edit_timestamp edit_buttons}
45
+ end
46
+ event.index = Radiant::AdminUI::RegionSet.new do |index|
47
+ index.top.concat %w{help_text}
48
+ index.thead.concat %w{date_header title_header calendar_header time_header location_header modify_header}
49
+ index.tbody.concat %w{date_cell title_cell calendar_cell time_cell location_cell modify_cell}
50
+ index.bottom.concat %w{buttons}
51
+ end
52
+ event.remove = event.index
53
+ event.new = event.edit
54
+ end
55
+ end
56
+
57
+ def load_default_event_venue_regions
58
+ returning OpenStruct.new do |event_venue|
59
+ event_venue.edit = Radiant::AdminUI::RegionSet.new do |edit|
60
+ edit.main.concat %w{edit_header edit_form}
61
+ edit.form.concat %w{edit_event_venue}
62
+ edit.form_bottom.concat %w{edit_timestamp edit_buttons}
63
+ end
64
+ event_venue.index = Radiant::AdminUI::RegionSet.new do |index|
65
+ index.top.concat %w{help_text}
66
+ index.thead.concat %w{title_header location_header url_header modify_header}
67
+ index.tbody.concat %w{title_cell location_cell url_cell modify_cell}
68
+ index.bottom.concat %w{buttons}
69
+ end
70
+ event_venue.remove = event_venue.index
71
+ event_venue.new = event_venue.edit
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,1021 @@
1
+ require 'chronic'
2
+
3
+ module EventCalendarTags
4
+ include Radiant::Taggable
5
+ class TagError < StandardError; end
6
+
7
+ tag 'all_events' do |tag|
8
+ tag.locals.events = Event.all
9
+ tag.expand if tag.locals.events.any?
10
+ end
11
+
12
+ #### events:* tags
13
+ #### initiate an event search and display the results
14
+
15
+ desc %{
16
+ Specify a set of events ready for listing, tabulation or other display.
17
+ Normally but not necessarily called as r:events:each
18
+
19
+ Note that this also sets a calendars variable - so within it you can call r:calendars:each to display calendar names or filtering links - and a calendar variable so that for each event you can also use r:calendar tags. Conversely, r:events will also do the right thing within an r:calendar or r:calendars:each tag.
20
+
21
+ You can limit the set of events in various ways. To show only selected calendars, supply a name as 'calendar' or a comma-separated list of names as 'calendars'. Slugs work there too.
22
+
23
+ <pre><code><r:events:each calendars="oneteam, anotherteam">...</r:events:each></code></pre>
24
+
25
+ To limit to a particular period, any of these will work:
26
+
27
+ * year, week, month and day are numerical attributes that designate a period. If any is specified then the others will default to the present. For any of them you can also supply 'now', 'next' or 'previous'.
28
+ * years, months, weeks and days are numerical attributes that describe an interval from the present.
29
+ * calendar_months is similar but describes a period from the start of the present month
30
+ * from and to are date strings that describe a period. Either can also be set to 'now'.
31
+ * until is like 'to' but forces 'from' to be 'now'.
32
+ * since is like 'from' but forces 'to' to be 'now'.
33
+
34
+ To display a list of all events in April 2010:
35
+
36
+ <pre><code><r:events:each year="2010" month="4">...</r:events:each></code></pre>
37
+
38
+ To display the same events in a calendar view:
39
+
40
+ <pre><code><r:events:month year="2010" month="4" /></code></pre>
41
+
42
+ To list events from the 'fell races' calendar for the next year:
43
+
44
+ <pre><code><r:events:each calendar="fell races" months="12">...</r:events:each></code></pre>
45
+
46
+ To show a stack of small calendar tables for the year with event links and styling:
47
+
48
+ <pre><code><r:events:months calendar="fell races" months="12" compact="true" /></code></pre>
49
+
50
+ If no period or interval is specified, we will return all events in the present month. Often all you want is this:
51
+
52
+ <pre><code><r:events:month /></code></pre>
53
+ }
54
+
55
+ # I really want to put the retrieval logic in the root 'events' tag, but the attributes aren't available there
56
+
57
+ tag "events" do |tag|
58
+ tag.expand
59
+ end
60
+
61
+ tag "events:each" do |tag|
62
+ tag.locals.events ||= get_events(tag)
63
+ result = []
64
+ tag.locals.previous_headers = {}
65
+ tag.locals.events.each do |event|
66
+ tag.locals.event = event
67
+ tag.locals.calendar = event.calendar
68
+ result << tag.expand
69
+ end
70
+ result
71
+ end
72
+
73
+ tag "if_events" do | tag|
74
+ tag.locals.events ||= get_events(tag)
75
+ tag.expand if tag.locals.events.any?
76
+ end
77
+
78
+ tag "unless_events" do | tag|
79
+ tag.locals.events ||= get_events(tag)
80
+ tag.expand unless tag.locals.events.any?
81
+ end
82
+
83
+ desc %{
84
+ This is a shortcut that returns a set of tabulated months covering the period defined.
85
+ It works in the same way as r:events:each but presents the results in a familiar calendar format.
86
+
87
+ See r:events for attributes that specify the period, or try the examples.
88
+
89
+ For the present month, pageable (if the page type is right), with the events listed for each day:
90
+
91
+ <pre><code><r:events:as_calendar month="now" /></code></pre>
92
+
93
+ For the next three months, in little mini-tables that link to event anchors in a separate list
94
+
95
+ <pre><code>
96
+ <r:events:as_calendar calendar_months="3" compact="true" />
97
+ <r:events:each months="3">
98
+ <r:event:summary />
99
+ </r:events:each>
100
+ </code></pre>
101
+
102
+ Note that if you set 'months=3' instead of 'calendar_months=3' then it means three months from today, which actually extends over four calendar months.
103
+
104
+ Full size months until the end of the year:
105
+
106
+ <pre><code><r:events:as_calendar until="12-12" /></code></pre>
107
+
108
+ For more control and different views, see tags like r:events:year, r:events:month and r:events:week.
109
+ }
110
+ tag "events:as_calendar" do |tag|
111
+ attr = parse_boolean_attributes(tag)
112
+ tag.locals.events ||= get_events(tag)
113
+ result = ''
114
+ this_month = nil
115
+ tag.locals.period = period_from_events(tag.locals.events)
116
+ tag.locals.period.start.upto(tag.locals.period.finish) do |day|
117
+ if day.month != this_month
118
+ this_month = day.month
119
+ if !attr[:omit_empty] || tag.locals.events.any? {|event| event.in_this_month?(day) }
120
+ tag.locals.calendar_start = day
121
+ result << tag.render(attr[:compact] ? 'events:minimonth' : 'events:month', attr.dup)
122
+ end
123
+ end
124
+ end
125
+ result
126
+ end
127
+
128
+ desc %{
129
+ Renders a friendly description of the period currently being displayed and any other filters that have been applied.
130
+
131
+ *Usage:*
132
+ <pre><code><r:events:summary [pageinated="true"]/></code></pre>
133
+ }
134
+ tag "events:summary" do |tag|
135
+ tag.locals.events ||= get_events(tag)
136
+ total_events = tag.locals.events.length
137
+ filters = filters_applied(tag)
138
+ html = %{<p class="list_summary">}
139
+ html << %{Showing #{total_events} #{pluralize(total_events, "event")} #{filters.join(" ")}} if filters.any?
140
+ html << %{</p>}
141
+ html
142
+ end
143
+
144
+ desc %{
145
+ Renders a partially modified link to the current set of events.
146
+ Supply a part to override it: all other parts will be carried over.
147
+ Only works on a calendar page.
148
+
149
+ *Usage:*
150
+ <pre><code>
151
+ <r:events:link stem="/map">These events on a map</r:events:link> (applies the present set of filters to a different page)
152
+ <r:events:link year="2011" month="">All events in 2011</r:events:link>
153
+ <r:events:link month="3" year="2010">All events next march</r:events:link>
154
+ <r:events:link slug="public">All events next march</r:events:link>
155
+ </code></pre>
156
+ }
157
+
158
+ tag "events:link" do |tag|
159
+ options = tag.attr.dup
160
+ overrides = {}
161
+ url_parts.keys.each do |part|
162
+ overrides[part] = tag.attr[part.to_s] unless tag.attr[part.to_s].nil?
163
+ end
164
+ text = tag.double? ? tag.expand : "link"
165
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
166
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
167
+
168
+ logger.warn ">> events:link: overrides are #{overrides.inspect}"
169
+
170
+ %{<a href="#{tag.locals.page.url(overrides)}#{anchor}" #{attributes}>#{text}</a>}
171
+ end
172
+
173
+ desc %{
174
+ Renders a feed link that will continue to apply the rules that have been applied to
175
+ get events for this page. Periods, calendars, tags (if you're using them) and other
176
+ filters that apply to the page carrying the link will also apply to the feed.
177
+
178
+ *Usage:*
179
+ <pre><code>
180
+ <r:events:feedlink format="rss" />
181
+ <r:events:feedlink format="ics" protocal="webcal" />
182
+ </code></pre>
183
+ }
184
+
185
+ tag "events:feedlink" do |tag|
186
+ options = tag.attr.dup
187
+ format = options.delete('format') || 'rss'
188
+ protocol = options.delete('protocol') || request.protocol
189
+ protocol += "://" unless protocol.match /\:\/\/$/
190
+ parameters = url_parts
191
+ parameters.delete(:path)
192
+ parameters[:format] = format.to_sym
193
+ parameters[:host] = request.host
194
+ parameters[:protocol] = protocol
195
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
196
+ url = calendar_url(parameters)
197
+ text = tag.double? ? tag.expand : format
198
+ %{<a href="#{url}" #{attributes}>#{text}</a>}
199
+ end
200
+
201
+ #### Calendars:* tags
202
+ #### iterate over the set of calendars
203
+
204
+ desc %{
205
+ Loop over a set of calendars specified by the usual search conditions.
206
+
207
+ *Usage:*
208
+ <pre><code><r:calendars:each>...</r:calendars:each></code></pre>
209
+ }
210
+ tag 'calendars' do |tag|
211
+ tag.locals.calendars ||= set_calendars(tag)
212
+ tag.expand
213
+ end
214
+
215
+ tag 'calendars:each' do |tag|
216
+ result = []
217
+ tag.locals.calendars.each do |cal|
218
+ tag.locals.calendar = cal
219
+ result << tag.expand
220
+ end
221
+ result
222
+ end
223
+
224
+ desc %{
225
+ Renders a sensible description of the currently displayed calendars, with links to each separate calendar.
226
+ Pass with_subscription="true" to append a little ical subscription icon to each link.
227
+
228
+ *Usage:*
229
+ <pre><code><r:calendars:summary /></code></pre>
230
+ }
231
+ tag 'calendars:summary' do |tag|
232
+ result = "Showing events from the "
233
+ result << tag.render('calendars:list', tag.attr.dup)
234
+ result << ' '
235
+ result << pluralize(tag.locals.calendars.length, 'calendar')
236
+ result << %{ <a href="#{tag.render('url')}" class="note">(show all)</a>} if calendar_category
237
+ result << '.'
238
+ result
239
+ end
240
+
241
+ desc %{
242
+ Renders a plain list (in sentence form) of the currently displayed calendars, with links to each separate calendar.
243
+ Pass with_subscription="true" to append a little ical subscription icon to each link.
244
+
245
+ *Usage:*
246
+ <pre><code><r:calendars:list /></code></pre>
247
+ }
248
+ tag 'calendars:list' do |tag|
249
+ links = []
250
+ with_subscription = (tag.attr['with_subscription'] == 'true')
251
+ tag.locals.calendars.each do |calendar|
252
+ tag.locals.calendar = calendar
253
+ link = tag.render("calendar:link")
254
+ link << tag.render("calendar:ical_icon") if with_subscription
255
+ links << link
256
+ end
257
+ links.to_sentence
258
+ end
259
+
260
+ #### Calendar:* tags
261
+ #### select and display attributes of a single calendar
262
+
263
+ tag 'calendar' do |tag|
264
+ tag.locals.calendar ||= get_calendar(tag)
265
+ raise TagError, "No calendar" unless tag.locals.calendar
266
+ tag.expand
267
+ end
268
+
269
+ [:id, :name, :description, :category, :slug].each do |attribute|
270
+ desc %{
271
+ Renders the #{attribute} attribute of the current calendar.
272
+
273
+ Usage:
274
+ <pre><code><r:calendar:#{attribute} /></code></pre>
275
+ }
276
+ tag "calendar:#{attribute}" do |tag|
277
+ tag.locals.calendar.send(attribute)
278
+ end
279
+ end
280
+
281
+ [:last_refresh_date, :last_refresh_count, :url, :username, :password].each do |attribute|
282
+ desc %{
283
+ Renders the #{attribute} attribute of the ical subscription associated with the present calendar.
284
+
285
+ Usage:
286
+ <pre><code><r:calendar:#{attribute} /></code></pre>
287
+ }
288
+ tag "calendar:#{attribute}" do |tag|
289
+ tag.locals.calendar.ical.send(attribute)
290
+ end
291
+ end
292
+
293
+ desc %{
294
+ Loops over the events of the present calendar.
295
+ Takes the same sort and selection options as r:events (except for those that specify which calendars to show), and within it you can use all the usual r:event and r:calendar tags.
296
+
297
+ *Usage:*
298
+ <pre><code><r:calendar:events:each>...</r:calendar:events:each></code></pre>
299
+ }
300
+ tag 'calendar:events' do |tag|
301
+ tag.expand
302
+ end
303
+
304
+ tag 'calendar:events:each' do |tag|
305
+ tag.locals.events ||= get_events(tag)
306
+ result = []
307
+ tag.locals.previous_headers = {}
308
+ tag.locals.events.each do |event|
309
+ tag.locals.event = event
310
+ result << tag.expand
311
+ end
312
+ result
313
+ end
314
+
315
+ desc %{
316
+ Renders the address that would be used to display the current calendar by itself on the current page.
317
+ If the page isn't an EventCalendar page, this will still work but the destination probably won't.
318
+
319
+ Usage:
320
+ <pre><code><r:calendar:url /></code></pre>
321
+ }
322
+ tag "calendar:url" do |tag|
323
+ clean_url([tag.locals.page.url, tag.locals.calendar.category, tag.locals.calendar.slug].join('/'))
324
+ end
325
+
326
+ desc %{
327
+ Renders a link to the address that would be used to display the current calendar by itself on the current page.
328
+ Attributes and contained text are passed through exactly as for other links.
329
+
330
+ If the page isn't an EventCalendar page, this will still work but the destination probably won't.
331
+
332
+ Usage:
333
+ <pre><code><r:calendar:link /></code></pre>
334
+ }
335
+ tag "calendar:link" do |tag|
336
+ options = tag.attr.dup
337
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
338
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
339
+ attributes = " #{attributes}" unless attributes.empty?
340
+ text = tag.double? ? tag.expand : tag.render('calendar:name')
341
+ %{<a href="#{tag.render('calendar:url')}#{anchor}"#{attributes}>#{text}</a>}
342
+ end
343
+
344
+ desc %{
345
+ Renders the path to the local .ics file for this calendar, suitable for read-only subscription in iCal or other calendar programs.
346
+
347
+ Usage:
348
+ <pre><code><r:calendar:ical_url /></code></pre>
349
+ }
350
+ tag "calendar:ical_url" do |tag|
351
+ tag.locals.calendar.to_ics
352
+ end
353
+
354
+ desc %{
355
+ Renders a link to the local .ics file for this calendar, suitable for read-only subscription in iCal or other calendar programs.
356
+ Link attributes are passed through as usual.
357
+
358
+ Usage:
359
+ <pre><code><r:calendar:ical_link /></code></pre>
360
+ }
361
+ tag "calendar:ical_link" do |tag|
362
+ options = tag.attr.dup
363
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
364
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
365
+ attributes = " #{attributes}" unless attributes.empty?
366
+ text = tag.double? ? tag.expand : tag.render('calendar:name')
367
+ %{<a href="#{tag.render('calendar:ical_url')}#{anchor}"#{attributes}>#{text}</a>}
368
+ end
369
+
370
+ desc %{
371
+ Renders a small graphical link to the local .ics file for this calendar.
372
+
373
+ Usage:
374
+ <pre><code><r:calendar:ical_icon /></code></pre>
375
+ }
376
+ tag "calendar:ical_icon" do |tag|
377
+ text = tag.attr['text'] || ''
378
+ %{<a href="#{tag.render('calendar:ical_url')}" class="ical"><img src="/images/event_calendar/ical16.png" alt="subscribe to #{tag.render('calendar:name')}" width="16" height="16" /> #{text}</a>}
379
+ end
380
+
381
+ #### Event:* tags
382
+ #### display attributes of a single event
383
+
384
+ tag "event" do |tag|
385
+ raise TagError, "can't have r:event without an event" unless tag.locals.event
386
+ tag.expand
387
+ end
388
+
389
+ desc %{
390
+ We display the content between these tags only when it has changed.
391
+ Here this is normally used to display the date above a list of events for that day but you can also list by calendar or by week, or whatever.
392
+ You will want to pass a corresponding order parameter to r:events or r:calendar:events.
393
+ }
394
+ tag 'event:header' do |tag|
395
+ previous_headers = tag.locals.previous_headers || {}
396
+ name = tag.attr['name'] || :unnamed
397
+ restart = (tag.attr['restart'] || '').split(';')
398
+ header = tag.expand
399
+ unless header == previous_headers[name]
400
+ previous_headers[name] = header
401
+ unless restart.empty?
402
+ restart.each do |n|
403
+ previous_headers[n] = nil
404
+ end
405
+ end
406
+ header
407
+ end
408
+ end
409
+
410
+ [:id, :title, :description, :short_description, :location, :url].each do |attribute|
411
+ desc %{
412
+ Renders the #{attribute} attribute of the current event.
413
+
414
+ Usage:
415
+ <pre><code><r:event:#{attribute} /></code></pre>
416
+ }
417
+ tag "event:#{attribute}" do |tag|
418
+ tag.locals.event.send(attribute)
419
+ end
420
+
421
+ desc %{
422
+ Contents are rendered if the event has a #{attribute}.
423
+
424
+ Usage:
425
+ <pre><code><r:event:if_#{attribute}>...</r:event:if_#{attribute}></code></pre>
426
+ }
427
+ tag "event:if_#{attribute}" do |tag|
428
+ tag.expand if tag.locals.event.send(attribute)
429
+ end
430
+
431
+ desc %{
432
+ Contents are rendered unless the event has a #{attribute}.
433
+
434
+ Usage:
435
+ <pre><code><r:event:unless_#{attribute}>...</r:event:unless_#{attribute}></code></pre>
436
+ }
437
+ tag "event:unless_#{attribute}" do |tag|
438
+ tag.expand unless tag.locals.event.send(attribute)
439
+ end
440
+ end
441
+
442
+ #todo: venue:* tags
443
+
444
+ desc %{
445
+ If the current event has a venue, this renders a sensible description and link. If not, it returns the location string.
446
+
447
+ Usage:
448
+ <pre><code><r:event:venue /></code></pre>
449
+ }
450
+ tag "event:venue" do |tag|
451
+ if venue = tag.locals.event.event_venue
452
+ if venue.url
453
+ %{<a href="#{venue.url}">#{venue.title}</a>, #{venue.address}}
454
+ else
455
+ %{#{venue.title}, #{venue.address}}
456
+ end
457
+ else
458
+ tag.render('event:location')
459
+ end
460
+ end
461
+
462
+ desc %{
463
+ If the event has a url, renders a link to that address around the title of the event. If not, just the title without a link.
464
+ As usual, if the tag is double the contents are placed within the link, and any other attributes are passed through to the link tag.
465
+
466
+ Usage:
467
+ <pre><code>
468
+ <r:event:link />
469
+ <r:event:link class="dated"><r:event:date />: <r:event:title /></r:event:link>
470
+ </code></pre>
471
+ }
472
+ tag "event:link" do |tag|
473
+ options = tag.attr.dup
474
+ text = tag.double? ? tag.expand : tag.render('event:title')
475
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
476
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
477
+ if tag.locals.event.url
478
+ %{<a href="#{tag.render('url')}#{anchor}" #{attributes}>#{text}</a>}
479
+ else
480
+ %{<span #{attributes}>#{text}</span>}
481
+ end
482
+ end
483
+
484
+ desc %{
485
+ Renders a standard block listing the event and adding whatever links and descriptions are available.
486
+
487
+ Supply with_month="true" or with_year="true" to show more date information. Supply with_time="false" if you don't want to show the start time.
488
+
489
+ This may well be all you need:
490
+ <pre><code>
491
+ <r:events:each year="now" />
492
+ <r:events:header name="month"><r:event:month /></r:events:header>
493
+ <r:event:summary />
494
+ </r:events:each />
495
+ </code></pre>
496
+ }
497
+ tag "event:summary" do |tag|
498
+ options = tag.attr.dup
499
+ result = %{
500
+ <li class="event" id ="event_#{tag.render('event:id')}">
501
+ <span class="date">#{tag.render('event:day_ordinal')}}
502
+ result << %{ #{tag.render('event:month')}} if options['with_month'] == 'true'
503
+ result << %{ #{tag.render('event:year')}} if options['with_year'] == 'true'
504
+ result << %{ at #{tag.render('event:start')}} unless options['with_time'] == 'false'
505
+ result << %{</span>
506
+ #{tag.render('event:link', options.merge({'class' => 'title'}))}
507
+ }
508
+ result << %{<br /><span class="location">#{tag.render('event:location')}</span>} if tag.locals.event.location
509
+ result << %{<br /><span class="description">#{tag.render('event:description')}</span>} if tag.locals.event.description
510
+ result << %{</li>}
511
+ result
512
+ end
513
+
514
+ desc %{
515
+ Renders a sensible presentation of the time of the event. This is usually all you need, as it will do the right thing with all-day events.
516
+
517
+ The presentation is minimal: 10:00am will be shortened to 10am.
518
+
519
+ Usage:
520
+ <pre><code><r:event:time />: <r:event:title /></code></pre>
521
+ }
522
+ tag "event:time" do |tag|
523
+ if tag.locals.event.all_day?
524
+ "All day"
525
+ else
526
+ tag.locals.event.start_time
527
+ end
528
+ end
529
+
530
+ [:start, :end].each do |attribute|
531
+ desc %{
532
+ Renders the #{attribute} time (or date) of the current event with the specified strftime format. Default is 24 hour hh:mm.
533
+
534
+ Usage:
535
+ <pre><code><r:event:#{attribute} [format=""] /></code></pre>
536
+ }
537
+ tag "event:#{attribute}" do |tag|
538
+ date = tag.locals.event.send("#{attribute}_date")
539
+ date.strftime(tag.attr['format'] || "%H:%M")
540
+ end
541
+ desc %{
542
+ Renders the #{attribute} date (or time) of the current event as ordinal day, month.
543
+
544
+ Usage:
545
+ <pre><code><r:event:#{attribute}_date /></code></pre>
546
+ }
547
+ tag "event:#{attribute}_date" do |tag|
548
+ date = tag.locals.event.send("#{attribute}_date")
549
+ %{#{date.mday.ordinalize} #{Date::MONTHNAMES[date.month]}}
550
+ end
551
+ end
552
+
553
+ desc %{
554
+ Renders the start time of the current event with the specified strftime format. Unlike start and end, the default here is '%m/%d/%Y'
555
+
556
+ Usage:
557
+ <pre><code>
558
+ <r:events:each>
559
+ <r:event:header>
560
+ <h3><r:event:date [format="%d %M"] /></h3>
561
+ <ul>
562
+ </r:event:header>
563
+ <r:event:start /> <r:event:title />
564
+ </r:events:each>
565
+ </ul>
566
+ </code></pre>
567
+ }
568
+ tag "event:date" do |tag|
569
+ tag.locals.event.start_date.strftime(tag.attr['format'] || "%m/%d/%Y")
570
+ end
571
+
572
+ desc %{
573
+ Prints the day-of-week of the start date of the current event.
574
+ Equivalent to calling <r:event:date format="%A" />.
575
+ For the short form, set short="true"
576
+
577
+ Usage:
578
+ <pre><code><r:event:weekday [short="true"] /></code></pre>
579
+ }
580
+ tag "event:weekday" do |tag|
581
+ tag.attr['short'] == 'true' ? Date::ABBR_DAYNAMES[tag.locals.event.start_date.wday] : Date::DAYNAMES[tag.locals.event.start_date.wday]
582
+ end
583
+
584
+ desc %{
585
+ Prints the day-of-month of the start date of the current event.
586
+ Equivalent to calling <r:event:date format="%d" />.
587
+ Supply zeropad="true" to get 1 as 01.
588
+
589
+ Usage:
590
+ <pre><code><r:event:day /></code></pre>
591
+ }
592
+ tag "event:day" do |tag|
593
+ d = tag.locals.event.start_date.mday
594
+ if tag.attr['zeropad'] == 'true'
595
+ "%02d" % d
596
+ else
597
+ d
598
+ end
599
+ end
600
+
601
+ desc %{
602
+ Prints the ordinal form of the day-of-month of the start date of the current event.
603
+
604
+ Usage:
605
+ <pre><code><r:event:day_ordinal /></code></pre>
606
+ }
607
+ tag "event:day_ordinal" do |tag|
608
+ tag.locals.event.start_date.mday.ordinalize
609
+ end
610
+
611
+ desc %{
612
+ Prints the week-of-year of the start date of the current event.
613
+ Equivalent to calling <r:event:date format="%W" />.
614
+
615
+ Usage:
616
+ <pre><code><r:event:week /></code></pre>
617
+ }
618
+ tag "event:week" do |tag|
619
+ tag.locals.event.start_date.cweek
620
+ end
621
+
622
+ desc %{
623
+ Prints the name of the month of the start date of the current event.
624
+ Equivalent to calling <r:event:date format="%B" />.
625
+
626
+ Usage:
627
+ <pre><code><r:event:month /></code></pre>
628
+ }
629
+ tag "event:month" do |tag|
630
+ Date::MONTHNAMES[tag.locals.event.start_date.month]
631
+ end
632
+
633
+ desc %{
634
+ Prints the year of the start date of the current event.
635
+ Equivalent to calling <r:event:date format="%Y" />.
636
+
637
+ Usage:
638
+ <pre><code><r:event:year /></code></pre>
639
+ }
640
+ tag "event:year" do |tag|
641
+ tag.locals.event.start_date.year
642
+ end
643
+
644
+ desc %{
645
+ The date range of the event. If start and finish are the same, shows just a start time.
646
+ If they differ, shows a range using the format and separator specified.
647
+ All day events just show "all day".
648
+
649
+ Usage:
650
+ <pre><code><r:event:period [format=""] [separator=""] /></code></pre>
651
+ }
652
+ tag "event:period" do |tag|
653
+ tag.locals.event.summarize_period
654
+ end
655
+
656
+ desc %{
657
+ Contents are rendered only if this is an all-day event.
658
+
659
+ Usage:
660
+ <pre><code><r:event:if_all_day>...</r:event:if_all_day></code></pre>
661
+ }
662
+ tag "event:if_all_day" do |tag|
663
+ tag.expand if tag.locals.event.allday?
664
+ end
665
+
666
+ desc %{
667
+ Contents are rendered only if this is not an all-day event.
668
+
669
+ Usage:
670
+ <pre><code><r:event:unless_all_day>...</r:event:unless_all_day></code></pre>
671
+ }
672
+ tag "event:unless_all_day" do |tag|
673
+ tag.expand unless tag.locals.event.allday?
674
+ end
675
+
676
+ desc %{
677
+ Returns a day-and-date stamp suitable for CSS styling.
678
+ Supply a 'class' parameter to add classes to the containing div.
679
+
680
+ Usage:
681
+ <pre><code><r:event:datemark /></code></pre>
682
+ }
683
+ tag "event:datemark" do |tag|
684
+ html = ""
685
+ html << _datemark(tag.locals.event.start_date)
686
+ html
687
+ end
688
+
689
+ def _datemark(date=Time.now)
690
+ %{
691
+ <div class="datemark"><span class="month">#{Date::ABBR_MONTHNAMES[date.month]}</span><span class="dom">#{"%02d" % date.mday}</span></div>
692
+ }
693
+ end
694
+
695
+ desc %{
696
+ Renders a little calendar table for a single month. Like all events: tags, if no period is specified, it defaults to the present month.
697
+ Usually you'll want to specify month and year attributes. An EventCalendar page will also obey month and year request parameters.
698
+ If a period is specified longer than a month, we just render the first month: in that case you might want to use r:events:months to get several displayed at once.
699
+
700
+ Usage:
701
+ <pre><code><r:events:minimonth [year=""] [month=""] /></code></pre>
702
+
703
+ }
704
+ tag "events:minimonth" do |tag|
705
+ attr = parse_boolean_attributes(tag)
706
+ tag.locals.events ||= get_events(tag)
707
+
708
+ first_day = tag.locals.calendar_start ? tag.locals.calendar_start.beginning_of_month : tag.locals.period.start.beginning_of_month
709
+ last_day = first_day.end_of_month
710
+ first_shown = first_day.beginning_of_week # padding of period to show whole months
711
+ last_shown = last_day.end_of_week
712
+ previous = first_day - 1.day
713
+ following = last_day + 1.day
714
+
715
+ with_paging = attr[:month_links] == 'true'
716
+ with_list = attr[:event_list] == 'true'
717
+ with_subscription = attr[:subscription_link] == 'true'
718
+ day_links = attr[:day_links] == 'true'
719
+
720
+ cal = %(<table class="minimonth"><thead><tr>)
721
+ cal << %(<th class="month_link"><a href="#{tag.locals.page.url(:year => previous.year, :month => previous.month)}" title="#{month_names[previous.month]}" class="previous">&lt;</a></th>) if with_paging
722
+ cal << %(<th colspan="#{with_paging ? 5 : 7}" class="month_name">)
723
+ cal << %(<a href="#{tag.locals.page.url(:year => first_day.year, :month => first_day.month)}">) if with_paging
724
+ cal << %(#{month_names[first_day.month]} #{first_day.year})
725
+ cal << %(</a>) if with_paging
726
+ cal << %(</th>)
727
+ cal << %(<th class="month_link"><a href="#{tag.locals.page.url(:year => following.year, :month => following.month)}" title="#{month_names[following.month]}" class="next">&gt;</a></th>) if with_paging
728
+ cal << %(</tr><tr>)
729
+ cal << day_names.map { |d| %{<th class="day_name" scope="col">#{d.first}</th>} }.join
730
+ cal << "</tr></thead><tbody>"
731
+
732
+ first_shown.upto(last_shown) do |day|
733
+ events_today = tag.locals.events.select{ |e| e.on_this_day?(day) }
734
+ event_list = cell_text = date_label = ""
735
+ cell_class = "day"
736
+ cell_class += " today" if today?(day)
737
+ cell_class += " past" if day < Date.today
738
+ cell_class += " future" if day > Date.today
739
+ cell_class += " other_month" if day.month != first_day.month
740
+ unless day.month != first_day.month
741
+ cell_class += " weekend_day" if weekend?(day)
742
+ cell_class += " weekend_today" if weekend?(day) && today?(day)
743
+ date_label = day.mday
744
+ if events_today.any?
745
+ cell_class += " eventful"
746
+ cell_class += " eventful_weekend" if weekend?(day)
747
+ cell_class += events_today.map{|e| " #{e.slug}"}.join
748
+ cell_url = day_links ? url(:day => day) : "#event_#{events_today.first.id}"
749
+ date_label = %{<a href="#{cell_url}">#{date_label}</a>}
750
+ else
751
+ cell_class += " uneventful"
752
+ end
753
+ cell_text = %{#{date_label}#{event_list}}
754
+ end
755
+ cal << "<tr>" if day == day.beginning_of_week
756
+ cal << %{<td class="#{cell_class}">#{cell_text}</td>}
757
+ cal << "</tr>" if day == day.end_of_week
758
+ end
759
+ if with_list
760
+ cal << %{<tr><td colspan="7" class="event_list"><ul>}
761
+ tag.locals.events.each do |event|
762
+ tag.locals.event = event
763
+ cal << tag.render('event:summary')
764
+ end
765
+ cal << "</ul>"
766
+ cal << %{<p>#{tag.render('calendar:ical_icon', tag.attr.merge("text" => "Subscribe to calendar"))}} if with_subscription
767
+ cal << "</td></tr>"
768
+ end
769
+ cal << %{</tbody></table>}
770
+ cal
771
+ end
772
+
773
+ desc %{
774
+ Renders a full calendar table for a single month. Like all events: tags, if no period is specified, it defaults to the present month.
775
+ Usually you'll want to specify month and year attributes. An EventCalendar page will also obey month and year request parameters
776
+ but only if the corresponding attributes have been specified.
777
+
778
+ If a period is specified longer than a month, we just render the first month.
779
+
780
+ If you use 'previous', 'now' and 'next' in your period attributes, an EventCalendar page will show the right period relative to input.
781
+
782
+ Usage:
783
+ <pre><code><r:event:month [year=""] [month=""] [compact="true"] /></code></pre>
784
+
785
+ }
786
+ tag "events:month" do |tag|
787
+ attr = parse_boolean_attributes(tag)
788
+ tag.locals.events ||= get_events(tag)
789
+ table_class = 'month'
790
+
791
+ first_day = tag.locals.calendar_start ? tag.locals.calendar_start.beginning_of_month : tag.locals.period.start.beginning_of_month
792
+ first_shown = first_day.beginning_of_week # padding of period to fill month table
793
+ last_day = first_day.end_of_month
794
+ last_shown = last_day.end_of_week
795
+ previous = first_day - 1.day
796
+ following = last_day + 1.day
797
+
798
+ month_names = Date::MONTHNAMES.dup
799
+ day_names = Date::DAYNAMES.dup
800
+ day_names.push(day_names.shift) # Class::Date and ActiveSupport::CoreExtensions::Time::Calculations have different ideas of when is the start of the week. We've gone for the rails standard.
801
+
802
+ cal = %(<table class="#{table_class}"><thead><tr>)
803
+ cal << %(<th colspan="2" class="month_link"><a href="?year=#{previous.year}&amp;month=#{previous.month}">&lt; #{month_names[previous.month]}</a></th>) if attr[:month_links]
804
+ cal << %(<th colspan="#{attr[:month_links] ? 3 : 7}" class="month_name">#{month_names[first_day.month]} #{first_day.year}</th>)
805
+ cal << %(<th colspan="2" class="month_link"><a href="?year=#{following.year}&amp;month=#{following.month}">#{month_names[following.month]} &gt;</a></th>) if attr[:month_links]
806
+ cal << %(</tr><tr class="day_name">)
807
+ cal << day_names.map { |d| "<th scope='col'>#{d}</th>" }.join
808
+ cal << "</tr></thead><tbody>"
809
+
810
+ first_shown.upto(last_shown) do |day|
811
+ events_today = tag.locals.events.select{ |e| e.start_date <= day + 1.day && e.end_date >= day }
812
+ event_list = cell_text = date_label = ""
813
+ cell_class = "day"
814
+ cell_class += " other_month" if day.month != first_day.month
815
+ cell_class += " weekend_day" if weekend?(day)
816
+ cell_class += " today" if today?(day)
817
+ cell_class += " weekend_today" if weekend?(day) && today?(day)
818
+ date_label = day.mday
819
+
820
+ if events_today.any?
821
+ cell_class += " eventful"
822
+ cell_class += " eventful_weekend" if weekend?(day)
823
+ cell_class += events_today.map{|e| " #{e.calendar.slug}"}.join
824
+ event_list << %{<ul>} << events_today.map { |e| %{<li><span class="time">#{e.nice_start_time}:</span> #{e.title}</li>} }.join << "</ul>"
825
+ else
826
+ cell_class += " uneventful"
827
+ end
828
+
829
+ date_label = %{<h4>#{date_label}</h4>}
830
+ cell_text = %{<div class="event_holder">#{date_label}#{event_list}</div>}
831
+ cal << "<tr>" if day == day.beginning_of_week
832
+ cal << %{<td class="#{cell_class}">#{cell_text}</td>}
833
+ cal << "</tr>" if day == day.end_of_week
834
+ end
835
+ cal << %{</tbody></table>}
836
+ cal
837
+ end
838
+
839
+ private
840
+
841
+ # parse_boolean_attributes turns "true" into true and everything else into false
842
+ # and incidentally symbolizes the keys
843
+
844
+ def parse_boolean_attributes(tag)
845
+ attr = tag.attr.symbolize_keys
846
+ [:month_links, :date_links, :event_links, :compact].each do |param|
847
+ attr[param] = false unless attr[param] == 'true'
848
+ end
849
+ attr
850
+ end
851
+
852
+ # these calculations all return a CalendarPeriod object, which is really just a beginning and end marker with some useful operations
853
+
854
+ def set_period(tag)
855
+ attr = tag.attr.symbolize_keys
856
+ if self.class == EventCalendarPage && period = self.calendar_period
857
+ return period
858
+ end
859
+
860
+ date_parts = [:year, :month, :week, :day]
861
+ interval_parts = [:months, :calendar_months, :days, :since, :until, :from, :to]
862
+ relatives = {'previous' => -1, 'now' => 0, 'next' => 1}
863
+
864
+ # 1. fully specified period: any numeric date part found
865
+ # eg. <r:events:each year="2010" month="3">
866
+
867
+ specified_date_parts = date_parts.select {|p| attr[p] && attr[p] == attr[p].to_i.to_s}
868
+ if specified_date_parts.any?
869
+ return period_from_parts(attr.slice(*specified_date_parts))
870
+ end
871
+
872
+ # 2. relative period: any relative date part specified (and no numeric date part)
873
+ # eg. <r:events:each month="now"> or <r:events:each month="next">
874
+
875
+ if relative_part = date_parts.find {|p| relatives.keys.include? attr[p]}
876
+ period = period_from_parts(relative_part => Date.today.send(relative_part))
877
+ period += 1.send(relative_part) if attr[relative_part] == 'next'
878
+ period -= 1.send(relative_part) if attr[relative_part] == 'previous'
879
+ return period
880
+ end
881
+
882
+ # 3. relative interval
883
+ # eg. <r:events:each months="3"> or <r:events:each since="12/12/1969">
884
+ # any date string understood by chronic should be ok
885
+
886
+ specified_interval_parts = interval_parts.select {|p| !attr[p].blank?}
887
+ if specified_interval_parts.any?
888
+ parts = attr.slice(*specified_interval_parts)
889
+ return period_from_interval(parts)
890
+ end
891
+
892
+ # overall default will be to show (paginated) all future events
893
+ return CalendarPeriod.default
894
+ end
895
+
896
+ def period_from_parts(parts={})
897
+ parts.each {|k,v| parts[k] = parts[k].to_i}
898
+ return CalendarPeriod.from(Date.civil(parts[:year], 1, 1), 1.year) if parts[:year] and not parts[:month]
899
+ parts[:year] ||= Date.today.year
900
+ return CalendarPeriod.from(Date.civil(parts[:year], parts[:month], 1), 1.month - 1.day) if parts[:month] && !parts[:week] && !parts[:day]
901
+ parts[:month] ||= Date.today.month
902
+ return CalendarPeriod.from(Date.commercial(parts[:year], parts[:week], 1), 1.week ) if parts[:week]
903
+ return CalendarPeriod.from(Date.civil(parts[:year], parts[:month], parts[:day]), 1.day) if parts[:day]
904
+
905
+ # default from parts is the present month
906
+ return CalendarPeriod.from(Date.civil(parts[:year], parts[:month], 1), 1.month)
907
+ end
908
+
909
+ def period_from_interval(parts={})
910
+ # from and to fully specified (including since and until as they are always relative to the present)
911
+ return CalendarPeriod.between(Time.now, Chronic.parse(parts[:until])) if parts[:until]
912
+ return CalendarPeriod.between(Chronic.parse(parts[:since]), Time.now) if parts[:since]
913
+ return CalendarPeriod.between(Chronic.parse(parts[:from]), Chronic.parse(parts[:to])) if parts[:from] && parts[:to]
914
+
915
+ # starting point defaults to now
916
+ parts[:from] = Date.today if parts[:from].blank? || parts[:from] == 'now'
917
+ from = Chronic.parse(parts[:from])
918
+
919
+ # start moves to the first of the month if we're displaying calendar months
920
+ return CalendarPeriod.from(from.beginning_of_month, parts[:calendar_months].to_i.months - 1.day) if parts[:calendar_months]
921
+
922
+ # and in the end it's just a question of how much of the future to show
923
+ return CalendarPeriod.from(from, parts[:years].to_i.years) if parts[:years]
924
+ return CalendarPeriod.from(from, parts[:months].to_i.months) if parts[:months]
925
+ return CalendarPeriod.from(from, parts[:weeks].to_i.weeks) if parts[:weeks]
926
+ return CalendarPeriod.from(from, parts[:days].to_i.days) if parts[:days]
927
+
928
+ # default is all future
929
+ return CalendarPeriod.from(from)
930
+ end
931
+
932
+ # If no period has been specified then we may need to examine the current page of events to work out which month tables to display
933
+ def period_from_events(events=[])
934
+ if events.any?
935
+ period_from_interval :from => events.first.start_date, :to => events.last.start_date
936
+ end
937
+ end
938
+
939
+ # filter by calendar
940
+ # returns a simple list which can be passed to Event.in_calendar
941
+
942
+ def set_calendars(tag)
943
+ attr = tag.attr.symbolize_keys
944
+ if tag.locals.calendar # either we're inside an r:calendar tag or we're eaching calendars. either way, it's set for us and parameters have no effect
945
+ return [tag.locals.calendar]
946
+ elsif attr[:slugs] && attr[:slugs] != 'all'
947
+ return Calendar.with_slugs(attr[:slugs])
948
+ elsif attr[:calendars]
949
+ return Calendar.with_names_like(attr[:calendars])
950
+ elsif self.class == EventCalendarPage
951
+ calendar_set
952
+ else
953
+ Calendar.find(:all)
954
+ end
955
+ end
956
+
957
+ # combines all the scopes that have been set
958
+ # and returns a list of events
959
+
960
+ def get_events(tag)
961
+ Ical.check_refreshments
962
+ tag.locals.period ||= set_period(tag)
963
+ tag.locals.calendars ||= set_calendars(tag)
964
+ ef = event_finder(tag)
965
+ tag.attr[:by] ||= 'start_date'
966
+ tag.attr[:order] ||= 'asc'
967
+ ef.find(:all, standard_find_options(tag))
968
+ end
969
+
970
+ # other extensions - eg taggable_events - will chain the event_finder to add more scopes
971
+
972
+ def event_finder(tag)
973
+ if tag.locals.period.bounded?
974
+ ef = Event.within(tag.locals.period)
975
+ elsif tag.locals.period.start
976
+ ef = Event.after(tag.locals.period.start)
977
+ else
978
+ ef = Event.before(tag.locals.period.finish)
979
+ end
980
+ ef = ef.in_calendars(tag.locals.calendars) if tag.locals.calendars
981
+ ef
982
+ end
983
+
984
+ def get_calendar(tag)
985
+ raise TagError, "'title' or 'id' attribute required" unless tag.locals.calendar || tag.attr['title'] || tag.attr['id']
986
+ tag.locals.calendar || Calendar.find_by_name(tag.attr['name']) || Calendar.find_by_id(tag.attr['id'])
987
+ end
988
+
989
+ def standard_find_options(tag)
990
+ attr = tag.attr.symbolize_keys
991
+ by = attr[:by] || "name"
992
+ order = attr[:order] || "asc"
993
+ {
994
+ :order => "#{by} #{order}",
995
+ :limit => attr[:limit] || nil,
996
+ :offset => attr[:offset] || nil
997
+ }
998
+ end
999
+
1000
+ # chain anchor point
1001
+ # for building a description of the current result set
1002
+
1003
+ def filters_applied(tag)
1004
+ html = []
1005
+ html << %{<a href="#{tag.locals.page.url(:year => nil, :month => nil)}" class="defilter">#{tag.locals.period.description}</a>} if tag.locals.period && !tag.locals.period.default?
1006
+ html
1007
+ end
1008
+
1009
+ def weekend?(date)
1010
+ [0,6].include?(date.wday)
1011
+ end
1012
+
1013
+ def today?(date)
1014
+ date == ::Date.current
1015
+ end
1016
+
1017
+ def pluralize(count, singular, plural = nil)
1018
+ (count == 1 || count == '1') ? singular : (plural || singular.pluralize)
1019
+ end
1020
+
1021
+ end