Skip to content

Instantly share code, notes, and snippets.

@jamesu
Created March 5, 2009 11:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jamesu/74325 to your computer and use it in GitHub Desktop.
Save jamesu/74325 to your computer and use it in GitHub Desktop.
# Some people have been emailing me complaining that my calendar_helper code
# (http://gist.github.com/54116) doesn't work properly.
#
# So for all of you who can't perform a little bit of problem solving to get the code working correctly in your app, here is the code as used in a fork of RuckSack.
# Enjoy.
#==
# Copyright (C) 2009 James S Urquhart
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#++
# calendars_controller.rb
class CalendarsController < ApplicationController
# GET /calendars
# GET /calendars.xml
def index
@calendars = Calendar.find(:all)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @calendars }
end
end
# GET /calendars/1
# GET /calendars/1.xml
def show
@calendar = Calendar.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @calendar }
end
end
# GET /calendars/new
# GET /calendars/new.xml
def new
@calendar = Calendar.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @calendar }
end
end
# GET /calendars/1/edit
def edit
@calendar = Calendar.find(params[:id])
end
# POST /calendars
# POST /calendars.xml
def create
@calendar = Calendar.new(params[:calendar])
@calendar.created_by = @logged_user
@calendar.color ||= "\#%06x" % (rand() * 0xFFFFFF).to_i
respond_to do |format|
if @calendar.save
flash[:notice] = 'Calendar was successfully created.'
format.html { redirect_to(@calendar) }
format.js { @calendars = Calendar.find(:all) }
format.xml { render :xml => @calendar, :status => :created, :location => @calendar }
else
format.html { render :action => "new" }
format.js
format.xml { render :xml => @calendar.errors, :status => :unprocessable_entity }
end
end
end
# PUT /calendars/1
# PUT /calendars/1.xml
def update
@calendar = Calendar.find(params[:id])
@calendar.updated_by = @logged_user
respond_to do |format|
if @calendar.update_attributes(params[:calendar])
flash[:notice] = 'Calendar was successfully updated.'
format.html { redirect_to(@calendar) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @calendar.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /calendars/1
# DELETE /calendars/1.xml
def destroy
@calendar = Calendar.find(params[:id])
@calendar.updated_by = @logged_user
@calendar.destroy
respond_to do |format|
format.html { redirect_to(calendars_url) }
format.xml { head :ok }
end
end
end
# events_controller.rb
class EventsController < ApplicationController
layout 'pages'
def calc_cbtw(first, second)
if first > second
second + (7 - first)
else
second - first
end
end
def calc_bow(date, start = 1)
days_to_beg = calc_cbtw(start, date.wday)
date - days_to_beg
end
# GET /events
# GET /events.xml
def index
@month = params[:month].nil? ? Time.now.month : params[:month].to_i
@year = params[:year].nil? ? Time.now.year : params[:year].to_i
@content_for_sidebar = 'event_sidebar'
# Start of month, end of month
@now_date = Date.civil(@year, @month)
@prev_date = @now_date - 1.month
@prev_date = Date.civil(@prev_date.year, @prev_date.month, -1)
@next_date = @now_date + 1.month
@next_date = Date.civil(@next_date.year, @next_date.month)
# Note: works up till 6
@first_day_of_week = 1
# offset by weekdays
@strip_start = calc_bow(@now_date, @first_day_of_week)
@next_date = calc_bow(@next_date + 7, @first_day_of_week)-1
@event_strips = [[nil] * (@next_date - @strip_start + 1)]
@calendars = Calendar.find(:all)
if params[:calendar].nil?
@calendar_ids = @calendars.collect {|c| c.id}
else
@calendar_ids = (params[:calendar] || "").split(',').collect {|id| id.to_i}
end
@events = Event.find(:all,
:include => :calendar,
:conditions => ['calendar_id IN (?) AND ((start_date >= ? AND start_date < ?) OR
(end_date NOT NULL AND
(end_date > ? AND start_date < ?)
))',
@calendar_ids,
@strip_start, @next_date,
@strip_start, @next_date+1], :order => 'start_date ASC').collect do |evt|
cur_date = evt.start_date.to_date
end_date = evt.to_date
cur_date, end_date = evt.clip_range(@strip_start, @next_date)
range = ((cur_date - @strip_start).to_i)...((end_date - @strip_start).to_i)
# Find strip
found_strip = nil
for strip in @event_strips
is_in = true
range.each do |r|
if !strip[r].nil?
is_in = false
break
end
end
if is_in
found_strip = strip
break
end
end
# Make strip or add to found strip
if !found_strip.nil?
range.each {|r| found_strip[r] = evt}
else
found_strip = [nil] * (@next_date - @strip_start + 1)
range.each {|r| found_strip[r] = evt}
@event_strips << found_strip
end
evt
end
#for strip in @event_strips
# puts strip.collect {|s| s == nil ? 'nil' : s.title}.join(',')
#end
respond_to do |format|
format.html # index.html.erb
format.js { render :action => 'update' }
format.xml { render :xml => @events }
end
end
# GET /events/1
# GET /events/1.xml
def show
@event = Event.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.js
format.xml { render :xml => @event }
end
end
# GET /events/new
# GET /events/new.xml
def new
@event = Event.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @event }
end
end
# GET /events/1/edit
def edit
@event = Event.find(params[:id])
respond_to do |format|
format.js
format.xml { render :xml => @event }
end
end
# POST /events
# POST /events.xml
def create
@event = Event.new(params[:event])
respond_to do |format|
if @event.save
flash[:notice] = 'Event was successfully created.'
format.html { redirect_to(events_url) }
format.js { render :text => '' }
format.xml { render :xml => @event, :status => :created, :location => @event }
else
format.html { render :action => "new" }
format.xml { render :xml => @event.errors, :status => :unprocessable_entity }
end
end
end
# PUT /events/1
# PUT /events/1.xml
def update
@event = Event.find(params[:id])
respond_to do |format|
if @event.update_attributes(params[:event])
flash[:notice] = 'Event was successfully updated.'
format.html { redirect_to(@event) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @event.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /events/1
# DELETE /events/1.xml
def destroy
@event = Event.find(params[:id])
@event.destroy
respond_to do |format|
format.html { redirect_to(events_url) }
format.xml { head :ok }
end
end
# PUT /events/1/move
def move
@event = Event.find(params[:id])
new_data = false
event_attribs = params[:event]
unless event_attribs.nil?
new_data = @event.move(params[:event]) and @event.save
end
respond_to do |format|
format.html { redirect_to(events_url) }
format.js {
if new_data
render(:text => 'RebindCalendar(null);')
else
render(:text => '')
end
}
format.xml { head :ok }
end
end
def grab_events
end
end
# calendars_helper.rb
module CalendarsHelper
def events_calendar_opts
{ :year => @now_date.year,
:month => @now_date.month,
:style => "red",
:first_day_of_week => @first_day_of_week,
:event_strips => @event_strips,
:start => @strip_start,
:event_width => 81, # total width per day (including margins)
:event_height => 16, # height
:event_margin => 1, # height margin
:previous_month_text => link_to('Previous', {:month => @prev_date.month, :year => @prev_date.year}, :id => 'calPrev'),
:next_month_text => link_to('Next', {:month => @next_date.month, :year => @next_date.year}, :id => 'calNext') }
end
def events_calendar
calendar events_calendar_opts do |event, days, cur_offs, idx|
"<div evtid=\"#{event.id}\" day=\"#{event.start_date.day}\" class=\"event\" style=\"background: #{event.color}; width: #{(81*days)-1}px; top: #{idx*18}px; left:#{cur_offs}px; \"><div>#{h(event.title)}</div></div>"
end
end
# Returns an HTML calendar. In its simplest form, this method generates a plain
# calendar (which can then be customized using CSS).
# However, this may be customized in a variety of ways -- changing the default CSS
# classes, generating the individual day entries yourself, and so on.
#
# The following are optional, available for customizing the default behaviour:
# :month => Time.now.month # The month to show the calendar for. Defaults to current month.
# :year => Time.now.year # The year to show the calendar for. Defaults to current year.
# :table_class => "calendar" # The class for the <table> tag.
# :month_name_class => "monthName" # The class for the name of the month, at the top of the table.
# :other_month_class => "otherMonth" # Not implemented yet.
# :day_name_class => "dayName" # The class is for the names of the weekdays, at the top.
# :day_class => "day" # The class for the individual day number cells.
# This may or may not be used if you specify a block (see below).
# :abbrev => (0..2) # This option specifies how the day names should be abbreviated.
# Use (0..2) for the first three letters, (0..0) for the first, and
# (0..-1) for the entire name.
# :first_day_of_week => 0 # Renders calendar starting on Sunday. Use 1 for Monday, and so on.
# :accessible => true # Turns on accessibility mode. This suffixes dates within the
# # calendar that are outside the range defined in the <caption> with
# # <span class="hidden"> MonthName</span>
# # Defaults to false.
# # You'll need to define an appropriate style in order to make this disappear.
# # Choose your own method of hiding content appropriately.
#
# :show_today => false # Highlights today on the calendar using the CSS class 'today'.
# # Defaults to true.
# :month_name_text => nil # Displayed center in header row. Defaults to current month name from Date::MONTHNAMES hash.
# :previous_month_text => nil # Displayed left of the month name if set
# :next_month_text => nil # Displayed right of the month name if set
#
# For more customization, you can pass a code block to this method, that will get one argument, a Date object,
# and return a values for the individual table cells. The block can return an array, [cell_text, cell_attrs],
# cell_text being the text that is displayed and cell_attrs a hash containing the attributes for the <td> tag
# (this can be used to change the <td>'s class for customization with CSS).
# This block can also return the cell_text only, in which case the <td>'s class defaults to the value given in
# +:day_class+. If the block returns nil, the default options are used.
#
# Example usage:
# calendar # This generates the simplest possible calendar with the curent month and year.
# calendar({:year => 2005, :month => 6}) # This generates a calendar for June 2005.
# calendar({:table_class => "calendar_helper"}) # This generates a calendar, as
# # before, but the <table>'s class
# # is set to "calendar_helper".
# calendar(:abbrev => (0..-1)) # This generates a simple calendar but shows the
# # entire day name ("Sunday", "Monday", etc.) instead
# # of only the first three letters.
# calendar do |d| # This generates a simple calendar, but gives special days
# if listOfSpecialDays.include?(d) # (days that are in the array listOfSpecialDays) one CSS class,
# [d.mday, {:class => "specialDay"}] # "specialDay", and gives the rest of the days another CSS class,
# else # "normalDay". You can also use this highlight today differently
# [d.mday, {:class => "normalDay"}] # from the rest of the days, etc.
# end
# end
#
# An additional 'weekend' class is applied to weekend days.
#
# For consistency with the themes provided in the calendar_styles generator, use "specialDay" as the CSS class for marked days.
#
def calendar(options = {}, &block)
block ||= Proc.new {|d| nil}
defaults = {
:year => Time.now.year,
:month => Time.now.month,
:table_class => 'calendar',
:month_name_class => 'monthName',
:other_month_class => 'otherMonth',
:day_name_class => 'dayName',
:day_class => 'day',
:abbrev => (0..2),
:first_day_of_week => 0,
:accessible => false,
:show_today => true,
:previous_month_text => nil,
:next_month_text => nil,
:start => nil,
:event_strips => nil,
:event_width => 81,
:event_height => 24,
:event_margin => 2
}
options = defaults.merge options
options[:month_name_text] ||= Date::MONTHNAMES[options[:month]]
first = Date.civil(options[:year], options[:month], 1)
last = Date.civil(options[:year], options[:month], -1)
start = options[:start]
event_strips = options[:event_strips]
event_width = options[:event_width]
event_height = options[:event_height]
event_margin = options[:event_margin]
first_weekday = first_day_of_week(options[:first_day_of_week])
last_weekday = last_day_of_week(options[:first_day_of_week])
day_names = Date::DAYNAMES.dup
first_weekday.times do
day_names.push(day_names.shift)
end
# TODO Use some kind of builder instead of straight HTML
cal = %(<table class="#{options[:table_class]}" border="0" cellspacing="0" cellpadding="0">)
cal << %(<thead><tr>)
if options[:previous_month_text] or options[:next_month_text]
cal << %(<th colspan="2">#{options[:previous_month_text]}</th>)
colspan=3
else
colspan=7
end
cal << %(<th colspan="#{colspan}" class="#{options[:month_name_class]}">#{options[:month_name_text]}</th>)
cal << %(<th colspan="2">#{options[:next_month_text]}</th>) if options[:next_month_text]
cal << %(</tr><tr class="#{options[:day_name_class]}">)
day_names.each do |d|
unless d[options[:abbrev]].eql? d
cal << "<th scope='col'><abbr title='#{d}'>#{d[options[:abbrev]]}</abbr></th>"
else
cal << "<th scope='col'>#{d[options[:abbrev]]}</th>"
end
end
cal << "</tr></thead><tbody><tr>"
beginning_of_week(first, first_weekday).upto(first - 1) do |d|
cal << %(<td class="#{options[:other_month_class]})
cal << " weekendDay" if weekend?(d)
if options[:accessible]
cal << %(">#{d.day}<span class="hidden"> #{Date::MONTHNAMES[d.month]}</span></td>)
else
cal << %(">#{d.day}</td>)
end
end unless first.wday == first_weekday
start_row = beginning_of_week(first, first_weekday)
last_row = start_row
first.upto(last) do |cur|
cell_text, cell_attrs = nil#block.call(cur)
cell_text ||= cur.mday
cell_attrs ||= {:class => options[:day_class]}
cell_attrs[:class] += " weekendDay" if [0, 6].include?(cur.wday)
cell_attrs[:class] += " today" if (cur == Date.today) and options[:show_today]
cell_attrs = cell_attrs.map {|k, v| %(#{k}="#{v}") }.join(" ")
cal << "<td #{cell_attrs}>#{cell_text}</td>"
if cur.wday == last_weekday
content = calendar_row(event_strips,
event_width,
event_height,
start_row,
last_row..cur,
&block)
cal << "</tr>#{event_row(content, event_height, event_margin)}<tr>"
last_row = cur + 1
end
end
(last + 1).upto(beginning_of_week(last + 7, first_weekday) - 1) do |d|
cal << %(<td class="#{options[:other_month_class]})
cal << " weekendDay" if weekend?(d)
if options[:accessible]
cal << %(">#{d.day}<span class='hidden'> #{Date::MONTHNAMES[d.mon]}</span></td>)
else
cal << %(">#{d.day}</td>)
end
end unless last.wday == last_weekday
content = calendar_row(event_strips,
event_width,
event_height,
start_row,
last_row..(beginning_of_week(last + 7, first_weekday) - 1),
&block)
cal << "</tr>#{event_row(content, event_height, event_margin)}</tbody></table>"
end
private
def calendar_row(event_strips, event_width, event_height, start, date_range, &block)
start_date = date_range.first
range = ((date_range.first - start).to_i)...((date_range.last - start + 1).to_i)
idx = -1
#print "ROW: #{date_range} [#{start}] == #{range}\n"
last_offs = 0
event_strips.collect do |strip|
idx += 1
range.collect do |r|
event = strip[r]
if !event.nil?
# Clip event dates (if it extends before or beyond the row)
dates = event.clip_range(start_date, date_range.last)
if dates[0] - start_date == r-range.first
# Event somewhere on this row
cur_offs = (event_width*(r-range.first))
start_d = event.start_date.to_date
end_d = event.end_date.nil? ? start_d+1 : event.end_date.to_date+1
block.call(event, dates[1]-dates[0], cur_offs, idx)
else
nil
end
else
nil
end
end.compact
end
end
def event_row(content, height, margin)
"<tr><td colspan=\"7\"><div class=\"events\" style=\"height:#{(height+margin)*content.length}px\">#{content.join}</div><div class=\"clear\"></div></td></tr>"
end
def first_day_of_week(day)
day
end
def last_day_of_week(day)
if day > 0
day - 1
else
6
end
end
def days_between(first, second)
if first > second
second + (7 - first)
else
second - first
end
end
def beginning_of_week(date, start = 1)
days_to_beg = days_between(start, date.wday)
date - days_to_beg
end
def weekend?(date)
[0, 6].include?(date.wday)
end
end
# event.rb
class Event < ActiveRecord::Base
belongs_to :calendar
belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id'
belongs_to :updated_by, :class_name => 'User', :foreign_key => 'updated_by_id'
@@repeat_lookup = {:never => 0, :yearly => 1, :monthly => 2, :fortnightly => 3, :weekly => 4, :daily => 5}
@@repeat_id_lookup = @@repeat_lookup.invert
after_create :process_create
before_update :process_update_params
before_destroy :process_destroy
def process_create
ApplicationLog.new_log(self, self.created_by, :add)
end
def process_update_params
ApplicationLog.new_log(self, self.updated_by, :edit)
end
def process_destroy
ApplicationLog.new_log(self, self.updated_by, :delete)
end
def page
nil
end
def year
date.year
end
def month
date.month
end
def day
date.day
end
def to_date
(end_date || start_date).to_date + 1
end
def color
calendar.color || '#FF0000'
end
def days
to_date.to_date - start_date.to_date
end
def clip_range(start_d, end_d)
# Clip start date
if (start_date < start_d and to_date > start_d)
clipped_start = start_d
else
clipped_start = start_date.to_date
end
# Clip end date
if (to_date >= end_d)
clipped_end = end_d + 1
else
clipped_end = to_date
end
[clipped_start, clipped_end]
end
def move(options)
new_date = self.start_date
# Calculate new date
nyear = options.has_key?(:year) ? options[:year].to_i : new_date.year
nmonth = options.has_key?(:month) ? options[:month].to_i : new_date.month
nday = options.has_key?(:day) ? options[:day].to_i : new_date.day
nhour = options.has_key?(:hour) ? options[:hour].to_i : new_date.hour
new_date = Time.zone.local(nyear, nmonth, nday, nhour, new_date.min, new_date.sec)
delta = new_date - start_date
if delta == 0
return false
end
new_end = self.end_date
unless new_end.nil?
# Need to increment end date by delta
delta = new_date - self.start_date
self.start_date = new_date
self.end_date = new_end + delta
else
# Just set the new date
self.start_date = new_date
end
return true
end
def object_name
self.content
end
def friendly_at_time
@cached_friendly_time || self.at_time.to_s
end
def friendly_at_time=(value)
@cached_friendly_time = value
times = Chronic.parse(value, :now => Time.zone.now, :guess => false)
# TODO: possible to extract subject from query?
start_time = nil
end_time = nil
if !times.nil?
if times.class == Chronic::Span
start_time = times.begin
end_time = times.end-1
else
start_time = times
end
# re-interpret
start_time = Time.zone.local(start_time.year, start_time.mon, start_time.day, start_time.hour, start_time.min, start_time.sec)
unless end_time.nil?
end_time = Time.zone.local(end_time.year, end_time.mon, end_time.day, end_time.hour, end_time.min, end_time.sec)
end
else
# Default to now + 3 hours
start_time = (Time.zone.now + (60*60*3))
end
self.start_date = start_time
self.end_date = end_time
self.title = value
end
def friendly_repeat
"reminder_repeat_#{self.repeat}".to_sym.l
end
def repeat
@@repeat_id_lookup[self.repeat_id]
end
def repeat=(val)
self.repeat_id = @@repeat_lookup[val.to_sym]
end
def done?
expires_at <= Time.zone.now
end
def repeatable?
self.repeat_id > 0
end
def expires_at
self.end_date || self.start_date
end
# Common permissions
def self.can_be_created_by(user)
user.member_of_owner?
end
def can_be_edited_by(user)
return (user.is_admin or user.id == self.created_by_id)
end
def can_be_deleted_by(user)
return (user.is_admin or user.id == self.created_by_id)
end
def can_be_seen_by(user)
return (user.is_admin or self.created_by_id == user.id)
end
def self.select_repeat
@@repeat_lookup.keys.map do |key|
["reminder_repeat_#{key}".to_sym.l, key]
end
end
# Accesibility
attr_accessible :repeat, :friendly_at_time, :title, :description, :at_time, :calendar_id
# Validation
validates_presence_of :title
end
# calendar.rb
class Calendar < ActiveRecord::Base
belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id'
belongs_to :updated_by, :class_name => 'User', :foreign_key => 'updated_by_id'
has_many :application_logs, :as => :rel_object#, :dependent => :nullify
has_many :events
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment