From 49b8fa1007afa17e31a99be9791977f3ef220ffb Mon Sep 17 00:00:00 2001 From: bsag Date: Sun, 16 Jan 2005 15:24:34 +0000 Subject: [PATCH] * Started to add some of the framework for iCal integration (not working yet) * Added validation for next actions (on description and notes field) git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@7 a4c988fc-2ded-0310-b66e-134b36920a42 --- tracks/app/controllers/application.rb | 1 + tracks/app/controllers/todo_controller.rb | 22 +- tracks/app/models/todo.rb | 9 + tracks/app/views/todo/list.rhtml | 10 + tracks/lib/iCal.rb | 289 ++++++++++++++++++++++ tracks/public/stylesheets/standard.css | 2 +- 6 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 tracks/lib/iCal.rb diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 141e688a..cde82533 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -3,6 +3,7 @@ require_dependency "login_system" require_dependency "redcloth" +require_dependency "iCal" require 'date' $delete_img = "" diff --git a/tracks/app/controllers/todo_controller.rb b/tracks/app/controllers/todo_controller.rb index b0e030cc..49f80558 100644 --- a/tracks/app/controllers/todo_controller.rb +++ b/tracks/app/controllers/todo_controller.rb @@ -32,24 +32,34 @@ class TodoController < ApplicationController # Parameters from form fields should be passed to create new item # def add_item - item = Todo.new - item.attributes = @params["new_item"] + @item = Todo.new + @item.attributes = @params["new_item"] # Convert the date format entered (as set in config/settings.yml) # to the mysql format YYYY-MM-DD if @params["new_item"]["due"] != "" date_fmt = app_configurations["formats"]["date"] formatted_date = DateTime.strptime(@params["new_item"]["due"], "#{date_fmt}") - item.due = formatted_date.strftime("%Y-%m-%d") + @item.due = formatted_date.strftime("%Y-%m-%d") else - item.due = "0000-00-00" + @item.due = "0000-00-00" end - if item.save + # This doesn't seem to be working. No error, but the error + # message isn't printed either. + unless @item.errors.empty? + error_msg = '' + @item.errors.each_full do |message| + error_msg << message + end + return error_msg + end + + if @item.save flash["confirmation"] = "Next action was successfully added" redirect_to( :action => "list" ) else - flash["warning"] = "Couldn't add the action" + flash["warning"] = "Couldn't add the action because of an error: #{error_msg}" redirect_to( :action => "list" ) end end diff --git a/tracks/app/models/todo.rb b/tracks/app/models/todo.rb index 89a965f0..831413f9 100644 --- a/tracks/app/models/todo.rb +++ b/tracks/app/models/todo.rb @@ -3,6 +3,14 @@ class Todo < ActiveRecord::Base belongs_to :context belongs_to :project + # Description field can't be empty, and must be < 100 bytes + # Notes must be < 60,000 bytes (65,000 actually, but I'm being cautious) + validates_presence_of :description, :message => "no description provided" + validates_length_of :description, :maximum => 100, :message => "description is too long" + validates_lenght_of :notes, :maximum => 60000, :message => "notes are too long" + #validates_format_of :due, :with => /^[\d]{2,2}\/[\d]{2,2}\/[\d]{4,4}$/, :message => "date format incorrect" + + def before_save # Add a creation date (Ruby object format) to item before it's saved # if there is no existing creation date (this prevents creation date @@ -13,4 +21,5 @@ class Todo < ActiveRecord::Base end end + end diff --git a/tracks/app/views/todo/list.rhtml b/tracks/app/views/todo/list.rhtml index 2c51a32f..c31789ec 100644 --- a/tracks/app/views/todo/list.rhtml +++ b/tracks/app/views/todo/list.rhtml @@ -55,6 +55,15 @@
+

Today's events are:

+ + +

Add next action


