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,138 @@
1
+ require 'rack/utils'
2
+
3
+ class EventCalendarPage < Page
4
+ include WillPaginate::ViewHelpers
5
+
6
+ attr_accessor :filters, :calendar_parameters, :calendar_filters, :calendar_year, :calendar_month, :calendar_page, :calendar_category, :calendar_slug, :calendar_period
7
+
8
+ description %{ Create a viewer for calendar data. }
9
+
10
+ def self.sphinx_indexes
11
+ []
12
+ end
13
+
14
+ def cache?
15
+ true
16
+ end
17
+
18
+ def find_by_url(url, live = true, clean = false)
19
+ url = clean_url(url) if clean
20
+ my_url = self.url_without_parts
21
+ if url =~ /^#{Regexp.quote(my_url)}(.*)/
22
+ read_parameters($1)
23
+ self
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def read_parameters(path)
30
+ @calendar_parameters = []
31
+ unless path.blank?
32
+ parts = path.split(/\/+/)
33
+ @calendar_page = parts.pop if parts.last =~ /^\d{1,3}$/
34
+ @period = {}
35
+ parts.each do |part|
36
+ if part.match(/^\d\d\d\d$/)
37
+ @calendar_year = part
38
+ elsif part.match(/^\d+$/)
39
+ @calendar_day = part
40
+ elsif Date::MONTHNAMES.include?(part.titlecase)
41
+ @calendar_month = Date::MONTHNAMES.index(part.titlecase)
42
+ elsif Calendar.categories.include?(part)
43
+ @calendar_category = part
44
+ elsif Calendar.slugs.include?(part)
45
+ @calendar_slug = part
46
+ else
47
+ @calendar_parameters.push(part)
48
+ end
49
+ end
50
+
51
+ if @calendar_year && @calendar_month
52
+ start = Date.civil(@calendar_year.to_i, @calendar_month.to_i)
53
+ @calendar_period = CalendarPeriod.between(start, start.to_datetime.end_of_month)
54
+ elsif calendar_year
55
+ start = Date.civil(@calendar_year.to_i)
56
+ @calendar_period = CalendarPeriod.between(start, start.to_datetime.end_of_year)
57
+ end
58
+
59
+ @calendar_parameters
60
+ end
61
+ end
62
+
63
+ def calendar_set
64
+ if calendar_category and calendar_slug
65
+ calendars.in_category(calendar_category).with_slugs(calendar_slug)
66
+ end
67
+ end
68
+
69
+ def url_parts
70
+ {
71
+ :path => url_without_parts,
72
+ :day => @calendar_day,
73
+ :month => @calendar_month,
74
+ :year => @calendar_year,
75
+ :category => @calendar_category,
76
+ :slug => @calendar_slug
77
+ }
78
+ end
79
+
80
+ def url_with_parts(overrides={})
81
+ parts = url_parts.merge(overrides)
82
+ page = parts.delete(:page)
83
+ path = parts.delete(:path)
84
+ parts[:month] = month_names[parts[:month]].downcase unless parts[:month].blank? || parts[:month] =~ /[a-zA-Z]/
85
+ clean_url([path, parts.values, page].select{|p| !p.blank?}.join('/'))
86
+ end
87
+ alias_method_chain :url, :parts
88
+
89
+ def month_names
90
+ @month_names ||= Date::MONTHNAMES.dup
91
+ end
92
+
93
+ def day_names
94
+ unless @day_names
95
+ @day_names = Date::DAYNAMES.dup
96
+ @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.
97
+ end
98
+ @day_names
99
+ end
100
+
101
+
102
+ desc %{
103
+ Renders a trail of breadcrumbs to the current page. On an event calendar page this tag is
104
+ overridden to show the filters applied to calendar data (including category, slug and date rage)
105
+ as well as the path to this page.
106
+
107
+ *Usage:*
108
+
109
+ <pre><code><r:breadcrumbs [separator="separator_string"] [nolinks="true"] /></code></pre>
110
+ }
111
+ tag 'breadcrumbs' do |tag|
112
+ page = tag.locals.page
113
+ nolinks = (tag.attr['nolinks'] == 'true')
114
+
115
+ if calendar_category
116
+ crumbs = nolinks ? [page.breadcrumb] : [%{<a href="#{url}">#{tag.render('breadcrumb')}</a>}]
117
+ if calendar_slug
118
+ crumbs << (nolinks ? calendar_category : %{<a href="#{url}/#{calendar_category}">#{calendar_category}</a>})
119
+ crumbs << calendar_slug
120
+ else
121
+ crumbs << calendar_category
122
+ end
123
+ else
124
+ crumbs = [page.breadcrumb]
125
+ end
126
+ page.ancestors.each do |ancestor|
127
+ tag.locals.page = ancestor
128
+ if nolinks
129
+ crumbs.unshift tag.render('breadcrumb')
130
+ else
131
+ crumbs.unshift %{<a href="#{tag.render('url')}">#{tag.render('breadcrumb')}</a>}
132
+ end
133
+ end
134
+ separator = tag.attr['separator'] || ' &gt; '
135
+ crumbs.join(separator)
136
+ end
137
+
138
+ end
@@ -0,0 +1,89 @@
1
+ class EventOccurrence < ActiveRecord::Base
2
+
3
+ belongs_to :event
4
+ belongs_to :calendar # for efficient retrieval
5
+ belongs_to :event_venue # we can just grab occurrences
6
+
7
+ has_site if respond_to? :has_site
8
+
9
+ delegate :title, :description, :location, :postcode, :url, :keywords, :contact, :all_day, :one_day?, :within_day?, :to => :event
10
+
11
+ before_validation_on_create :set_defaults
12
+
13
+ named_scope :imported, { :conditions => ["status_id = ?", Status[:imported].to_s] }
14
+ named_scope :submitted, { :conditions => ["status_id = ?", Status[:submitted].to_s] }
15
+ named_scope :approved, { :conditions => ["status_id >= (?)", Status[:published].to_s] }
16
+
17
+ named_scope :in_calendars, lambda { |calendars| # list of calendar objects
18
+ ids = calendars.map{ |c| c.id }
19
+ { :conditions => [ ids.map{"calendar_id = ?"}.join(" OR "), *ids] }
20
+ }
21
+
22
+ named_scope :after, lambda { |datetime| # datetime. eg calendar.occurrences.after(Time.now)
23
+ { :conditions => ['start_date > ?', datetime] }
24
+ }
25
+
26
+ named_scope :before, lambda { |datetime| # datetime. eg calendar.occurrences.before(Time.now)
27
+ { :conditions => ['start_date < ?', datetime] }
28
+ }
29
+
30
+ named_scope :between, lambda { |start, finish| # datetimable objects. eg. Event.between(reader.last_login, Time.now)
31
+ { :conditions => ['(start_date < :finish AND end_date > :start) OR (end_date IS NULL AND start_date < :finish AND start_date > :start)', {:start => start, :finish => finish}] }
32
+ }
33
+
34
+ named_scope :within, lambda { |period| # seconds. eg calendar.occurrences.within(6.months)
35
+ start = Time.now
36
+ finish = start + period
37
+ between(start, finish)
38
+ }
39
+
40
+ named_scope :in_the_last, lambda { |period| # seconds. eg calendar.occurrences.in_the_last(1.week)
41
+ finish = Time.now
42
+ start = finish - period
43
+ between(start, finish)
44
+ }
45
+
46
+ named_scope :in_year, lambda { |year| # just a number. eg calendar.occurrences.in_year(2010)
47
+ start = DateTime.civil(year)
48
+ finish = start + 1.year
49
+ between(start, finish)
50
+ }
51
+
52
+ named_scope :in_month, lambda { |year, month| # numbers. eg calendar.occurrences.in_month(2010, 12)
53
+ start = DateTime.civil(year, month)
54
+ finish = start + 1.month
55
+ between(start, finish)
56
+ }
57
+
58
+ named_scope :in_week, lambda { |year, week| # numbers, with a commercial week: eg calendar.occurrences.in_week(2010, 35)
59
+ start = DateTime.commercial(year, week)
60
+ finish = start + 1.week
61
+ between(start, finish)
62
+ }
63
+
64
+ named_scope :on_day, lambda { |year, month, day| # numbers: eg calendar.occurrences.on_day(2010, 12, 12)
65
+ start = DateTime.civil(year, month, day)
66
+ finish = start + 1.day
67
+ between(start, finish)
68
+ }
69
+
70
+ def status
71
+ Status.find(self.status_id)
72
+ end
73
+
74
+ def status=(value)
75
+ self.status_id = value.id
76
+ end
77
+
78
+ protected
79
+
80
+ # the default occurrence just echoes the event
81
+ # recurrences will override at least start_date.
82
+
83
+ def set_defaults
84
+ [:start_date, :end_date, :calendar_id, :event_venue_id, :status_id].each do |col|
85
+ self[col] ||= event.send(col)
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,85 @@
1
+ class EventRecurrenceRule < ActiveRecord::Base
2
+ include ActionView::Helpers::TextHelper
3
+
4
+ belongs_to :created_by, :class_name => 'User'
5
+ belongs_to :updated_by, :class_name => 'User'
6
+ belongs_to :event
7
+ has_site if respond_to? :has_site
8
+
9
+ def active
10
+ new_record? ? false : read_attribute(:active)
11
+ end
12
+
13
+ def basis
14
+ basis = read_attribute(:basis)
15
+ return nil unless basis == 'count' || basis == 'limit'
16
+ basis
17
+ end
18
+
19
+ def unbounded?
20
+ basis.nil?
21
+ end
22
+
23
+ def single?
24
+ true if basis == 'limit' && limiting_count == 1
25
+ end
26
+
27
+ def to_s
28
+ summary = []
29
+ if interval > 1
30
+ unit = period.downcase.sub(/ly$/, '')
31
+ unit = "day" if unit == 'dai'
32
+ summary << "every #{interval} #{unit.pluralize}"
33
+ else
34
+ summary << period.downcase
35
+ end
36
+ if unbounded?
37
+ summary.last << ","
38
+ summary << 'indefinitely'
39
+ elsif basis == 'limit' && limiting_date
40
+ summary << "until #{limiting_date.to_datetime.strftime('%d %B %Y')}" if limiting_date
41
+ elsif basis == 'count' && limiting_count
42
+ summary.last << ","
43
+ summary << "#{limiting_count} times"
44
+ end
45
+ summary.join(' ')
46
+ end
47
+
48
+ def rule=(rule)
49
+ rule = RiCal::PropertyValue::RecurrenceRule.convert(nil, rule) unless rule.is_a? RiCal::PropertyValue::RecurrenceRule
50
+ self.period = rule.freq
51
+ self.interval = rule.interval
52
+ if rule.until
53
+ self.basis = 'limit'
54
+ self.limiting_date = rule.until.value
55
+ elsif rule.count
56
+ self.basis = 'count'
57
+ self.limiting_count = rule.count
58
+ end
59
+ end
60
+
61
+ def rule
62
+ rule = RiCal::PropertyValue::RecurrenceRule.convert(nil, "") unless rule.is_a? RiCal::PropertyValue::RecurrenceRule
63
+ rule.freq = period.upcase
64
+ rule.until = limiting_date if basis == 'limit' && limiting_date
65
+ rule.count = limiting_count if basis == 'count' && limiting_count
66
+ rule.interval = interval
67
+ rule
68
+ end
69
+
70
+ def self.from(rule) # ical string or RiCal::PropertyValue::RecurrenceRule
71
+ rrule = self.new
72
+ rrule.rule = rule
73
+ rrule.active = true
74
+ rrule
75
+ end
76
+
77
+ def to_ical
78
+ rule.to_ical
79
+ end
80
+
81
+ end
82
+
83
+
84
+
85
+
@@ -0,0 +1,17 @@
1
+ class EventVenue < ActiveRecord::Base
2
+ has_many :events, :dependent => :nullify
3
+ has_site if respond_to? :has_site
4
+ validates_presence_of :title, :address
5
+ default_scope :order => 'title asc'
6
+
7
+ def to_s
8
+ %{#{title}, #{address}}
9
+ end
10
+
11
+ def location
12
+ address
13
+ end
14
+ def location=(location)
15
+ address = location
16
+ end
17
+ end
@@ -0,0 +1,132 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'ri_cal'
4
+ require 'date'
5
+ require 'ftools'
6
+
7
+ class Ical < ActiveRecord::Base
8
+ belongs_to :calendar
9
+ validates_presence_of :url
10
+ has_site if respond_to? :has_site
11
+
12
+ @@calendars_path = Radiant::Config["event_calendar.icals_path"]
13
+
14
+ def refresh
15
+ retrieve_file
16
+ parse_file
17
+ logger.info self.calendar.category + "/" + self.calendar.name + " - iCalendar subscription refreshed on " + Time.now.strftime("%m/%d at %H:%M")
18
+ end
19
+
20
+ def retrieve_file
21
+ File.makedirs filepath
22
+ begin
23
+ uri = URI.parse(url)
24
+ if authenticated?
25
+ http = Net::HTTP.new(uri.host, uri.port)
26
+ http.use_ssl = uri.scheme == 'https'
27
+ req = Net::HTTP::Get.new(uri.path)
28
+ req.basic_auth username, password
29
+ response = http.request(req)
30
+ else
31
+ response = Net::HTTP.get_response(uri)
32
+ end
33
+ File.open(filename, "w") do |file|
34
+ file << response.body
35
+ end
36
+ rescue => error
37
+ logger.error "iCal url or file error with: #{self.calendar.name} - #{url} -> (#{filename}): #{error}."
38
+ raise
39
+ end
40
+ end
41
+
42
+ def parse_file(thisfile=filename)
43
+ begin
44
+ Ical.transaction do
45
+ self.last_refresh_count = 0
46
+ event_count = 0
47
+ File.open(thisfile, "r") do |file|
48
+ components = RiCal.parse(file)
49
+ cal = components.first
50
+ cal.events.each do |cal_event|
51
+ if event = Event.find_by_uuid(cal_event.uid)
52
+ if cal_event.dtstamp > event.updated_at
53
+ event.update_from(cal_event)
54
+ else
55
+ end
56
+ event_count += 1
57
+ else
58
+ event = Event.create_from(cal_event)
59
+ event.site = self.calendar.site if event.respond_to? :site=
60
+ self.calendar.events << event
61
+ event.save!
62
+ end
63
+ end
64
+ end
65
+ self.last_refresh_count = event_count
66
+ self.last_refresh_date = Time.now.utc
67
+ self.save!
68
+ end
69
+ rescue => error
70
+ logger.error "RiCal parse error with: #{self.calendar.name}: #{error}."
71
+ raise
72
+ end
73
+ end
74
+
75
+ def filepath
76
+ File.join RAILS_ROOT, "public", ics_path
77
+ end
78
+
79
+ def filename
80
+ File.join filepath, ics_file
81
+ end
82
+
83
+ def ics_path
84
+ File.join @@calendars_path, self.calendar.category
85
+ end
86
+
87
+ def ics_file
88
+ "#{self.calendar.slug}.ics"
89
+ end
90
+
91
+ def to_ics
92
+ File.join "", ics_path, ics_file
93
+ end
94
+
95
+ # I've changed this to make the ical refresh decision a simple yes or no
96
+ # and to make the refresh interval a global value
97
+ def refresh_interval
98
+ (Radiant::Config['event_calendar.refresh_interval'] || 3600).to_i.seconds
99
+ end
100
+
101
+ def refresh_automatically?
102
+ refresh_interval.nil? || refresh_interval.to_i != 0
103
+ end
104
+
105
+ def needs_refreshment?
106
+ last_refresh_date.nil? || Time.now > last_refresh_date + refresh_interval
107
+ end
108
+
109
+ def self.check_refreshments
110
+ find(:all).each do |i|
111
+ i.refresh if i.refresh_automatically? && i.needs_refreshment?
112
+ end
113
+ end
114
+
115
+ def self.refresh_all
116
+ find(:all).each do |i|
117
+ i.refresh
118
+ end
119
+ return true
120
+ end
121
+
122
+ protected
123
+
124
+ def authenticated?
125
+ !username.blank?
126
+ end
127
+
128
+ def secured?
129
+ url.match(/^https/)
130
+ end
131
+
132
+ end