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,57 @@
1
+ class Calendar < ActiveRecord::Base
2
+ has_one :ical, :dependent => :destroy
3
+ has_many :events, :dependent => :destroy
4
+ belongs_to :created_by, :class_name => 'User'
5
+ belongs_to :updated_by, :class_name => 'User'
6
+ has_site if respond_to? :has_site
7
+
8
+ validates_presence_of :name
9
+ validates_uniqueness_of :name
10
+ validates_uniqueness_of :slug, :scope => :category
11
+
12
+ accepts_nested_attributes_for :ical, :reject_if => proc { |attributes| attributes['url'].blank? }
13
+ # validates_associated :ical
14
+
15
+ named_scope :in_category, lambda { |category| # string. needs to match exactly
16
+ { :conditions => [ "calendars.category = ?", category ] }
17
+ }
18
+
19
+ named_scope :with_slugs, lambda { |calendar_slugs| # array , or pipe-separated string
20
+ slugs = calendar_slugs.split('|') unless slugs.is_a? Array
21
+ { :conditions => [ slugs.map{"calendars.slug = ?"}.join(' OR '), *slugs ] }
22
+ }
23
+
24
+ named_scope :with_names_like, lambda { |calendar_names| # comma or pipe--separated string of (partial) names. eg. Calendar.with_names_like('team_')
25
+ names = calendar_names.split(/[,\|]\s*/).map{|n| "%#{n}%"}
26
+ { :conditions => [ names.map{"calendars.name LIKE ?"}.join(' OR '), *names ] }
27
+ }
28
+
29
+ def self.categories
30
+ categories = find( :all, :select => "DISTINCT category" ).map(&:category)
31
+ end
32
+
33
+ def self.slugs
34
+ slugs = find( :all, :select => "DISTINCT slug" ).map(&:slug)
35
+ end
36
+
37
+ def to_ics
38
+ ical.to_ics if ical
39
+ end
40
+
41
+ def to_s
42
+ self.name
43
+ end
44
+
45
+ def to_ri_cal
46
+ RiCal.Calendar do |cal|
47
+ events.primary.each do |event|
48
+ cal.add_subcomponent(event.to_ri_cal)
49
+ end
50
+ end
51
+ end
52
+
53
+ def to_ical
54
+ self.to_ri_cal.to_s
55
+ end
56
+
57
+ end
@@ -0,0 +1,409 @@
1
+ require 'uuidtools'
2
+ class Event < ActiveRecord::Base
3
+ include ActionView::Helpers::DateHelper
4
+ include ActionView::Helpers::TextHelper
5
+
6
+ belongs_to :created_by, :class_name => 'User'
7
+ belongs_to :updated_by, :class_name => 'User'
8
+ belongs_to :calendar
9
+ has_site if respond_to? :has_site
10
+
11
+ belongs_to :event_venue
12
+ accepts_nested_attributes_for :event_venue, :reject_if => proc { |attributes| attributes.all? {|k,v| v.blank?} } # radiant 0.8.1 is using rails 2.3.4, which doesn't include the :all_blank sugar
13
+
14
+ belongs_to :master, :class_name => 'Event'
15
+ has_many :occurrences, :class_name => 'Event', :foreign_key => 'master_id', :dependent => :destroy
16
+ has_many :recurrence_rules, :class_name => 'EventRecurrenceRule', :dependent => :destroy, :conditions => {:active => 1}
17
+ accepts_nested_attributes_for :recurrence_rules, :allow_destroy => true, :reject_if => lambda { |attributes| attributes['active'].to_s != '1' }
18
+
19
+ validates_presence_of :uuid, :title, :start_date, :status_id
20
+ validates_uniqueness_of :uuid
21
+
22
+ before_validation :set_uuid
23
+ before_validation :set_default_status
24
+ after_save :update_occurrences
25
+
26
+ default_scope :order => 'start_date ASC', :include => :event_venue
27
+ named_scope :imported, { :conditions => ["status_id = ?", Status[:imported].to_s] }
28
+ named_scope :submitted, { :conditions => ["status_id = ?", Status[:submitted].to_s] }
29
+ named_scope :approved, { :conditions => ["status_id >= (?)", Status[:published].to_s] }
30
+ named_scope :primary, { :conditions => "master_id IS NULL" }
31
+ named_scope :recurrent, { :conditions => "master_id IS NOT NULL" }
32
+
33
+ named_scope :in_calendars, lambda { |calendars| # list of calendar objects
34
+ ids = calendars.map{ |c| c.id }
35
+ { :conditions => [ ids.map{"calendar_id = ?"}.join(" OR "), *ids] }
36
+ }
37
+
38
+ named_scope :after, lambda { |datetime| # datetime. eg calendar.occurrences.after(Time.now)
39
+ { :conditions => ['start_date > ?', datetime] }
40
+ }
41
+
42
+ named_scope :before, lambda { |datetime| # datetime. eg calendar.occurrences.before(Time.now)
43
+ { :conditions => ['start_date < ?', datetime] }
44
+ }
45
+
46
+ named_scope :within, lambda { |period| # CalendarPeriod object
47
+ { :conditions => ['start_date > :start AND start_date < :finish', {:start => period.start, :finish => period.finish}] }
48
+ }
49
+
50
+ named_scope :between, lambda { |start, finish| # datetimable objects. eg. Event.between(reader.last_login, Time.now)
51
+ { :conditions => ['start_date > :start AND start_date < :finish', {:start => start, :finish => finish}] }
52
+ }
53
+
54
+ named_scope :future_and_current, {
55
+ :conditions => ['(end_date > :now) OR (end_date IS NULL AND start_date > :now)', {:now => Time.now}]
56
+ }
57
+
58
+ named_scope :unfinished, lambda { |start| # datetimable object.
59
+ { :conditions => ['start_date < :start AND end_date > :start', {:start => start}] }
60
+ }
61
+
62
+ named_scope :coincident_with, lambda { |start, finish| # datetimable objects.
63
+ { :conditions => ['(start_date < :finish AND end_date > :start) OR (end_date IS NULL AND start_date > :start AND start_date < :finish)', {:start => start, :finish => finish}] }
64
+ }
65
+
66
+ named_scope :by_end_date, { :order => "end_date ASC" }
67
+
68
+ def self.in_the_last(period) # seconds. eg calendar.occurrences.in_the_last(1.week)
69
+ finish = Time.now
70
+ start = finish - period
71
+ between(start, finish)
72
+ end
73
+
74
+ def self.in_year(year) # just a number. eg calendar.occurrences.in_year(2010)
75
+ start = DateTime.civil(year)
76
+ finish = start + 1.year
77
+ between(start, finish)
78
+ end
79
+
80
+ def self.in_month(year, month) # numbers. eg calendar.occurrences.in_month(2010, 12)
81
+ start = DateTime.civil(year, month)
82
+ finish = start + 1.month
83
+ between(start, finish)
84
+ end
85
+
86
+ def self.in_week(year, week) # numbers, with a commercial week: eg calendar.occurrences.in_week(2010, 35)
87
+ start = DateTime.commercial(year, week)
88
+ finish = start + 1.week
89
+ between(start, finish)
90
+ end
91
+
92
+ def self.on_day (year, month, day) # numbers: eg calendar.occurrences.on_day(2010, 12, 12)
93
+ start = DateTime.civil(year, month, day)
94
+ finish = start + 1.day
95
+ between(start, finish)
96
+ end
97
+
98
+ def self.future
99
+ after(Time.now)
100
+ end
101
+
102
+ def self.past
103
+ before(Time.now)
104
+ end
105
+
106
+ def self.as_months
107
+ stack = {}
108
+ find(:all).each_with_object({}) do |event, stack|
109
+ y = event.start_date.year
110
+ m = Date::MONTHNAMES[event.start_date.month]
111
+ stack[y] ||= {}
112
+ stack[y][m] ||= []
113
+ stack[y][m].push event
114
+ end
115
+ stack
116
+ end
117
+
118
+ def category
119
+ calendar.category if calendar
120
+ end
121
+
122
+ def slug
123
+ calendar.slug if calendar
124
+ end
125
+
126
+ def description_paragraph
127
+ if description =~ /\<p/
128
+ description
129
+ else
130
+ "<p>#{description}</p>"
131
+ end
132
+ end
133
+
134
+ def short_description(length=256, ellipsis="...")
135
+ truncate(description, length, ellipsis)
136
+ end
137
+
138
+ def master?
139
+ master.nil?
140
+ end
141
+
142
+ def occurrence?
143
+ !master?
144
+ end
145
+
146
+ def location
147
+ event_venue ? event_venue.to_s : read_attribute(:location)
148
+ end
149
+
150
+ def address
151
+ event_venue ? event_venue.address : read_attribute(:location)
152
+ end
153
+
154
+ def postcode
155
+ event_venue ? event_venue.postcode : read_attribute(:postcode)
156
+ end
157
+
158
+ def date
159
+ start_date.to_datetime.strftime(date_format)
160
+ end
161
+
162
+ def month
163
+ Date::MONTHNAMES[start_date.month]
164
+ end
165
+
166
+ def short_month
167
+ Date::ABBR_MONTHNAMES[start_date.month]
168
+ end
169
+
170
+ def year
171
+ start_date.year
172
+ end
173
+
174
+ def day
175
+ Date::DAYNAMES[start_date.day]
176
+ end
177
+
178
+ def mday
179
+ start_date.mday
180
+ end
181
+
182
+ def short_date
183
+ start_date.to_datetime.strftime(short_date_format)
184
+ end
185
+
186
+ def start_time
187
+ start_date.to_datetime.strftime(start_date.min == 0 ? round_time_format : time_format).downcase
188
+ end
189
+
190
+ def end_time
191
+ end_date.to_datetime.strftime(end_date.min == 0 ? round_time_format : time_format).downcase if end_date
192
+ end
193
+
194
+ def last_day
195
+ end_date.to_datetime.strftime(date_format)if end_date
196
+ end
197
+
198
+ def duration
199
+ if end_date
200
+ end_date - start_date
201
+ else
202
+ 0
203
+ end
204
+ end
205
+
206
+ def starts
207
+ if all_day?
208
+ "all day"
209
+ else
210
+ start_time
211
+ end
212
+ end
213
+
214
+ def finishes
215
+ if all_day?
216
+ "all day"
217
+ else
218
+ end_time
219
+ end
220
+ end
221
+
222
+ def summarize_start
223
+ if one_day?
224
+ "all day on #{date}"
225
+ elsif all_day?
226
+ "from #{date}"
227
+ else
228
+ "#{start_time} on #{date}"
229
+ end
230
+ end
231
+
232
+ def summarize_period
233
+ period = []
234
+ if one_day?
235
+ period << "all day on #{date}"
236
+ elsif all_day?
237
+ period << "from #{date} to #{end_date.to_datetime.strftime(date_format)}"
238
+ elsif within_day?
239
+ period << "#{start_time}"
240
+ period << "to #{end_time}" if end_time
241
+ period << "on #{date}"
242
+ else
243
+ period << "#{start_time} on #{date}"
244
+ period << "to #{end_time} on #{end_date.to_datetime.strftime(date_format)}"
245
+ end
246
+ period.join(' ')
247
+ end
248
+
249
+ def url
250
+ if url = read_attribute(:url)
251
+ return nil if url.blank?
252
+ url = "http://#{url}" unless url =~ /^(http:\/){0,1}\//
253
+ url.strip
254
+ end
255
+ end
256
+
257
+ def one_day?
258
+ all_day? && within_day?
259
+ end
260
+
261
+ def within_day?
262
+ (!end_date || start_date.to_date.jd == end_date.to_date.jd || end_date == start_date + 1.day)
263
+ end
264
+
265
+ # sometimes we need to filter an existing list to get the day's events
266
+ # usually in a radius tag, to avoid going back to the database for each day in a list
267
+ # so we call events.select {|e| e.on_this_day?(day) }
268
+
269
+ def on_this_day?(date)
270
+ if end_date
271
+ start_date < date.end_of_day && end_date > date.beginning_of_day
272
+ else
273
+ start_date > date.beginning_of_day && start_date < date.end_of_day
274
+ end
275
+ end
276
+
277
+ def in_this_month?(date)
278
+ if end_date
279
+ start_date < date.end_of_month && end_date > date.beginning_of_month
280
+ else
281
+ start_date > date.beginning_of_month && start_date < date.end_of_month
282
+ end
283
+ end
284
+
285
+ def continuing?
286
+ end_date && start_date < Time.now && end_date > Time.now
287
+ end
288
+
289
+ def editable?
290
+ status != Status[:imported]
291
+ end
292
+
293
+ def recurs?
294
+ master || occurrences.any?
295
+ end
296
+
297
+ def status
298
+ Status.find(self.status_id)
299
+ end
300
+
301
+ def status=(value)
302
+ self.status_id = value.id
303
+ end
304
+
305
+ def recurrence
306
+ recurrence_rules.first.to_s
307
+ end
308
+
309
+ def add_recurrence(rule)
310
+ self.recurrence_rules << EventRecurrenceRule.from(rule)
311
+ end
312
+
313
+ def to_ri_cal
314
+ RiCal.Event do |cal_event|
315
+ cal_event.uid = uuid
316
+ cal_event.summary = title
317
+ cal_event.description = description if description
318
+ cal_event.dtstart = (all_day? ? start_date.to_date : start_date) if start_date
319
+ cal_event.dtend = (all_day? ? end_date.to_date : end_date) if end_date
320
+ cal_event.url = url if url
321
+ cal_event.rrules = recurrence_rules.map(&:to_ical) if recurrence_rules.any?
322
+ cal_event.location = location if location
323
+ end
324
+ end
325
+
326
+ def ical
327
+ self.to_ri_cal.to_s
328
+ end
329
+
330
+ def self.create_from(cal_event)
331
+ event = new({
332
+ :uuid => cal_event.uid,
333
+ :title => cal_event.summary,
334
+ :description => cal_event.description,
335
+ :location => cal_event.location,
336
+ :url => cal_event.url,
337
+ :start_date => cal_event.dtstart,
338
+ :end_date => cal_event.dtend,
339
+ :all_day => !cal_event.dtstart.is_a?(DateTime),
340
+ :created_at => cal_event.dtstamp
341
+ })
342
+ event.status = Status[:imported]
343
+ cal_event.rrule.each { |rule| event.add_recurrence(rule) }
344
+ event
345
+ rescue => error
346
+ logger.error "Event import error: #{error}."
347
+ raise
348
+ end
349
+
350
+ def update_from(cal_event)
351
+ self.update_attributes({
352
+ :title => cal_event.summary,
353
+ :description => cal_event.description,
354
+ :location => cal_event.location,
355
+ :url => cal_event.url,
356
+ :start_date => cal_event.dtstart,
357
+ :end_date => cal_event.dtend,
358
+ :all_day => !cal_event.dtstart.is_a?(DateTime)
359
+ })
360
+ self.status = Status[:imported]
361
+ cal_event.rrule.each { |rule| self.add_recurrence(rule) }
362
+ self
363
+ rescue => error
364
+ logger.error "Event update error: #{error}."
365
+ raise
366
+ end
367
+
368
+ protected
369
+
370
+ def set_uuid
371
+ self.uuid = UUIDTools::UUID.timestamp_create.to_s if uuid.blank?
372
+ end
373
+
374
+ def set_default_status
375
+ self.status ||= Status[:Published]
376
+ end
377
+
378
+ # doesn't yet observe exceptions
379
+ def update_occurrences
380
+ occurrences.destroy_all
381
+ if recurrence_rules.any?
382
+ recurrence_horizon = Time.now + (Radiant::Config['event_calendar.recurrence_horizon'] || 10).to_i.years
383
+ to_ri_cal.occurrences(:before => recurrence_horizon).each do |occ|
384
+ occurrences.create!(self.attributes.merge(:start_date => occ.dtstart, :end_date => occ.dtend, :uuid => nil)) unless occ.dtstart == self.start_date
385
+ end
386
+ end
387
+ end
388
+
389
+ def date_format
390
+ Radiant::Config['event_calendar.date_format'] || "%-1d %B"
391
+ end
392
+
393
+ def short_date_format
394
+ Radiant::Config['event_calendar.short_date_format'] || "%-1d/%m/%Y"
395
+ end
396
+
397
+ def time_format
398
+ Radiant::Config['event_calendar.time_format'] || "%-1I:%M%p"
399
+ end
400
+
401
+ def round_time_format
402
+ Radiant::Config['event_calendar.round_time_format'] || "%-1I%p"
403
+ end
404
+
405
+ def dates_are_in_order
406
+
407
+ end
408
+
409
+ end