@@ -83,6 +92,7 @@
+
<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> diff --git a/tracks/lib/iCal.rb b/tracks/lib/iCal.rb new file mode 100644 index 00000000..00645ac2 --- /dev/null +++ b/tracks/lib/iCal.rb @@ -0,0 +1,289 @@ + +require 'date' +require 'ParseDate' + +module ICal + +SECONDS_PER_DAY = 24 * 60 * 60 + +class ICalReader + @@iCalFolder = "/Users/jchappell/Library/Calendars" + + def initialize(calendarName = nil) + @events = [] + @iCalFiles = Dir[@@iCalFolder + "/*.ics"] + @iCalFiles.sort! {|x, y| x.downcase <=> y.downcase} + if calendarName then + fullName = @@iCalFolder + "/" + calendarName + ".ics" + @iCalFiles = @iCalFiles.select {|name| name == fullName} + end + end + + def calendars + @iCalFiles.collect {|name| File.basename(name, ".ics")} + end + + def readEvents + @events = [] + @iCalFiles.each do |fileName| + lines = File.readlines(fileName); + inEvent = false + eventLines = [] + lines.each do |line| + if line =~ /^BEGIN:VEVENT/ then + inEvent = true + eventLines = [] + end + + if inEvent + eventLines << line + if line =~ /^END:VEVENT/ then + inEvent = false + @events << parseEvent(eventLines) + end + end + end + end + @events + end + + def parseEvent(lines) + event = ICalEvent.new() + startDate = nil + rule = nil + + lines.each do |line| + if line =~ /^SUMMARY:(.*)/ then + event.summary = $1 + elsif line =~ /^DTSTART;.*:(.*).*/ then + startDate = parseDate($1) + elsif line =~ /^EXDATE.*:(.*)/ then + event.addExceptionDate(parseDate($1)) + elsif line =~ /^RRULE:(.*)/ then + rule = $1 + end + end + + event.startDate = startDate + event.addRecurrenceRule(rule) + event + end + + def parseDate(dateStr) + # We constrain the year to 1970 because Time won't handle lesser years + # If it is less than 1970 then its probably a birthday or something + # in which case, we don't really care about the year + year = dateStr[0,4].to_i + year = 1970 if year < 1970 + + month = dateStr[4,2].to_i + day = dateStr[6,2].to_i + hour = dateStr[9,2].to_i + minute = dateStr[11,2].to_i + Time.local(year, month, day, hour, minute) + end + + def events + readEvents if @events == [] + @events + end + + def selectEvents(&predicate) + # + # The start date of each event could be different due to recurring events + # Since we can assume the date is the same for all events, we just compare + # the times when sorting + # + now = Time.now + events.select(&predicate).sort do |event1, event2| + time1 = Time.local(now.year, now.month, now.day, event1.startDate.hour, + event1.startDate.min) + time2 = Time.local(now.year, now.month, now.day, event2.startDate.hour, + event2.startDate.min) + time1 <=> time2 + end + end + + def todaysEvents + #events.select {|event| event.startsToday? } + selectEvents {|event| event.startsToday? } + end + + def tomorrowsEvents + #events.select {|event| event.startsTomorrow? } + selectEvents {|event| event.startsTomorrow?} + end + + def eventsFor(date) + #events.select {|event| event.startsOn?(date)} + selectEvents {|event| event.startsOn?(date)} + end +end + +class DateParser + # Given a date as a string, returns a Time object + def DateParser.parse(dateStr) + dateValues = ParseDate::parsedate(dateStr) + Time.local(*dateValues[0, 3]) + end + + def DateParser.format(date) + date.strftime("%m/%d/%Y") + end +end + +class ICalEvent + + def initialize + @exceptionDates = [] + end + + def <=>(otherEvent) + return @startDate <=> otherEvent.startDate + end + + def addExceptionDate(date) + @exceptionDates << date + end + + def addRecurrenceRule(rule) + @dateSet = DateSet.new(@startDate, rule) + end + + def startsToday? + startsOn?(Time.now) + end + + def startsTomorrow? + tomorrow = Time.now + SECONDS_PER_DAY; + startsOn?(tomorrow) + end + + def startsOn?(date) + (startDate.year == date.year and startDate.month == date.month and + startDate.day == date.day) or @dateSet.includes?(date) + end + + def to_s + "#{@startDate.strftime("%m/%d/%Y (%I:%M %p)")} - #{@summary}" + end + + def startTime + @startDate + end + + attr_accessor :startDate, :summary +end + +class DateSet + + def initialize(startDate, rule) + @startDate = startDate + @frequency = nil + @count = nil + @untilDate = nil + @byMonth = nil + @byDay = nil + parseRecurrenceRule(rule) + end + + def parseRecurrenceRule(rule) + + if rule =~ /FREQ=(.*?);/ then + @frequency = $1 + end + + if rule =~ /COUNT=(\d*)/ then + @count = $1.to_i + end + + if rule =~ /UNTIL=(.*?);/ then + @untilDate = DateParser.parse($1) + #puts @untilDate + end + + if rule =~ /INTERVAL=(\d*)/ then + @interval = $1.to_i + end + + if rule =~ /BYMONTH=(.*?);/ then + @byMonth = $1 + end + + if rule =~ /BYDAY=(.*?);/ then + @byDay = $1 + #puts "byDay = #{@byDay}" + end + end + + def to_s + puts "#" + end + + def includes?(date) + return true if date == @startDate + return false if @untilDate and date > @untilDate + + case @frequency + when 'DAILY' + #if @untilDate then + # return (@startDate..@untilDate).include?(date) + #end + increment = @interval ? @interval : 1 + d = @startDate + counter = 0 + until d > date + + if @count then + counter += 1 + if counter >= @count + return false + end + end + + d += (increment * SECONDS_PER_DAY) + if d.day == date.day and + d.year == date.year and + d.month == date.month then + return true + end + + end + + when 'WEEKLY' + return true if @startDate.wday == date.wday + + when 'MONTHLY' + + when 'YEARLY' + end + + false + end + + attr_reader :frequency + attr_accessor :startDate +end + +if $0 == __FILE__ then + #reader = ICalReader.new("Test") + reader = ICalReader.new + + puts + puts "Today" + puts "=====" + puts reader.todaysEvents + + puts + puts "Tomorrow" + puts "========" + puts reader.tomorrowsEvents + + puts + puts "08/14/2003 Events" + puts "=================" + puts reader.eventsFor(Time.local(2003, 8, 14)) + puts +end + +end diff --git a/tracks/public/stylesheets/standard.css b/tracks/public/stylesheets/standard.css index 93fc3974..b272e063 100644 --- a/tracks/public/stylesheets/standard.css +++ b/tracks/public/stylesheets/standard.css @@ -185,7 +185,7 @@ h2 a:hover { border: 1px solid #ED2E38; background-color: #F6979C; padding: 5px; - color: #007E00; + color: #ED2E38; text-align: center; }