Merge branch 'master' of git://github.com/bsag/tracks

This commit is contained in:
Reinier Balt 2008-10-07 09:43:12 +02:00
commit 122426ab40
74 changed files with 1431 additions and 251 deletions

View file

@ -16,6 +16,8 @@ require 'time'
#
# Tag
class CannotAccessContext < RuntimeError; end
class ApplicationController < ActionController::Base
protect_from_forgery :secret => SALT
@ -173,7 +175,7 @@ class ApplicationController < ActionController::Base
if show_from_date.nil?
todo.show_from=nil
else
todo.show_from = show_from_date.to_time < Time.now.utc ? nil : show_from_date
todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date
end
saved = todo.save

View file

@ -14,36 +14,10 @@ class BackendController < ApplicationController
def new_rich_todo(username, token, default_context_id, description, notes)
check_token(username,token)
description,context = split_by_char('@',description)
description,project = split_by_char('>',description)
if(!context.nil? && project.nil?)
context,project = split_by_char('>',context)
end
# logger.info("context='#{context}' project='#{project}")
context_id = default_context_id
unless(context.nil?)
found_context = @user.active_contexts.find_by_namepart(context)
found_context = @user.contexts.find_by_namepart(context) if found_context.nil?
context_id = found_context.id unless found_context.nil?
end
check_context_belongs_to_user(context_id)
project_id = nil
unless(project.blank?)
if(project[0..3].downcase == "new:")
found_project = @user.projects.build
found_project.name = project[4..255+4].strip
found_project.save!
else
found_project = @user.active_projects.find_by_namepart(project)
found_project = @user.projects.find_by_namepart(project) if found_project.nil?
end
project_id = found_project.id unless found_project.nil?
end
todo = create_todo(description, context_id, project_id, notes)
todo.id
item = Todo.from_rich_message(@user, default_context_id, description, notes)
item.save
raise item.errors.full_messages.to_s if item.new_record?
item.id
end
def list_contexts(username, token)
@ -84,25 +58,6 @@ class BackendController < ApplicationController
raise item.errors.full_messages.to_s if item.new_record?
item
end
def split_by_char(separator,string)
parts = string.split(separator)
# if the separator is used more than once, concat the last parts this is
# needed to get 'description @ @home > project' working for contexts
# starting with @
if parts.length > 2
2.upto(parts.length-1) { |i| parts[1] += (separator +parts[i]) }
end
return safe_strip(parts[0]), safe_strip(parts[1])
end
def safe_strip(s)
s.strip! unless s.nil?
s
end
end
class InvalidToken < RuntimeError; end
class CannotAccessContext < RuntimeError; end

View file

@ -3,7 +3,7 @@ class DataController < ApplicationController
require 'csv'
def index
@page_title = "TRACKS::Export"
@page_title = "TRACKS::Export"
end
def import
@ -24,6 +24,7 @@ class DataController < ApplicationController
all_tables['tags'] = current_user.tags.find(:all)
all_tables['taggings'] = current_user.taggings.find(:all)
all_tables['notes'] = current_user.notes.find(:all)
all_tables['recurring_todos'] = current_user.recurring_todos.find(:all)
result = all_tables.to_yaml
result.gsub!(/\n/, "\r\n") # TODO: general functionality for line endings
@ -34,21 +35,21 @@ class DataController < ApplicationController
content_type = 'text/csv'
CSV::Writer.generate(result = "") do |csv|
csv << ["id", "Context", "Project", "Description", "Notes", "Tags",
"Created at", "Due", "Completed at", "User ID", "Show from",
"state"]
"Created at", "Due", "Completed at", "User ID", "Show from",
"state"]
current_user.todos.find(:all, :include => [:context, :project]).each do |todo|
# Format dates in ISO format for easy sorting in spreadsheet
# Print context and project names for easy viewing
# Format dates in ISO format for easy sorting in spreadsheet Print
# context and project names for easy viewing
csv << [todo.id, todo.context.name,
todo.project_id = todo.project_id.nil? ? "" : todo.project.name,
todo.description,
todo.notes, todo.tags.collect{|t| t.name}.join(', '),
todo.created_at.to_formatted_s(:db),
todo.due = todo.due? ? todo.due.to_formatted_s(:db) : "",
todo.completed_at = todo.completed_at? ? todo.completed_at.to_formatted_s(:db) : "",
todo.user_id,
todo.show_from = todo.show_from? ? todo.show_from.to_formatted_s(:db) : "",
todo.state]
todo.project_id = todo.project_id.nil? ? "" : todo.project.name,
todo.description,
todo.notes, todo.tags.collect{|t| t.name}.join(', '),
todo.created_at.to_formatted_s(:db),
todo.due = todo.due? ? todo.due.to_formatted_s(:db) : "",
todo.completed_at = todo.completed_at? ? todo.completed_at.to_formatted_s(:db) : "",
todo.user_id,
todo.show_from = todo.show_from? ? todo.show_from.to_formatted_s(:db) : "",
todo.state]
end
end
send_data(result, :filename => "todos.csv", :type => content_type)
@ -58,16 +59,17 @@ class DataController < ApplicationController
content_type = 'text/csv'
CSV::Writer.generate(result = "") do |csv|
csv << ["id", "User ID", "Project", "Note",
"Created at", "Updated at"]
# had to remove project include because it's association order is leaking through
# and causing an ambiguous column ref even with_exclusive_scope didn't seem to help -JamesKebinger
"Created at", "Updated at"]
# had to remove project include because it's association order is leaking
# through and causing an ambiguous column ref even with_exclusive_scope
# didn't seem to help -JamesKebinger
current_user.notes.find(:all,:order=>"notes.created_at").each do |note|
# Format dates in ISO format for easy sorting in spreadsheet
# Print context and project names for easy viewing
# Format dates in ISO format for easy sorting in spreadsheet Print
# context and project names for easy viewing
csv << [note.id, note.user_id,
note.project_id = note.project_id.nil? ? "" : note.project.name,
note.body, note.created_at.to_formatted_s(:db),
note.updated_at.to_formatted_s(:db)]
note.project_id = note.project_id.nil? ? "" : note.project.name,
note.body, note.created_at.to_formatted_s(:db),
note.updated_at.to_formatted_s(:db)]
end
end
send_data(result, :filename => "notes.csv", :type => content_type)
@ -81,6 +83,7 @@ class DataController < ApplicationController
result << current_user.tags.find(:all).to_xml(:skip_instruct => true)
result << current_user.taggings.find(:all).to_xml(:skip_instruct => true)
result << current_user.notes.find(:all).to_xml(:skip_instruct => true)
result << current_user.recurring_todos.find(:all).to_xml(:skip_instruct => true)
send_data(result, :filename => "tracks_backup.xml", :type => 'text/xml')
end
@ -88,8 +91,113 @@ class DataController < ApplicationController
# Draw the form to input the YAML text data
end
def yaml_import
# Logic to load the YAML text file and create new records from data
# adjusts time to utc
def adjust_time(timestring)
if (timestring=='') or ( timestring == nil)
return nil
else
return Time.parse(timestring + 'UTC')
end
end
end
def yaml_import
@errmessage = ''
@inarray = YAML::load(params['import']['yaml'])
# arrays to handle id translations
# contexts
translate_context = Hash.new
translate_context[nil] = nil
current_user.contexts.each { |context| context.destroy }
@inarray['contexts'].each { | item |
newitem = Context.new(item.ivars['attributes'])
newitem.user_id = current_user.id
newitem.created_at = adjust_time(item.ivars['attributes']['created_at'])
newitem.save(false)
translate_context[item.ivars['attributes']['id'].to_i] = newitem.id
}
# projects
translate_project = Hash.new
translate_project[nil] = nil
current_user.projects.each { |item| item.destroy }
@inarray['projects'].each { |item|
newitem = Project.new(item.ivars['attributes'])
# ids
newitem.user_id = current_user.id
newitem.default_context_id = translate_context[newitem.default_context_id]
newitem.save(false)
translate_project[item.ivars['attributes']['id'].to_i] = newitem.id
# state + dates
newitem.transition_to(item.ivars['attributes']['state'])
newitem.completed_at = adjust_time(item.ivars['attributes']['completed_at'])
newitem.created_at = adjust_time(item.ivars['attributes']['created_at'])
newitem.position = item.ivars['attributes']['position']
newitem.save(false)
}
# todos
translate_todo = Hash.new
translate_todo[nil] = nil
current_user.todos.each { |item| item.destroy }
@inarray['todos'].each { |item|
newitem = Todo.new(item.ivars['attributes'])
# ids
newitem.user_id = current_user.id
newitem.context_id = translate_context[newitem.context_id]
newitem.project_id = translate_project[newitem.project_id]
# TODO: vyresit recurring_todo_id
newitem.save(false)
translate_todo[item.ivars['attributes']['id'].to_i] = newitem.id
# state + dates
case item.ivars['attributes']['state']
when 'active' then newitem.activate!
when 'project_hidden' then newitem.hide!
when 'completed'
newitem.complete!
newitem.completed_at = adjust_time(item.ivars['attributes']['completed_at'])
when 'deferred' then newitem.defer!
end
newitem.created_at = adjust_time(item.ivars['attributes']['created_at'])
newitem.save(false)
}
# tags
translate_tag = Hash.new
translate_tag[nil] = nil
current_user.tags.each { |item| item.destroy }
@inarray['tags'].each { |item|
newitem = Tag.new(item.ivars['attributes'])
newitem.created_at = adjust_time(item.ivars['attributes']['created_at'])
newitem.save
translate_tag[item.ivars['attributes']['id'].to_i] = newitem.id
}
# taggings
current_user.taggings.each { |item| item.destroy }
@inarray['taggings'].each { |item|
newitem = Tagging.new(item.ivars['attributes'])
newitem.user_id = current_user.id
newitem.tag_id = translate_tag[newitem.tag_id]
case newitem.taggable_type
when 'Todo' then newitem.taggable_id = translate_todo[newitem.taggable_id]
else newitem.taggable_id = 0
end
newitem.save
}
# notes
current_user.notes.each { |item| item.destroy }
@inarray['notes'].each { |item|
newitem = Note.new(item.ivars['attributes'])
newitem.id = item.ivars['attributes']['id']
newitem.user_id = current_user.id
newitem.project_id = translate_project[newitem.project_id]
newitem.created_at = adjust_time(item.ivars['attributes']['created_at'])
newitem.save
}
end
end

View file

@ -2,9 +2,9 @@ class TodosController < ApplicationController
helper :todos
skip_before_filter :login_required, :only => [:index]
prepend_before_filter :login_or_feed_token_required, :only => [:index]
append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, :edit, :update, :create ]
skip_before_filter :login_required, :only => [:index, :calendar]
prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar]
append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, :edit, :update, :create, :calendar ]
append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ]
session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) }
@ -128,8 +128,10 @@ class TodosController < ApplicationController
# Toggles the 'done' status of the action
#
def toggle_check
@source_view = params['_source_view'] || 'todo'
@original_item_due = @todo.due
@saved = @todo.toggle_completion!
# check if this todo has a related recurring_todo. If so, create next todo
check_for_next_todo if @saved
@ -139,6 +141,10 @@ class TodosController < ApplicationController
determine_remaining_in_context_count(@todo.context_id)
determine_down_count
determine_completed_count if @todo.completed?
if source_view_is :calendar
@original_item_due_id = get_due_id_for_calendar(@original_item_due)
@old_due_empty = is_old_due_empty(@original_item_due_id)
end
end
render
end
@ -172,6 +178,9 @@ class TodosController < ApplicationController
@original_item_context_id = @todo.context_id
@original_item_project_id = @todo.project_id
@original_item_was_deferred = @todo.deferred?
@original_item_due = @todo.due
@original_item_due_id = get_due_id_for_calendar(@todo.due)
if params['todo']['project_id'].blank? && !params['project_name'].nil?
if params['project_name'] == 'None'
project = Project.null_object
@ -221,7 +230,26 @@ class TodosController < ApplicationController
@saved = @todo.update_attributes params["todo"]
@context_changed = @original_item_context_id != @todo.context_id
@todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active?
determine_remaining_in_context_count(@original_item_context_id) if @context_changed
if source_view_is :calendar
@due_date_changed = @original_item_due != @todo.due
if @due_date_changed
@old_due_empty = is_old_due_empty(@original_item_due_id)
if @todo.due.nil?
# do not act further on date change when date is changed to nil
@due_date_changed = false
else
@new_due_id = get_due_id_for_calendar(@todo.due)
end
end
end
if @context_changed
determine_remaining_in_context_count(@original_item_context_id)
else
determine_remaining_in_context_count(@todo.context_id)
end
@project_changed = @original_item_project_id != @todo.project_id
if (@project_changed && !@original_item_project_id.nil?) then @remaining_undone_in_project = current_user.projects.find(@original_item_project_id).not_done_todo_count; end
determine_down_count
@ -245,6 +273,7 @@ class TodosController < ApplicationController
def destroy
@todo = get_todo_from_params
@original_item_due = @todo.due
@context_id = @todo.context_id
@project_id = @todo.project_id
@ -270,6 +299,9 @@ class TodosController < ApplicationController
determine_down_count
if source_view_is_one_of(:todo, :deferred)
determine_remaining_in_context_count(@context_id)
elsif source_view_is :calendar
@original_item_due_id = get_due_id_for_calendar(@original_item_due)
@old_due_empty = is_old_due_empty(@original_item_due_id)
end
end
render
@ -384,7 +416,63 @@ class TodosController < ApplicationController
end
end
private
def defer
@source_view = params['_source_view'] || 'todo'
numdays = params['days'].to_i
@todo = Todo.find(params[:id])
@todo.show_from = (@todo.show_from || @todo.user.date) + numdays.days
@saved = @todo.save
determine_down_count
determine_remaining_in_context_count(@todo.context_id)
respond_to do |format|
format.html { redirect_to :back }
format.js {render :action => 'update'}
end
end
def calendar
@source_view = params['_source_view'] || 'calendar'
@page_title = "TRACKS::Calendar"
due_today_date = Time.zone.now
due_this_week_date = Time.zone.now.end_of_week
due_next_week_date = due_this_week_date + 7.days
due_this_month_date = Time.zone.now.end_of_month
@due_today = current_user.todos.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date],
:order => "due")
@due_this_week = current_user.todos.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date],
:order => "due")
@due_next_week = current_user.todos.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date],
:order => "due")
@due_this_month = current_user.todos.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date],
:order => "due")
@due_after_this_month = current_user.todos.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date],
:order => "due")
respond_to do |format|
format.html
format.ics {
@due_all = current_user.todos.find(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND NOT todos.due IS NULL', 'active', 'deferred'],
:order => "due")
render :action => 'calendar', :layout => false, :content_type => Mime::ICS
}
end
end
private
def get_todo_from_params
@todo = current_user.todos.find(params['id'])
@ -688,14 +776,74 @@ class TodosController < ApplicationController
@recurring_todo = nil
if @todo.from_recurring_todo?
@recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id)
# check for next todo either from the due date or the show_from date
date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due
date_to_check = Date.today()-1.day if date_to_check.nil?
# if both due and show_from are nil, check for a next todo with yesterday
# as reference point. We pick yesterday so that new todos for today will
# be created instead of new todos for tomorrow.
date_to_check = Time.zone.now-1.day if date_to_check.nil?
if @recurring_todo.active? && @recurring_todo.has_next_todo(date_to_check)
date = date_to_check >= Date.today() ? date_to_check : Date.today()-1.day
# shift the reference date to yesterday if date_to_check is furher in
# the past. This is to make sure we do not get older todos for overdue
# todos. I.e. checking a daily todo that is overdue with 5 days will
# create a new todo which is overdue by 4 days if we don't shift the
# date. Discard the time part in the compare
date = date_to_check.at_midnight >= Time.zone.now.at_midnight ? date_to_check : Time.zone.now-1.day
@new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date)
end
end
end
def get_due_id_for_calendar(due)
return "" if due.nil?
due_today_date = Time.zone.now
due_this_week_date = Time.zone.now.end_of_week
due_next_week_date = due_this_week_date + 7.days
due_this_month_date = Time.zone.now.end_of_month
if due <= due_today_date
new_due_id = "due_today"
elsif due <= due_this_week_date
new_due_id = "due_this_week"
elsif due <= due_next_week_date
new_due_id = "due_next_week"
elsif due <= due_this_month_date
new_due_id = "due_this_month"
else
new_due_id = "due_after_this_month"
end
return new_due_id
end
def is_old_due_empty(id)
due_today_date = Time.zone.now
due_this_week_date = Time.zone.now.end_of_week
due_next_week_date = due_this_week_date + 7.days
due_this_month_date = Time.zone.now.end_of_month
case id
when "due_today"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date])
when "due_this_week"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date])
when "due_next_week"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date])
when "due_this_month"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date])
when "due_after_this_month"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date])
else
raise Exception.new, "unknown due id for calendar: '#{id}'"
end
end
class FindConditionBuilder

View file

@ -280,4 +280,8 @@ module TodosHelper
image_tag("blank.png", :title =>"Star action", :class => class_str)
end
def defer_link(days)
link_to_remote image_tag("defer_#{days}.png"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")}
end
end

View file

@ -0,0 +1,34 @@
class MessageGateway < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper
def receive(email)
user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip])
if user.nil?
user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip[1,100]])
end
return if user.nil?
context = user.prefs.sms_context
description = nil
notes = nil
if email.content_type == "multipart/related"
description = sanitize email.subject
body_part = email.parts.find{|m| m.content_type == "text/plain"}
notes = sanitize body_part.body.strip
else
if email.subject.empty?
description = sanitize email.body.strip
notes = nil
else
description = sanitize email.subject.strip
notes = sanitize email.body.strip
end
end
# stupid T-Mobile often sends the same message multiple times
return if user.todos.find(:first, :conditions => {:description => description})
todo = Todo.from_rich_message(user, context.id, description, notes)
todo.save!
end
end

View file

@ -1,5 +1,6 @@
class Preference < ActiveRecord::Base
belongs_to :user
belongs_to :sms_context, :class_name => 'Context'
def self.due_styles
{ :due_in_n_days => 0, :due_on => 1}
@ -21,7 +22,7 @@ class Preference < ActiveRecord::Base
def parse_date(s)
return nil if s.blank?
Date.strptime(s, date_format)
user.at_midnight(Date.strptime(s, date_format))
end
end

View file

@ -16,7 +16,7 @@ class Project < ActiveRecord::Base
state :active
state :hidden, :enter => :hide_todos, :exit => :unhide_todos
state :completed, :enter => Proc.new { |p| p.completed_at = Time.now.utc }, :exit => Proc.new { |p| p.completed_at = nil }
state :completed, :enter => Proc.new { |p| p.completed_at = Time.zone.now }, :exit => Proc.new { |p| p.completed_at = nil }
event :activate do
transitions :to => :active, :from => [:hidden, :completed]

View file

@ -12,7 +12,7 @@ class RecurringTodo < ActiveRecord::Base
t[:show_from], t.completed_at = nil, nil
t.occurences_count = 0
}
state :completed, :enter => Proc.new { |t| t.completed_at = Time.now.utc }, :exit => Proc.new { |t| t.completed_at = nil }
state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
validates_presence_of :description
validates_length_of :description, :maximum => 100
@ -243,7 +243,7 @@ class RecurringTodo < ActiveRecord::Base
if self.recurrence_selector == 0
return self.every_other2
else
return Time.now.month
return Time.zone.now.month
end
end
@ -257,7 +257,7 @@ class RecurringTodo < ActiveRecord::Base
if self.recurrence_selector == 1
return self.every_other2
else
return Time.now.month
return Time.zone.now.month
end
end
@ -397,7 +397,7 @@ class RecurringTodo < ActiveRecord::Base
# determine start
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
start = self.start_from.nil? ? Time.zone.now : self.start_from
else
# use the next day
start = previous + 1.day
@ -429,7 +429,7 @@ class RecurringTodo < ActiveRecord::Base
def get_weekly_date(previous)
# determine start
if previous == nil
start = self.start_from.nil? ? Time.now.utc : self.start_from
start = self.start_from.nil? ? Time.zone.now : self.start_from
else
start = previous + 1.day
if start.wday() == 0
@ -474,7 +474,7 @@ class RecurringTodo < ActiveRecord::Base
start += n.months
# go back to day
end
return Time.utc(start.year, start.month, day)
return Time.zone.local(start.year, start.month, day)
when 1 # relative weekday of a month
the_next = get_xth_day_of_month(self.every_other3, self.every_count, start.month, start.year)
@ -496,14 +496,14 @@ class RecurringTodo < ActiveRecord::Base
def get_xth_day_of_month(x, weekday, month, year)
if x == 5
# last -> count backwards
last_day = Time.utc(year, month, Time.days_in_month(month))
last_day = Time.zone.local(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
return last_day
else
# 1-4th -> count upwards
start = Time.utc(year,month,1)
start = Time.zone.local(year,month,1)
n = x
while n > 0
while start.wday() != weekday
@ -526,14 +526,14 @@ class RecurringTodo < ActiveRecord::Base
when 0 # specific day of a specific month
# if there is no next month n in this year, search in next year
if start.month >= month
start = Time.utc(start.year+1, month, 1) if start.day >= day
start = Time.utc(start.year, month, 1) if start.day <= day
start = Time.zone.local(start.year+1, month, 1) if start.day >= day
start = Time.zone.local(start.year, month, 1) if start.day <= day
end
return Time.utc(start.year, month, day)
return Time.zone.local(start.year, month, day)
when 1 # relative weekday of a specific month
# if there is no next month n in this year, search in next year
the_next = start.month > month ? Time.utc(start.year+1, month, 1) : start
the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start
# get the xth day of the month
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, the_next.year)
@ -602,7 +602,7 @@ class RecurringTodo < ActiveRecord::Base
def determine_start(previous)
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
start = self.start_from.nil? ? Time.zone.now : self.start_from
else
start = previous

View file

@ -13,7 +13,7 @@ class Todo < ActiveRecord::Base
# of state completed is not run, see #679
state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil }
state :project_hidden
state :completed, :enter => Proc.new { |t| t.completed_at = Time.now.utc }, :exit => Proc.new { |t| t.completed_at = nil }
state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
state :deferred
event :defer do
@ -68,6 +68,8 @@ class Todo < ActiveRecord::Base
end
def show_from=(date)
# parse Date objects into the proper timezone
date = user.at_midnight(date) if (date.is_a? Date)
activate! if deferred? && date.blank?
defer! if active? && !date.blank? && date > user.date
self[:show_from] = date
@ -125,4 +127,46 @@ class Todo < ActiveRecord::Base
return self.recurring_todo_id != nil
end
end
# Rich Todo API
def self.from_rich_message(user, default_context_id, description, notes)
fields = description.match /([^>@]*)@?([^>]*)>?(.*)/
description = fields[1].strip
context = fields[2].strip
project = fields[3].strip
context = nil if context == ""
project = nil if project == ""
context_id = default_context_id
unless(context.nil?)
found_context = user.active_contexts.find_by_namepart(context)
found_context = user.contexts.find_by_namepart(context) if found_context.nil?
context_id = found_context.id unless found_context.nil?
end
unless user.contexts.exists? context_id
raise(CannotAccessContext, "Cannot access a context that does not belong to this user.")
end
project_id = nil
unless(project.blank?)
if(project[0..3].downcase == "new:")
found_project = user.projects.build
found_project.name = project[4..255+4].strip
found_project.save!
else
found_project = user.active_projects.find_by_namepart(project)
found_project = user.projects.find_by_namepart(project) if found_project.nil?
end
project_id = found_project.id unless found_project.nil?
end
todo = user.todos.build
todo.description = description
todo.notes = notes
todo.context_id = context_id
todo.project_id = project_id unless project_id.nil?
return todo
end
end

View file

@ -59,7 +59,7 @@ class User < ActiveRecord::Base
has_many :active_contexts,
:class_name => 'Context',
:order => 'position ASC',
:conditions => [ 'hide = ?', 'true' ]
:conditions => [ 'hide = ?', false ]
has_many :todos,
:order => 'todos.completed_at DESC, todos.created_at DESC',
:dependent => :delete_all
@ -71,8 +71,7 @@ class User < ActiveRecord::Base
:conditions => [ 'state = ?', 'deferred' ],
:order => 'show_from ASC, todos.created_at DESC' do
def find_and_activate_ready
# assumes that active record uses :utc to store datetime in db
find(:all, :conditions => ['show_from <= ?', Time.now.utc ]).collect { |t| t.activate! }
find(:all, :conditions => ['show_from <= ?', Time.zone.now ]).collect { |t| t.activate! }
end
end
has_many :completed_todos,
@ -170,7 +169,11 @@ class User < ActiveRecord::Base
end
def date
time.to_date
time.midnight
end
def at_midnight(date)
return TimeZone[prefs.time_zone].local(date.year, date.month, date.day, 0, 0, 0)
end
def generate_token

View file

@ -53,4 +53,9 @@
</div>
<%
sortable_element 'list-contexts', get_listing_sortable_options
-%>
-%>
<script type="text/javascript">
window.onload=function(){
Nifty("div#context_new_container","normal");
}
</script>

View file

@ -1,37 +1,49 @@
<div id="feeds">
<div id="feedlegend">
<h3>Exporting data</h3>
<p>You can choose between the following formats:</p>
<ul>
<li><strong>YAML: </strong>Best for exporting data. <br/><i>Please note that Tracks currently does not support importing YAML files. Do not use this (yet) to backup your Tracks database.</i></li>
<li><strong>CSV: </strong>Best for importing into spreadsheet or data analysis software</li>
<li><strong>XML: </strong>Best for importing or repurposing the data</li>
</ul>
</div>
<div id="display_box">
<div id="feeds">
<div id="feedlegend">
<h3>Importing data</h3>
<p>Curently there is a experimental support for importing YAML files.
Beware: all your current data will be destroyed before importing the YAML
file, so if you have access to the database, we strongly recomment backing up
the database right now in case that anything goes wrong.
</p>
<p><%= link_to "Start import", :controller => 'data', :action => 'yaml_form' %>.</p>
<br/><br/><h3>Exporting data</h3>
<p>You can choose between the following formats:</p>
<ul>
<li><strong>YAML: </strong>Best for exporting data. <br/><i>Please note that importing YAML files is currently supported only in experimentally. Do not rely on it for backing up critical data.</i></li>
<li><strong>CSV: </strong>Best for importing into spreadsheet or data analysis software</li>
<li><strong>XML: </strong>Best for importing or repurposing the data</li>
</ul>
</div>
<br/><br/>
<table class="export_table">
<tr>
<th>Description</th>
<th>Download link</th>
</tr>
<tr>
<td>YAML file containing all your actions, contexts, projects, tags and notes</td>
<td><%= link_to "YAML file", :controller => 'data', :action => 'yaml_export' %></td>
</tr>
<tr>
<td>CSV file containing all of your actions, with named contexts and projects</td>
<td><%= link_to "CSV file (actions, contexts and projects)", :controller => 'data', :action => 'csv_actions' %></td>
</tr>
<tr>
<td>CSV file containing all your notes</td>
<td><%= link_to "CSV file (notes only)", :controller => 'data', :action => 'csv_notes' %></td>
</tr>
<tr>
<td>XML file containing all your actions, contexts, projects, tags and notes</td>
<td><%= link_to "XML file (actions only)", :controller => 'data', :action => 'xml_export' %></td>
</tr>
</table>
</div><!-- End of feeds -->
</div>
<p>
<table class="export_table">
<tr>
<th>Description</th>
<th>Download link</th>
</tr>
<tr>
<td>YAML file containing all your actions, contexts, projects, tags and notes</td>
<td><%= link_to "YAML file", :controller => 'data', :action => 'yaml_export' %></td>
</tr>
<tr>
<td>CSV file containing all of your actions, with named contexts and projects</td>
<td><%= link_to "CSV file (actions, contexts and projects)", :controller => 'data', :action => 'csv_actions' %></td>
</tr>
<tr>
<td>CSV file containing all your notes</td>
<td><%= link_to "CSV file (notes only)", :controller => 'data', :action => 'csv_notes' %></td>
</tr>
<tr>
<td>XML file containing all your actions, contexts, projects, tags and notes</td>
<td><%= link_to "XML file (actions only)", :controller => 'data', :action => 'xml_export' %></td>
</tr>
</table>
</p>
</div><!-- End of feeds -->
<script type="text/javascript">
window.onload=function(){
Nifty("div#feedlegend","normal");
}
</script>

View file

@ -1,19 +1,23 @@
<div id="display_box">
<div id="feeds">
<div id="feedlegend">
<p>Paste the contents of the YAML file you exported into the text box below:</p>
</div>
<p>
<% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %>
<%= f.text_area :yaml %><br />
<input type="submit" value="Import data">
<% end %>
</p>
</div><!-- End of feeds -->
<div id="feeds">
<div id="feedlegend">
<p><b>Beware</b>: all your current data will be destroyed before importing
the YAML file, so if you have access to the database, we strongly recommend
backing up the database right now in case that anything goes wrong.
</p>
<p>Paste the contents of the YAML file you exported into the text box below:</p>
</div>
<p>
<% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %>
<%= f.text_area :yaml %><br />
<input type="submit" value="Import data">
<% end %>
</p>
</div><!-- End of feeds -->
</div><!-- End of display_box -->
<div id="input_box">
</div><!-- End of input box -->
<script type="text/javascript">
window.onload=function(){
Nifty("div#feedlegend","normal");
}
</script>

View file

@ -1 +1,7 @@
<p>Import was successful</p>
<% if !(@errmessage == '') %>
<p>There were these errors:
<pre><%= @errmessage %></pre>
</p>
<% else %>
<p>Import was successful.</p>
<% end %>

View file

@ -12,6 +12,18 @@
<p>Do you have one of your own to add? <a href="http://www.rousette.org.uk/projects/forums/viewforum/10/" title="Tracks | Tips and Tricks">Tell us about it in our Tips and Tricks forum
</a> and we may include it on this page in a future versions of Tracks.</p>
<a name="message_gateway"> </a>
<h2>Integrated email/SMS receiver</h2>
<p>
If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up:
<ul>
<li>Go to <%= link_to "Preferences", preferences_url %> and set your "From email" and "default email context" for todos sent in via email (which could come from an SMS message)</li>
<li>In sendmail/qmail/postfix/whatever, set up an email address alias to pipe messages to <pre style="font-size:1.5em">/PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'</pre></li>
<li>Send an email to your newly configured address!</li>
</ul>
You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or "Call Bill > project X". The subject of the message will fill description, context, and project, while the body will populate the tasks's note.
</p>
<a name="applescript1-section"> </a>
<h2>Add an Action with Applescript</h2>
<p>This is a simple script that pops up a dialog box asking for a description, and then sends that to Tracks with a hard-coded context.</p>

View file

@ -16,9 +16,6 @@
<script type="text/javascript">
window.onload=function(){
Nifty("div#todo_new_action_container","normal");
Nifty("div#project_new_project_container","normal");
Nifty("div#context_new_container","normal");
Nifty("div#recurring_new_container","normal");
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
}
</script>
@ -57,6 +54,7 @@ window.onload=function(){
<% if current_user.is_admin? -%>
<li><%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %></li>
<% end -%>
<li><%= navigation_link(image_tag("x-office-calendar.png", :size => "16X16", :border => 0), calendar_path, :title => "Calendar of due actions" ) %></li>
<li><%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %></li>
<li><%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %></li>
<li><%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %></li>

View file

@ -2,7 +2,7 @@
<div id="<%= dom_id(note, 'container') %>">
<h2><%= link_to("Note #{note.id}", note_path(note), :title => "Show note #{note.id}" ) %></h2>
<div id="<%= dom_id(note) %>">
<%= sanitize(textilize(note.body.gsub(/((https?:\/\/[^ \n\t]*))/, '"\1":\2'))) %>
<%= sanitize(markdown(auto_link(note.body))) %>
<div class="note_footer">
<%= link_to_remote(

View file

@ -19,6 +19,8 @@
<li><strong>refresh:</strong> automatic refresh interval for each of the pages (in minutes)</li>
<li><strong>verbose action descriptor:</strong> when true, show project/context name in action listing; when false show [P]/[C] with tool tips</li>
<li><strong>mobile todos per page:</strong> the maximum number of actions to show on a single page in the mobile view</li>
<li><strong>From email:</string> the email address you use for sending todos as email or SMS messages to your Tracks account</li>
<li><string>Default email context:</string> the context to which tasks sent in via email or SMS should be added</li>
</ul>
</div>
@ -67,6 +69,8 @@
<%= row_with_text_field('refresh') %>
<%= row_with_select_field("verbose_action_descriptors") %>
<%= row_with_text_field("mobile_todos_per_page") %>
<%= row_with_text_field("sms_email") %>
<%= table_row("sms_context", false) { select('prefs', 'sms_context_id', current_user.contexts.map{|c| [c.name, c.id]}) } %>
<tr><td><%= submit_tag "Update" %></td>
<td><%= link_to "Cancel", :action => 'index' %></td>

View file

@ -28,6 +28,8 @@
<li>Refresh interval (in minutes): <span class="highlight"><%= prefs.refresh %></span></li>
<li>Verbose action descriptors: <span class="highlight"><%= prefs.verbose_action_descriptors %></span></li>
<li>Actions per page (Mobile View): <span class="highlight"><%= prefs.mobile_todos_per_page %></span></li>
<li>From email: <span class="highlight"><%= prefs.sms_email %></span></li>
<li>Default email context: <span class="highlight"><%= prefs.sms_context.nil? ? "None" : prefs.sms_context.name %></span></li>
</ul>
<div class="actions">
<%= link_to "Edit preferences &raquo;", { :controller => 'preferences', :action => 'edit'}, :class => 'edit_link' %>

View file

@ -61,4 +61,10 @@
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
window.onload=function(){
Nifty("div#project_new_project_container","normal");
}
</script>

View file

@ -101,22 +101,22 @@
<div id="recurring_monthly" style="display:none">
<label>Settings for monthly recurring actions</label><br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_x_day', true)%> Day <%=
text_field_tag('recurring_todo[monthly_every_x_day]', Time.now.mday, {"size" => 3, "tabindex" => 9}) %> on every <%=
text_field_tag('recurring_todo[monthly_every_x_day]', Time.zone.now.mday, {"size" => 3, "tabindex" => 9}) %> on every <%=
text_field_tag('recurring_todo[monthly_every_x_month]', 1, {"size" => 3, "tabindex" => 10}) %> month<br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_xth_day')%> The <%=
select_tag('recurring_todo[monthly_every_xth_day]', options_for_select(@xth_day), {}) %> <%=
select_tag('recurring_todo[monthly_day_of_week]' , options_for_select(@days_of_week, Time.now.wday), {}) %> of every <%=
select_tag('recurring_todo[monthly_day_of_week]' , options_for_select(@days_of_week, Time.zone.now.wday), {}) %> of every <%=
text_field_tag('recurring_todo[monthly_every_x_month2]', 1, {"size" => 3, "tabindex" => 11}) %> month<br/>
</div>
<div id="recurring_yearly" style="display:none">
<label>Settings for yearly recurring actions</label><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_x_day', true)%> Every <%=
select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, Time.now.month), {}) %> <%=
text_field_tag('recurring_todo[yearly_every_x_day]', Time.now.day, "size" => 3, "tabindex" => 9) %><br/>
select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, Time.zone.now.month), {}) %> <%=
text_field_tag('recurring_todo[yearly_every_x_day]', Time.zone.now.day, "size" => 3, "tabindex" => 9) %><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_xth_day')%> The <%=
select_tag('recurring_todo[yearly_every_xth_day]', options_for_select(@xth_day), {}) %> <%=
select_tag('recurring_todo[yearly_day_of_week]', options_for_select(@days_of_week, Time.now.wday), {}) %> of <%=
select_tag('recurring_todo[yearly_month_of_year2]', options_for_select(@months_of_year, Time.now.month), {}) %><br/>
select_tag('recurring_todo[yearly_day_of_week]', options_for_select(@days_of_week, Time.zone.now.wday), {}) %> of <%=
select_tag('recurring_todo[yearly_month_of_year2]', options_for_select(@months_of_year, Time.zone.now.month), {}) %><br/>
</div>
<div id="recurring_target">
<label>Set recurrence on</label><br/>

View file

@ -40,3 +40,9 @@
apply_behaviour "#recurring_edit_period:click",
"TracksForm.hide_all_edit_recurring(); $('recurring_edit_'+TracksForm.get_edit_period()).show();"
-%>
<script type="text/javascript">
window.onload=function(){
Nifty("div#recurring_new_container","normal");
}
</script>

View file

@ -11,6 +11,7 @@
<%= remote_star_icon %>
<%= remote_toggle_checkbox unless source_view_is :deferred %>
<div class="description<%= staleness_class( todo ) %>">
<% unless @todo.completed? %><span class="defer-container"><%= defer_link(1) %> <%= defer_link(7) %></span><% end %>
<%= date_span -%>
<span class="todo.descr"><%= h sanitize(todo.description) %></span>
<%= link_to(image_tag("recurring16x16.png"), {:controller => "recurring_todos", :action => "index"}, :class => "recurring_icon") if @todo.from_recurring_todo? %>

View file

@ -3,5 +3,5 @@
element.next('.todo_notes').toggle
end -%>
<div class="todo_notes" id="<%= dom_id(item, 'notes') %>" style="display:none">
<%= markdown( item.notes.gsub(/((https?:\/\/[^ \n\t]*))/, '"\1":\2') ) %>
<%= sanitize(markdown( auto_link(item.notes)) ) %>
</div>

View file

@ -0,0 +1,68 @@
<div id="display_box">
<div class="container">
<h2>Due today</h2>
<div id="empty_due_today" <%= "style=\"display:none\"" unless @due_today.empty? %>>
No actions due today
</div>
<div id="due_today">
<%= render :partial => "todos/todo", :collection => @due_today %>
</div>
</div>
<div class="container">
<h2>Due in rest of this week</h2>
<div id="empty_due_this_week" <%= "style=\"display:none\"" unless @due_this_week.empty? %>>
No actions due in rest of this week
</div>
<div id="due_this_week">
<%= render :partial => "todos/todo", :collection => @due_this_week %>
</div>
</div>
<div class="container">
<h2>Due next week</h2>
<div id="empty_due_next_week" <%= "style=\"display:none\"" unless @due_next_week.empty? %>>
No actions due in next week
</div>
<div id="due_next_week">
<%= render :partial => "todos/todo", :collection => @due_next_week %>
</div>
</div>
<div class="container">
<h2>Due in rest of this month</h2>
<div id="empty_due_this_month" <%= "style=\"display:none\"" unless @due_this_month.empty? %>>
No actions due in rest of this month
</div>
<div id="due_this_month">
<%= render :partial => "todos/todo", :collection => @due_this_month %>
</div>
</div>
<div class="container">
<h2>Due next month and later</h2>
<div id="empty_due_after_this_month" <%= "style=\"display:none\"" unless @due_after_this_month.empty? %>>
No actions due after this month
</div>
<div id="due_after_this_month">
<%= render :partial => "todos/todo", :collection => @due_after_this_month %>
</div>
</div>
</div><!-- End of display_box -->
<div class="input_box" id="input_box">
<!--
<input class="hide_tickler" id="hide_tickler" type="checkbox" tabindex="5" name="hide_tickler" checked="true"/>
<label for="hide_tickler"> Show actions in tickler</label>
<br/><br/>
-->
<p><%= link_to('<span class="feed">iCal</span>', {:format => 'ics', :token => current_user.token}, :title => "iCal feed" ) %>
- Get this calendar in iCal format</p>
</div>
<%
apply_behavior 'input.hide_tickler:click', :prevent_default => true do |page|
page << "alert('hiding action in tickler from calendar is not yet implemented');"
end
%>

View file

@ -0,0 +1,31 @@
BEGIN:VCALENDAR
PRODID:-//TRACKS//<%= TRACKS_VERSION %>//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Tracks
<% for todo in @due_all
due_date = todo.due
overdue_text = ""
if due_date.at_midnight < Time.zone.now.at_midnight
due_date = Time.zone.now
overdue_text = "Overdue: "
end
%>BEGIN:VEVENT
DTSTART;VALUE=DATE:<%= due_date.strftime("%Y%m%d") %>
DTEND;VALUE=DATE:<%= (due_date+1.day).strftime("%Y%m%d") %>
DTSTAMP:<%= due_date.strftime("%Y%m%dT%H%M%SZ") %>
UID:<%= todo_url(todo) %>
CLASS:PUBLIC
CATEGORIES:Tracks
CREATED:<%= todo.created_at.strftime("%Y%m%dT%H%M%SZ") %>
DESCRIPTION:<%= format_ical_notes(todo.notes) %>
LAST-MODIFIED:<%= due_date.strftime("%Y%m%dT%H%M%SZ") %>
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:<%= overdue_text + todo.description %>
TRANSP:TRANSPARENT
END:VEVENT
<% end
%>END:VCALENDAR

View file

@ -1,5 +1,6 @@
if @saved
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
page['badge_count'].replace_html @down_count
# remove context if empty

View file

@ -1,5 +1,6 @@
if @saved
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
if @todo.completed?
# completed todos move from their context to the completed container
@ -26,7 +27,7 @@ if @saved
page.notify :notice, "There is no next action after the recurring action you just finished. The recurrence is completed", 6.0 if @new_recurring_todo.nil?
end
end
else
# todo is activated from completed container
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo)

View file

@ -16,10 +16,18 @@ if @saved
if (@remaining_in_context == 0)
# remove context container from page if empty
source_view do |from|
from.todo { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 }
from.tag { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 }
from.context { page.show "c#{@original_item_context_id}empty-nd" }
if @context_changed
source_view do |from|
from.todo { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 }
from.tag { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 }
from.context { page.show "c#{@original_item_context_id}empty-nd" }
end
else
source_view do |from|
from.todo { page.visual_effect :fade, item_container_id(@todo), :duration => 0.4 }
from.tag { page.visual_effect :fade, item_container_id(@todo), :duration => 0.4 }
from.context { page.show "c#{@original_item_context_id}empty-nd" }
end
end
end
@ -93,6 +101,23 @@ if @saved
elsif source_view_is :stats
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
elsif source_view_is :calendar
if @due_date_changed
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
page.hide "empty_"+@new_due_id
page.insert_html :bottom, @new_due_id, :partial => 'todos/todo'
page.visual_effect :highlight, dom_id(@todo), :duration => 3
else
if @todo.due.nil?
# due date removed
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
end
else
logger.error "unexpected source_view '#{params[:_source_view]}'"
end

View file

@ -43,9 +43,12 @@ ActionController::Routing::Routes.draw do |map|
# so /todos/tag/version1.5.xml will result in :name => 'version1.5.xml'
# UPDATE: added support for mobile view. All tags ending on .m will be
# routed to mobile view of tags.
todos.tag 'todos/tag/:name', :action => "tag", :format => 'm', :name => /.*\.m/
todos.tag 'todos/tag/:name.m', :action => "tag", :format => 'm'
todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/
todos.calendar 'calendar.ics', :action => "calendar", :format => 'ics'
todos.calendar 'calendar', :action => "calendar"
todos.mobile 'mobile', :action => "index", :format => 'm'
todos.mobile_abbrev 'm', :action => "index", :format => 'm'
todos.mobile_abbrev_new 'm/new', :action => "new", :format => 'm'
@ -55,6 +58,10 @@ ActionController::Routing::Routes.draw do |map|
map.feeds 'feeds', :controller => 'feedlist', :action => 'index'
map.feeds 'feeds.m', :controller => 'feedlist', :action => 'index', :format => 'm'
if Rails.env == 'test'
map.connect '/selenium_helper/login', :controller => 'selenium_helper', :action => 'login'
end
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
map.integrations 'integrations', :controller => 'integrations', :action => 'index'

View file

@ -0,0 +1,11 @@
class AddSmsToPreference < ActiveRecord::Migration
def self.up
add_column :preferences, :sms_email, :string
add_column :preferences, :sms_context_id, :integer
end
def self.down
remove_column :preferences, :sms_context_id
remove_column :preferences, :sms_email
end
end

View file

@ -0,0 +1,27 @@
class ChangeDatesToDatetimes < ActiveRecord::Migration
def self.up
change_column :todos, :show_from, :datetime
change_column :todos, :due, :datetime
change_column :recurring_todos, :start_from, :datetime
change_column :recurring_todos, :end_date, :datetime
User.all(:include => [:todos, :recurring_todos]).each do |user|
user.todos.each do |todo|
todo.update_attribute(:show_from, user.at_midnight(todo.show_from)) unless todo.show_from.nil?
todo.update_attribute(:due, user.at_midnight(todo.due)) unless todo.due.nil?
end
user.recurring_todos.each do |todo|
todo.update_attribute(:start_from, user.at_midnight(todo.start_from)) unless todo.start_from.nil?
todo.update_attribute(:end_date, user.at_midnight(todo.end_date)) unless todo.end_date.nil?
end
end
end
def self.down
change_column :todos, :show_from, :date
change_column :todos, :due, :date
change_column :recurring_todos, :start_from, :date
change_column :recurring_todos, :end_date, :date
end
end

View file

@ -2,7 +2,7 @@
* Homepage: http://www.rousette.org.uk/projects/
* Author: bsag (http://www.rousette.org.uk/)
* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson
* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson, Eric Allen
* Version: 1.6
* Copyright: (cc) 2004-2008 rousette.org.uk
* License: GNU GPL
@ -13,6 +13,20 @@ Trac (for bug reports and feature requests): http://dev.rousette.org.uk/report/6
Wiki (deprecated - please use Trac): http://www.rousette.org.uk/projects/wiki/
== Version 1.7dev
New features:
1. Recurring todos
2. Cleanup of feed page and add feed for starred actions
3. Initial importer of yaml files (still very EXPERIMENTAL)
4. New interface to import an email / sms messages into Tracks (needs an email server on the same server as Tracks)
5. New buttons to quickly defer an action 1 or 7 days
6. Calendar view to review due actions, includes iCal feed to use in your calendar app (tested with Google Calendar, Evolution, Outlook 2007)
Under the hood:
1. Move selenium tests to RSpec
2. Bugfixes
== Version 1.6
1. upgrade to rails 2.0.2
2. new mobile interface (with some iPhone compatibility fixes)

View file

@ -31,7 +31,7 @@ RewriteEngine On
RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be rendered instead

BIN
public/images/defer_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

BIN
public/images/defer_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

View file

@ -1200,4 +1200,10 @@ body.integrations textarea {
padding:3px;
width:80%;
background-color:#ddd;
}
}
.defer-container {
float:right;
}
.defer-container a:hover {
background-color: inherit;
}

View file

@ -220,6 +220,8 @@ body.integrations h2 {margin-top:40px; padding-top:20px; margin-bottom:10px; bor
body.integrations p, body.integrations li {font-size:1.0em}
body.integrations li {list-style-type: disc; list-style-position: inside; margin-left:30px}
body.integrations textarea {margin:10px; padding:3px; width:80%; background-color:#ddd}
.defer-container {float:right}
.defer-container a:hover {background-color: inherit}
div.calendar {position: relative}
.calendar, .calendar table {border: 1px solid #556; font-size: 11px; color: #000; cursor: default; background: #eef; z-index: 110; font-family: tahoma,verdana,sans-serif}
.calendar .button {text-align: center; padding: 2px}

View file

@ -10,7 +10,7 @@ describe Todo do
def create_todo(attributes={})
todo = Todo.new(valid_attributes(attributes))
todo.stub!(:user).and_return(mock_model(User, :date => Time.now))
todo.stub!(:user).and_return(mock_model(User, :date => Time.zone.now))
todo.save!
todo
end
@ -32,7 +32,7 @@ describe Todo do
it 'ensures that show_from is a date in the future' do
todo = Todo.new(valid_attributes)
todo.stub!(:user).and_return(mock_model(User, :date => Time.now))
todo.stub!(:user).and_return(mock_model(User, :date => Time.zone.now))
todo.show_from = 3.days.ago
todo.should have(1).error_on(:show_from)
end

View file

@ -37,7 +37,7 @@ describe User do
it 'has many active contexts' do
User.should have_many(:active_contexts).
with_order('position ASC').
with_conditions('hide = ?', 'true').
with_conditions('hide = ?', false).
with_class_name('Context')
end
@ -178,4 +178,20 @@ describe User do
@user.remember_token_expires_at.should be_between(before, after)
end
end
it "should not activate todos that are showing when UTC is tomorrow" do
context = Context.create(:name => 'a context')
user = User.create(:login => 'user7', :password => 'foobar', :password_confirmation => 'foobar')
user.save!
user.create_preference
user.preference.update_attribute('time_zone', 'Pacific Time (US & Canada)')
# Time.zone = 'Pacific Time (US & Canada)'
Time.stub!(:now).and_return(Time.new.end_of_day - 20.minutes)
todo = user.todos.build(:description => 'test task', :context => context, :show_from => user.date + 1.days)
todo.save!
user.deferred_todos.find_and_activate_ready
user = User.find(user.id)
user.deferred_todos.should include(todo)
end
end

View file

@ -0,0 +1,36 @@
require File.dirname(__FILE__) + '/../../spec_helper'
describe "/notes/_notes.rhtml" do
before :each do
@project = mock_model(Project, :name => "a project")
@note = mock_model(Note, :body => "this is a note", :project => @project,
:created_at => Time.now, :updated_at? => false)
@controller.template.stub!(:apply_behavior)
@controller.template.stub!(:format_date)
@controller.template.stub!(:render)
@controller.template.stub!(:form_remote_tag)
end
it "should render" do
render :partial => "/notes/notes", :locals => {:notes => @note}
response.should have_tag("div.note_footer")
end
it "should auto-link URLs" do
@note.stub!(:body).and_return("http://www.google.com/")
render :partial => "/notes/notes", :locals => {:notes => @note}
response.should have_tag("a[href=\"http://www.google.com/\"]")
end
it "should auto-link embedded URLs" do
@note.stub!(:body).and_return("this is cool: http://www.google.com/")
render :partial => "/notes/notes", :locals => {:notes => @note}
response.should have_tag("a[href=\"http://www.google.com/\"]")
end
it "should parse Textile links correctly" do
@note.stub!(:body).and_return("\"link\":http://www.google.com/")
render :partial => "/notes/notes", :locals => {:notes => @note}
response.should have_tag("a[href=\"http://www.google.com/\"]")
end
end

View file

@ -0,0 +1,33 @@
require File.dirname(__FILE__) + '/../../spec_helper'
describe "/todos/_toggle_notes.rhtml" do
# include ControllerHelper
before :each do
@item = mock_model(Todo, :notes => "this is a note")
@controller.template.stub!(:apply_behavior)
end
it "should render" do
render :partial => "/todos/toggle_notes", :locals => {:item => @item}
response.should have_tag("div.todo_notes")
end
it "should auto-link URLs" do
@item.stub!(:notes).and_return("http://www.google.com/")
render :partial => "/todos/toggle_notes", :locals => {:item => @item}
response.should have_tag("a[href=\"http://www.google.com/\"]")
end
it "should auto-link embedded URLs" do
@item.stub!(:notes).and_return("this is cool: http://www.google.com/")
render :partial => "/todos/toggle_notes", :locals => {:item => @item}
response.should have_tag("a[href=\"http://www.google.com/\"]")
end
it "should parse Textile URLs correctly" do
@item.stub!(:notes).and_return("\"link\":http://www.google.com/")
render :partial => "/todos/toggle_notes", :locals => {:item => @item}
response.should have_tag("a[href=\"http://www.google.com/\"]")
end
end

View file

@ -113,3 +113,21 @@ someday_maybe:
user_id: 1
created_at: <%= today %>
updated_at: <%= today %>
inbox:
id: 13
name: Inbox
position: 1
hide: false
user_id: 4
created_at: <%= today %>
updated_at: <%= today %>
anothercontext:
id: 14
name: anothercontext
position: 2
hide: false
user_id: 4
created_at: <%= today %>
updated_at: <%= today %>

View file

@ -34,3 +34,23 @@ other_user_prefs:
time_zone: "London"
verbose_action_descriptors: false
show_project_on_todo_done: true
sms_user_prefs:
id: 3
user_id: 4
staleness_starts: 7
date_format: "%d/%m/%Y"
title_date_format: "%A, %d %B %Y"
show_number_completed: 5
show_completed_projects_in_sidebar: true
show_hidden_contexts_in_sidebar: true
show_hidden_projects_in_sidebar: true
admin_email: butshesagirl@rousette.org.uk
week_starts: 1
due_style: 0
refresh: 0
time_zone: "London"
verbose_action_descriptors: false
show_project_on_todo_done: true
sms_email: 5555555555@tmomail.net
sms_context_id: 13

374
test/fixtures/sample_mms.txt vendored Normal file
View file

@ -0,0 +1,374 @@
Return-Path: <15555555555/TYPE=PLMN@tmomail.net>
Date: Fri, 6 Jun 2008 21:38:26 -0400
From: 15555555555@tmomail.net
To: gtd@tracks.com
Message-ID: <3645873.13759311212802713215.JavaMail.mms@rlyatl28>
Subject: This is the subject
MIME-Version: 1.0
Content-Type: multipart/related; type="text/html";
boundary="----=_Part_1240237_22156211.1212802713213"
X-UA: treo_600
Importance: Normal
X-Mms-Sender-Visibility: Show
X-MMS-Message-Type: MM4_forward.REQ
X-Priority: 3
X-Proofpoint-Spam-Reason: safe
------=_Part_1240237_22156211.1212802713213
Content-Type: text/html
Content-Transfer-Encoding: quoted-printable
Content-ID: <0000>
Content-Location:mms.smil
Content-Disposition: inline
<html>
<head>
<title>T-Mobile</title>=20
<!--
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
If you can read this text, but much of the message below se=
ems unreadable, you might be using an e-mail program that does not work wit=
h HTML.
=20
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D-->
<style type=3D"text/css">
<!--
=20
.footer {
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
color: #555555;
text-decoration: none;
}
.normal {
font-family: Arial, Helvetica, sans-serif;
font-size: 10px;
color: #555555;
text-decoration: none;
}
=20
-->
</style>
</head>
<body marginwidth=3D"0" marginheight=3D"0" leftmargin=3D"0" topmarg=
in=3D"0" bgcolor=3D"#ffffff">
<table border=3D"0" width=3D"600" cellspacing=3D"0" cellpad=
ding=3D"0">
<tr>
<td width=3D"20" rowspan=3D"8"><img src=3D"=
cid:tmobilespace.gif" width=3D"20" height=3D"20"></td>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:tmobilespace.gif" width=3D"600" height=3D"20"></td>
<td width=3D"20" rowspan=3D"8"><img src=3D"=
cid:tmobilespace.gif" width=3D"20" height=3D"20"></td>
</tr>
<tr>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:dottedline600.gif" width=3D"600"></td>
</tr>
<tr>
<td width=3D"370">
<!-- presentation starts here -->
<table border=3D0><tr><tr><td colspan=3D1=
align=3D"Left">This is the message body</td></tr></tr><TR><TD width=3D350 =
colSpan=3D1><IMG height=3D30 src=3D"cid:tmobilespace.gif" width=3D350></TD=
></TR><TR><TD width=3D350 colSpan=3D4><IMG src=3D"cid:dottedline350.gif" w=
idth=3D350></TD></TR><TR><TR><TD width=3D350 colSpan=3D4><IMG height=3D30 s=
rc=3D"cid:tmobilespace.gif" width=3D350></TD></TR></table> =20
<!-- presentation ends here -->
</td>
<td width=3D"240" bgcolor=3D"#f2f2f2"><BR><=
/td>
</tr>
<tr>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:tmobilelogo.gif" width=3D"600" height=3D"105"></td>
</tr>
</span></td>
</tr>
<tr>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:tmobilespace.gif" width=3D"600" height=3D"40"></td>
</tr>
</table>
</body></html>
------=_Part_1240237_22156211.1212802713213
Content-Type: text/plain; charset=utf-8; name=text.txt
Content-Transfer-Encoding: 7bit
Content-ID: <133>
Content-Location: text.txt
Content-Disposition: inline
This is the message body
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=dottedline350.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=dottedline350.gif
Content-ID: <dottedline350.gif>
R0lGODlhXgEBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBm
AABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/
MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNm
ZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/
mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZm
zGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb/
/5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZ
AJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwA
M8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZ
ZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8A
mf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+Z
zP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAABeAQEA
AAg1AFP9+yeQ4MCCCA8qNMgwYcOFDiNCnPiwokSLFC9qzMgRo8eNHzuCHCmyZMiTJFGaTMlSYUAA
Ow==
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=dottedline600.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=dottedline600.gif
Content-ID: <dottedline600.gif>
R0lGODlhWAIBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBm
AABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/
MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNm
ZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/
mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZm
zGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb/
/5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZ
AJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwA
M8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZ
ZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8A
mf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+Z
zP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAABYAgEA
AAhFAFP9+yeQ4MCCCA8qNMgwYcOFDiNCnPiwokSLFC9qzMgRo8eNHzuCHCmyZMiTJFGaTMlypUuV
MFvGfCmzJs2bM3Pa9BgQADs=
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=tmobilelogo.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=tmobilelogo.gif
Content-ID: <tmobilelogo.gif>
R0lGODlhWAJpAPcAAMPIyfOl1djV2tgFbZWgm+Xi5uzq7vWJxux4s+Lc2+Pq7fn27egjdupOlfu7
5e1ipOE/kfqVzviEtu2Lu/Xv7usChp2aoO359sHDwNzh4/p3uvv6/NbX1OxRqu/y7vb0+Ozy9PbW
67w5dv///vxptZaZlKyqrvX38/FZlamrqNsPhs3Lz/D1+Onm6/Xo6OMAY/c5rfHu8/ZBovUAjf/0
9a2vrLW7vc3T1Y6Qjf/5+Pr3/OAAc////Pv9+v/8+vP18vj1+vr8+ePm4vXy9/L08eMAb+jq5/b4
9PT5/N7g3f/9/+7r8Oju8P/9/uUDceLg5Oro7OAAeeoAeP37/fPw9e3v7OwAc+Hf4+vp7ejl6v//
/fDt8qWjp/r989MXdaqvst8Afvr9+vn8+aGgpJydmvvz/7Cusr68wKOlousui9/d4f/1+8PBxZWS
l6imqre1udoMd+QAavX/+tvZ3f/4/uU0iri6t9HLyrSytp2Wof38/dsCZvH//+UGd7O1su8AcLq4
vJ+fnf//+73DxZmXm9PQ1cfKxv/597K3ufz9+66ztsnHy+/x5+QIfPr3+/b7/pKYmtPOzf3j9PPn
8O/i6/vs89DSz+Ead///9ff69/n8+vj7+Pns+usvk/adz/enz7S0tPn2+/HB2+YwgexrqOAcftgr
gv3h695VmP7l/PX7/v/6+e0Qle2s0f/w/umPxNjo6NLX2tjj3uZpq//b9OVCjewTcOfp5vO72/vO
6/Ojxfy83PaUvOI9hf6w1f7H6uwohP38/v/8+e0PfN0pfeNZlenX4fEAZdLMy/zP5d5ap+Xx7N/W
4PRFmOLl4e+qyPf1+q+ptO4Aav/7+t8/nuLl4vvQ9f/X+p2Km6Khlujl6fDt8d7d6v3w8OHk8fGt
xPTK046Pje398NTj4+aUvvZXoq3CwfTx9f7E2/Y5k7+utt3o3Pb489/K3PX0+Nv/+Pby9+ro6/LW
2u3u6/Hz8fPv8/z++/Hz8OHm6eMAdfj69/f59vf8//37//n7+P7//CH5BAAAAAAALAAAAABYAmkA
AAj/AP8JHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEMeyiGypMmTKFOqXMmypcuX
MD8Ckcmvps2bOHPq3AmtX8yfQIMKHUq0qNGjSJMqXcq0qdOnUKFe8Cl1Z9SrWLNq3coV5QmaI32s
Ekt2LMmwZ8uqTdulbde3cOPKnUs3FBKqBV3h3euTBl+/ef9aHSz4Xd3DiBMrXrxxJr22nFDVqVUn
jeXKoy5r3oyZs+fPoENbhmCqDqlUbBmrXs26dVx6VW5BwZcASpooO/Lp3s07t+/ewH8LHx68uHHd
aVwrX868eVHHIPC5SMKNOhwwxI9r3869+6VOzsOL/x9P3iMNChQycLC06A6A692zOykyvz59+fft
58e/f37y8gAGKOCABMWWBAeLYHCGHWeYol98/fEnIYS45VYHgRhmqCFrLOyjgBoIIoKHGSbA96AU
FKaI4oosQvjfhjDGKCNXBgKgoAnZcOFFhSr26CNwF84o5JBEImUEbXfY8UU0Oe44IW9//NiilC8W
aeWVWHoFQgIh1sAFGXmUgl2EJ0IYJZRoPmlFmX0EmeWbcMZ50XlGrGAIIF6C6SSZZkqpYpVyBiro
oAYp4EKXY+jJo5+MxtfIKIRGKmmgCihwQ5IklgDJno32qWZwgE4q6qgxwnbonSkkSginn7ba6aJu
kv8q66wD/hCbAJh+mQerrr4aHwP+0SrssOMBcesieKrKK5++MhorsdBGq9o8QiAKponN9kphqNJ2
6+1r6uWqqLZxsFnulOQCB2yb37brLo0e3GItIXBgm2262kH67r78NtUhksleO+a9zPrJbb8IJ/yT
B1wim+q4AxCM73H6KmzxxS19wISl4m66qMSdrnswxiSXzNEJTJwacB71Dgyyr8+aLPPMFtGJjMOq
2vvyqyPT7PPPCf0777KdnnlvzEAnrXSBAH+hq85pFj0xbz0LZtbSWPMLT7yFALCyF1D/+IK5wBlN
dn6XIFfRCVvX4wFs98Qt99x012333W1/MMTbROD/zbffgOcd+OCEF2744YgnrvjijDfu+N6PRy75
5JRX7ne4SqraRtie7sxbKZ10GPhBkJdR+jmnp6766qybbrngr8durN6to1777bjTnrvuu/fu++/u
AC/88METz7vsxid/PBDL2568bBkYwoYfgViAw6acr9l5tmemvUMdxYevOl//IG++3a4z3xPr57fv
/vvw/y1//PTDXf/9jceTgBqougFmG0Qr2PYGCCoQzM9WdOMJ+vr2A/UxsIEPhKAEI+jACUqQDuJb
XwJhlz4LenByHYRG4UIowg+yjYIjzOABTbhCDJawgiw8YQxXaEISNm+GMtTBDXHIQxiqUIc+RGEP
/xdoQyH+0IhIfGESgzi7Gv7NLju8oAZpKEVqHShz1gOgy6ImwKn1CnxMvKBB0IPAIVLxiFFM4xOB
eEY15hB/9ivjEtfYxjnCcR93tKMZ86jHPvpxj28kIhpjd4v9dQ1HWWxZFz3nIgNC0YSk+2MgAVlE
R4SxkpO85CDT6EI24vEIbuwkJt1IR08qbgOfFCU7SLlKJ6ZSkkqUYyx3qMpNZpKWFqylJWfZSl7i
Uoq27KUghXlLTj4GmEPcJTJf+Uhi2lEICejY5rLnKC86CozF7MkYIajLX2qymdnkHSp9yU1xirCb
ywTnKGnXlnCq05nv1Aco52lO0/mDme6sZzx9iP9OcuJzn+uzp0D1SVB45vKg/mQnPwH6z4VmEDLj
fOj4CqpQZRr0nPdMqEZzOFCOXvSbDlVfF1hAgQ+xBxFosAAhtBixc+lHBS39mNictc59RNKUH6Un
TiPKUJvqNKeC8GkO25kJkQp1qA1NKlA7etSfNlWpTn1qT5kq1ahC1ahXrapVtTpVthGVqxgdKViz
ulSymhWrY0XqWSua1raWU55rjSvtoHnSlGpKkRKqwB7gsIfdYMtJ1Cxbj7D5VXDetKCYcKti5Xo1
xjq2k/YIAly9WtTCLjasj53sVi/L2cxS1bM7lehm26qEzpoWiJpFLVtBa1HDsla1p/0KPlOWhJv/
5WmlYJMpb3pRinI9QBQO6iuPRuGLT8CUew8K3VgPy9PXlnW5o41tdKXrXMxOl7rVhW12sQtd7W6X
tN8Nr3i9qzfLbqy203sYS/n0gFx4YgdwCAAfSBDTF/D1AT5RBl9dWl8ufpG8XC0fZJ/bF000d7yt
baxrwdtdBKd2Ew92cIAPDODKJpjCEq7whbvKYA13mLsTDnGGI+zhAttKPQqqnqZyKx9eyEESaUOB
A/TK3xfMoh+VaMAizzYhbpX3LtrcZj99KuCMGvnISE6ykt9K4iU7+cmRVXIPoCziEjdZylT+bJat
jOErExnLYQCzmC3LZS+PWctfRrOZxazmM7N5/81vrrJ1gxfmMTNXwG9D722nuRtTzEIDDxiFFyAg
Cj5UAgG9JYVvUEGKAwR6FpOQRDn24AVU6Naa+UiDaMucSrd02c5pdjOoR01qUZu61KhOtapXzWoD
t/rVsB5nlGO95VDT2ta3zjWWY0BXZGARgPtFxS9sgosJOECgDniFNW7jCTnwwSefeMUaKFGMAfii
DDOQ2HcIw220wlnX4AayKjb87U7jmtPoFne5183udrt73O8md7rjTe962/ve+Fa3u7EAovT+b08u
roQvJFGJVnzDhZLwRCuo0AFSrAEJlUDNLnThglMgoNnY7mtg0eVfkSlwwAVxm4llnW+eFHht4f8O
ecq7vXIyk7zkpCPfuVs+J5nTHIOBubnNd35ynSuY5z4xUNfwYFeP0ecT/bjGLBrQCSlAAOmnaMAl
JrAGBOAiB+iohSmacQkESGIap/DJBPqLabUNZAE+lzPMf4yW1Oxy5D2f99rnTve62/3uKMd7m/UO
d76TODr4sO1tvZcPBJSBHyHAxQPgK+0QDOMYB+DHBAgugbHNhwR6cQQfTlELyxNM02/ve9vPLvfQ
+90xp0676lfP+pm3/vWwj/3p0zO0gZHiGZLgQwia8QcJ/IMWoyiCtCcQgjWMfTc39gc4BFJ5kLkp
oH4PWtyjD3TpT1/21K8+9lMf57xvn+Xa//n/6fnOiCPV9d99kMYAGvAAFEjg6xqAg+9DkDaqa+Da
omhALR7QC8OfQgJIxwkBhC8eRxHQlzdipXZ7933cx4AO+IANGIEWBoEUWIEZRS3uoQ6ZYnS2kAtl
MHF6sAbKkA8TsHnAQg78wH46JAkhcAHFhTqSkQx8IAMxdWk/olwWmIOepoM8mH09CH4/OA1BuBZ1
tmu05293hR0MQHEUAG11UARe5wBpMwu0YApeEAG0cHic8AqkkAu/QA170F40yF9bhC8+tmZzU2Qj
0UACwWRWM4RTVlpZszDXF36NYYf3VCx4ES/VgjPVU4PbwQCEFzG2oB/DkB00Vi6A2BvYsl+9/3EJ
cAB6eNiGaOVIqEV6JpeHmriJcBiEz8ZtXgFzKgeKBgiEb3iHpJiKpjiJqmh9rfiKrYgeV3Apv8Zi
jZA2xNBXZiMhhfgxYNMykegJGlCGZPcxx7AmvYg0DrE3zrYGldAXnPAIovdwosiKbmeN3keNsJiJ
nIiNbIF6aueK1+iNwpCN3Gh6o+d621iN59iN7riGL9eO47iD4pdYPhhJDeMHiLQqAwMMvoALtdBb
aKJIuwhfA9CIaeAAvsBXJbhs9FGQ6pcdqNABAukEVaMQaQhBz4Z29qiNhyAO4lCO9ViKnThGlFiS
7/gcTOFtI1kzOEaOGImJ9KiSKfkQcRN4fv+4OVQjEChgX32gcf51kFxECi+GGy6WdZ4XWHtFCw6g
X2ZXk+lYPhL0ifBoF2o4jafYfVC5iiSpNO5QhFs5h8s4kiUVTTnJKfjVD7zAe3EwCrOAAKiQi+VC
DLNACg2wdcFXBJfAfqTQC8cwDAH4AJcwf3DJf17gkw+AAL0wJmkAAb3QCw9gCgigAZ0gXJIoEQvg
SM/YhFOhb1fZDR0JdB8Ak2EZlVkZc6i5jl05lqg4k6SZmq9ZmrE5my8xE6qpFLa5jkd4BuoVUxrw
jP1wAblQB83wCV8nCb6gYw3wDG6TeL8gAbZADOQAnKIADBinB6nQCgNHA1+HeGMnY1uQA6n/EAGN
EAXK5gvFdwCelw9wkA7YSEaftAxd4A16wQTwsJGppRchyY5t2J+y+Z/xuJrz2JqwKZq0KZbNkWdD
N3hjQnU2QQuvIAokEXb8sAvEcHX8cArAqQuj4KCi0IKvcGzN+AuikHupkAzPmAoNkAsvVqJ0wAuE
JmCSoJ68EYl1yHMkZZ8ltQ/XQA0VkG2sAAMwAFMdAAOsMAPDoAIH0J3q2I0G4DUYEAkAkAREWABs
4B6LIARwRTqxoCCDcAUyKRA4qaX/UA3TcwaFcA83GqYAKqDiuKXBcJvmKKfy2KZ2Cqd0mqcH6pJr
6i9c0z8C85N1UIL94AkNwAt68QzFgKiZ/2B4QKCojPoJCGAN/PAJDIACyeAJqIBxEjAKvmd8e/Cp
z4Bj+tcK/BADpHB1NPANUucEJgI6a2NAlSBBPuoFB9CCvvCjqXABotABPxp5J4mHP1AAK+A0b2AD
aFWsbjClzDhrVllUUGAJKWACc2A1yioAQZcANpACbKCn5Dia9/iSp0kVuelyQWWa/umR6IqnqgkY
7FqVXHmnOSevbjqnBTqvLsGHc3CWFdkLNTFpEcAHv+AgXnANSHBwyUAMa5IMdKALAfBw4OCwCQcG
HYBjwDB1/wAOOoYKNSEPQJBwugAOzPMKw7aQxTEy7uqZUrkMs6oA4lAJRnqrNdEKFcAKlP9AUg4g
A0pKlVMAVZ/pD1iAKW8AAmBZDxhgBkOLlTYRA3hSAOK6F84ACG/AAQW2CF8wCPQaik0woFnbkl3r
rX0qgUTIpq5Ztl9br8HJmmF7tph5oLJYeysCDFu7eAFroce4C5VaoXmJt5+wcIn1da5wAKVAAqf6
hJEXAn5Zsf3AsJtHcJwQAiTAt3zyfEq7iVggq/8yCazQCS3YIQEwAzNws892AEpKBQ4EcpT1d2yQ
Xk5bYFdwtEmbujcBmmMBBSNCpUQYPAVgAF9WCGbQrWjLtu86vMJbvGkbjsdrbsRrvMvLvFz7jWC5
tvEavM4Lb+k6tnGnoHdAIsrSjwIxC6H/+nuKVg5h9wqb15MNcLOeIAF6AZB0aWmEWwnAEAcagHjK
aUkMSwuBVgwSAJdX9wmLaCG6uQ0sWwWONAOEm6Oe8KMuwAnNCQMkQLTNim4ugKbTUwhssaBGQK4p
SQEMkqbfdid2oLLNKL33mrLXO34oSbbTq8L8+Y5DIIQyPMPrGqCoVJstTMIZ5QwHMgh2gAa6Ugr7
xQC1UBOLhwq5dwol+mxIbGguKgef0ADHVg8O+5yXEL/BF7CUcL9qGY2i0LepIAm/xQfvxYhP2aRM
o6OVgg8qEAEbo6aecKQUGhsyULESHFJJNQ8YwD/RgKyr1A9MsMe/Ow8k0SGJoA/6Ix0F/xYPJoAH
YJqRdMCmhvC7OEEFtEEE5loFHCALEIbGLKxz+FmnB7jCzQu9khXJpYzKpEyv91m9weq8BpSP+8gp
tcAJrrB4A4AAootjEnCplCqV+vAJo1AMLGoTioZfqfCEXpcMltYA0mgLDlpnv9AA1xYB6zlcJpzG
0mHAVTALovA3AaACFaDI21AJB0AOmik3X1VKYtWE8XAGV2QG+LBLVtoCSMu7x8Q/C/IGgyAAMVAT
S4AHdpABtXWsbNC6TMAgb5AAVXu1RsYBC+LDzGDIh0wQznADapDNnTy7MLy1XmvCKFzDqdzRogxu
Iqe8I/bRHDzSJd3SLm2aIU2BZbmg3f/LIwiAAMHlBA8QAZ4QAYp2DNRw0yTwfv8QAChSDgDYCs03
CoAWMenQ1GNDChogDf2ncMKosIfKlmVIWKJsAEYAeF59BNdQUl8NAuE8zjfLCIY2CeeRkXXzaVkA
CBkwCdFgBlSLRwgCAl/wBi5QyAJQAwedAxXsB8hwF7cwIleqBumBtNUqBshAInftEwDg0K08yPtw
JzegrgZ2KFRK0Sytd29TEwhFYLJVyMh7dwvomZ59XROo2tLo2iAdriv90q5M25xYSDSdhPThiGQH
iernAJ+ACg3goAfAI4IoiIqYlLrYi8sNLLqIIhEDNWcou4ViIDFQcfNQA0tiDhzABAv/XAHGMAST
vSR3YEDy89d4wAYo5AIkYtkJcNDbUKxvMM9RmwBU0MhLwDbbewbwSQGWgAfurN0grDctILV3INjb
GtmXTdnsMAiGcBMcYAYHThIFMCJmwJkVLbamjI60m7IdfmUYjsl+AUthcceX+NmqrMqrXeIcVg/n
QY39/eIvzs75OeI03LPwmrpsKNqvreM9fsofiXY7vuHoCNsaDrZb+ZVFbttVhWJE173CxZ7+ZQXD
YHVhDKFPSIbS8JMrcs3JHZTysW08buM17AK8Gw/4kObsjbTcDQXfbQyTMN7pTTcOPAckwt/Xrbvn
gA+KYOEGxOdzHeFTOw8QbZ97DT1K/6Lec+O7ChAvfW7fssW0lC0id+0BOWC16v0IcR0Lz4relq4K
QiAiioDhKUx3OnoBw9RJMU7jo6xOBexT9klW/6zOouNK+WTppxRMMXDHlogEhmxERI7SKf1k4KrZ
MP2zKI7a7HokemYGKmV0XD7lwlEKhKc91r5j184dBVjr1iviK84P0vEEZi4Eaa6Pv9vdcTwD4e01
jbwCBuwN+b0ETBDhVyuLzqOtjTzq9TAHaKoP2goKAEABCdI3+M0wfR7wi07YLMDniJAB5q2meIII
G5PgpwpKk4wBoJQBimAJT7A/0XS7/9LesV56SHbq/f1EvN7Wtb7rHKRZ4UnragrzGv/V1yg/TyOP
QqoO4ziuzjLu6zMOTnV0SfGemahz8y3fDW+9Wm5N5giYWaFtbg9PWb1u9PAW82jY7Rzttb9O0ka+
bqbLQDxsCbw5yzYoWIeYftFOQNj+G5LIho4kP+QzB+S+u1/93j5sDrXx5oFnA3wvAGtc1tEa0RNU
92fgw4Pw7vCMHmeKD2iKHofOAglAIu6e8JwetXb9cGS0z2aOJ5Nv8ZT9BO3u4A4OCHuMSktQ+JnO
D2SevH/3Vkj/885TTiafQtwc60bPR18/W81Uzg8v7/lD+7EvPrav+3jT2oyw8h6k8znqVa/fSzng
+s44FR+O86rv894u7A12/QsWYDz/f2QxYOMbJEcmdbRFZ4vZ3uVlT4zov/aLhE3HdDcH0QLMnua7
K/cevw7pDuewQP/OABD3hNS7ZYCCESwJtcVDOLAKBYEF8DlMAIgDEyN3aryJdMbhxmpMMihStOJh
DEb05tixRKTimwwQWdQbgiGaHRfa7HyJpOMHRI2DQFxIYqKQAQVLCmK5xwQaCx0IQ90Dws7dVahT
4X0g0tXRzxNeaYid1xTs0JMX0K4x2xYlCJlx4c5MScWu3LJO026j+rbvTLpuaS6bSy9w4TI0K4nV
ulYvVr6CJW/Be9AyRsqZH2uuvBmxh8mhY0B+2lk02dOFPXtdAjqr47CxDVsFu+rs/2thXPftPqKP
92/gvoUjIT6cX/A1q5TTGS6zC3DXf2dzUisddIYkK+y84UIIRxs4cKLk22GFvPnz5Ys4Wf+nUXow
6qWgh9++fnr68vHrL9IJiWmfmvpnQAL5maihAnJKACJYJvKkAlbayUIhBPPCp7UWGtqGwqUgWiqD
MwRIKok3zChRKSy+wGPClUww5DIPYjGjJSHw8EMN3ZzaySMXbIzlLwAQOQMiNXi6raqxhpCtrUx6
s66xHKNT7b/DntzLynMiU0zK1DbMcjrCTENLTNIAU21LALW8UrUO/RrNy4QGw5DNu7rsijMAk6zT
TCytO1NJO4/k0k8mpdOTNh/+Ov9k0Q2ag9JQRZdsFDo+KU2LtjAHlfKWBBIw5Iwv3CADkja8GO+F
/U7lT7wB2ItP1VTpc88+/mCNw9b3zquj0s4KFG6iJIQ4EFhiH2SFGYmGbbOgORZC8aALUTxQwTdE
VArUGZcy0RkjQHSxQ2du+CKJnExsCUN8QDXJCESMyjQohRAxSTIqS2OrzEDJ/HJNOAlCSK+8+J1z
qX8FBo0hOQHGM98997UzqZOgiFjMDAOuEF2UCFbYYhhb0zRgqgoi1OGEFxD00jf/zJThj/XV9NEA
ZZsUK+YAPXTMz7oBdOGtGOZ0jk/NQINUQkyN9b6j56M16VddxZVpXafs1Vd+EhD/VohOiWUGlpxe
qUAFYyjpllOkuL1aWGmlPVvsYfG4KCc2TGy2XJiYCvINiXNiIu5kVVwkbywqquEifEi6oWcK7q6O
DTY83qqFxuW6+eW6Wt4ZnpU5g9iDzfF0aB7APXz2smgLHt2gpibpGDPW41lddDUdN/jghcEFPUHT
n2hd736d5b1zF6DVvBuN5zKdZ5YNm3h5y2ffOPk+pZb8ZOpXIxn6y1NSAB8O7thp6O9KaXppqMtH
+tbz09dv1+tDr6Iff+DPrWqriR1WFm+sUaaCYVoJYft14E9BetNd8JTFjCospHRKKQQe7nAgDrjB
BhJRitySIgAVWYtuCSHRRi7y/wMFwAt0ZnCXQbBgkyFhRA0sGQKG7vAifFVJcp0zXu82xBSk5LBi
cYLYhablwx0KT1lpYwTCgDihIwLvIUm8ocgWGMQe0mNgx6OdDpP4xJxkkYe/MyATx0awApZti6y7
BxdxaJfP7c6EZGQj6JQixdQtkXmq81jx5giXzIHsetjjyvQatkeXdSw2Y6QY97Rjgu6QyhStWs/T
Gok+8+VHfY+cpNJU8AdTcEKNu6OagYBVv6wx4xUd6IQKWMGKTlBDGbtgW7C0ZjZXKrB+ZwNau/yw
iEhMhCVQ8JRNvnCGOyxIO/IqQCzURS1gsqEQsRimiISALY5MJEh+EwAVjHCTWP88oXuDwF0biffH
vFQleHe03Rl/2EXhkY6AWNykxIZoRMsky4un0+HE3tnEdhKMgG7k54hAcM4rMkSf8VQhQLV4UM5t
YXsIheIbqUjD33Vunvfklhlj91DZ0RGOz1MZ4vioRxni0Zvui9jFcAisSCwCEIhsQziwIQIvxHQP
Mo1pTWlqU5ze1Gim4mlPd6rTmw4AqJ0whReAUYxrwI4wSHRjJ2epBghioR0TgEAHrHpVZbSCG5TA
jiw/maGogjVrxUQG4275wCcA4Cj4WAEGynoGt6pwENuB6w1YlJ3t4bJduVSKp16oTA4A7YWDvZAA
4iYkNhTgn9b8l8425k58CjT/nU6kwBzkqEDXvfOIWtRsZ2HJ2YCacbN2ZKrIhsfOyO6zYBMdYixF
e08rqnahKqRjRWerz5AMVLI7xCJoCbIA0k72spihoD/9pTbU2tCiCVvj65RbxYwS0p6UBWuclOq7
+qk0VKMiADa8a4E8tCG84yWveMtr3vMSQr3eaWl614te9473JitAljcmAYt3QJahCTlO/OT31bFi
B6oDJrDuYikLqz3VGVc4oIK72lpYPnht9ZuEgivL4AFSkMH2S7BB86k2hVY3ohrjHdtMGuH7IkhZ
wNKigxWkYQ5R+IcrVjDZWttF+9kYsxBe54xpjDbclrjHYLzCQbg6YSSbbcgw/xZjsio8Ws+auIew
De0TRhxE30JMtuhMoJZh59w4QrekmTIgl5P7uW5ClsU+NmTQapCNEqyXAHMGbyAgUedAjIEMeuZz
n+csKj/TGdB53jOh/4znQOeBAFzwmyFuwI0MjGhCr51IE3oQhgIF+KuUQNaBJXzgFnSqwwbudGF/
Js9QjrqAVwClgKWMjwXBepaB07EJW6zQWOeYuMcbp/ugLNtpLXmdMkZ1NZ7waV7+2JWzNnCxuZrg
BTE7yTve7b+U7GWxhZjaQF7wpAGM4FcmS9XaCNa4tfZigRb7tsEWMRhPzKEre9jJmBU2U41RUGB3
OVpWpogbNYuxKcM74CdN2/+Ksa2x4maYzcVmdVsBYUtGL9oNKQgVxVPAaItnXOOIrAHHMU7CioP8
4iHf+MhNEA2UV9wOGAgsucL4WYP3N9OyLoC5C/xJaEd62a6+OcO7zeFjD7hswXrxHDS9cx0jneYG
76eQa71tg8Mc6gxudTWWXHN129zBWic3raNu6uJmnbXpzq2EObhwdIdVAcSmuldh7fUNSzvSSLyx
0K8N2iO7++sD5PtA+A10vSO32cqmYLwp2uvrCvxZ6158ORuP9q6fXbV4h3X3UmqDEqmDJL/cPCg6
byPQh/7zmKfrdkhSItOjXvWpR4ToXe9WSzw67byl/fxmXnUBg1v3Ovd5gEX/7WlW4/z3Rg811nLf
dt1nNgk11rAAi+5DAlsx7pLm9hez8HfOVhjHTg5gjnFnbN6TW9W5FlbOAU/4n48a3ern+c8Vnn6x
wz/ZEon2/Zj/9qDf3wWu7bbNyTY3AEK/6Oum6du7YyO7+CuteFC6eYs8qxmyvjJA6budtQnA67M6
prNAw6suFDO2+cM/rjM7EGSmFShBAMCAM1AHxlnBQWBBF0TBFsSlE/yUGISrGaTBG8zBGtTBRUDB
lDpBo0uCcdgwrqGl++Eq+bk9NQvC42vC/qM53Es1EISFVJO7C7S/4ZPCnhM+nAPBrcO/4gs2T8u7
r5M/8ou7LFQD8xPCKcSs/zW0Owj6tCg8wiGcwwOUpza8Q6abPeRTQ/oDQ0DEuigcPu/7tjwswwOJ
tRY7v6LrseC7QDsUQLqjw0L0uQrksBgrw1biwzC0RGZJRDBkvvtLgJa7gWWyvMASAKNTxVVEBqBR
g0hYxTnIJlY0ulhkxVuERTXMJvqKhVrMwlQkRVkEtQi7P6eCwloUP6obxDp0Qi3MufHzQyZMw01j
Q2Z8Rj9MQFD0QG58xPzzqjcMR2c0xLmzw1CCwuCLRmXMP2hsv0hciPs7oC48Ov8TRHRcPny8xysM
t/b7tnMMOn30PVdbx3sDSC9sFgYMxKUzSE+EwwDjRDyEAnhEP3jTww60Mv8rfMLyq8Sti0ZppMcM
KMV8FEKPJET6K8lxnMaUtEeN/MdjO0ZnFEeQDMjdQ8NsTMibXEiVtElKFLeP3MZxM8d53EfxO7uG
XEeaBMg3xL8G4UeZXEio/D2dDMqp7MOIvMp0DEGf7ESebEepHEqs40igFMuxTER1zMmyrEeu5MKT
vDuXO7CC5ECF3D+5TLFuHMWvckqJDEu+REt73EmwHEfjQ0qdTEqUZMireceakzkCWUmrHEmltEiB
XEaGREiSpMaZnExIPMuD9D687EL7C8SdxEahdDBydMLO/MOtjEqm7EvJBMprJE2qjEnDJMaeZM3Y
7EfarErUlDsGNMnCHMz/3jy/m7u6u8RE3ES2/cO9zWxN3iRGl0TMexRIWqPLs9k3BKw38MM56xzO
6WxLPaAayJTMr2RO1TTN3exIlszMzIRNmUxDj/zO1qxH8+TM9nTN9fRJrxzPsyxPnks7TZvIvHRO
/9TLAstNbExK55zP5XyyHes06iTPRjxP/FRIuCxHBf3GU7NIosTDM6xE4pzCA11KoKvP56xQeRw2
KGxQfIzOcktHz9TP+CSWJPyvmPTP/TRJCDXHKqTQG4VEf2xOCC1R2/RN9JSl2WxRzJzMF83Rn7zJ
w/xLBA3MJu1P4+vRrhJH+WTP8CPPnvvCKOUAHxVMVmPOUstSwIO25YQ//ygtTQ7dRoqMOs1sNbqk
MMvc0tTUQiB9zI1sS3asTXkE0am7qytkUn4ETf/qJEVdVEZtVEf1AcZMhEhVgke9tEq9VEub1BGY
Bk7V1EmBVGHAVE8V1VBdDlLNgVE91VTNVFUF1fBs1URlFFiV1FmtVVu9VVx1VVNF1VXtVR7QVXsA
1lwVBFYdVk3Y1WL1VWNdVmaN1WatUWV91miV1lLl1WmlVmzN1q+QOSS5Vm311m8NVzEAV3EtV3M9
V3QtjnRdV3JlV2h1V3iNV0eQV3qtV+NAVkp9V3vN133tV3/91Hb914AVWIId14GlVW4t2F/VAoZd
WIcl1mDYBIRtWIo91v+KxbQTuFhhPViI3Y1NnViQtVSM/dhO7ViLnQKDFVl+JVn5yYqTNVmU1ViO
fdWYZdlgfdmaLdmbldmVnVl9VdifBVqhxdehzVlsjaChQaQpSIGHLVpVfZ+GxYKmBVidVVlFTQFo
4Nmp3dSLwwFRSYJftZGIRdmNMFousNmlvdex1Vqj7S+shVm43dogkFQ0iFunvduNxdug1Vt5rVvf
cIOcfVu0pVmqHdy2tVu5ldjEPdzF7VjAJRC/nde5nVULYNudBdnHXY6hkVXwulweCATjMIF9GIOe
RdwNiNxk1QLSnVzLTd1GOVvTNVy+nV3arV1xXV0lIIOHpYrWLVx7+IH/xvXcUHiEDxjZATkH17Xa
0r1U2EXZ5p1a4I3b6KUaplXcekhed2ha3A1WLrABDjiOtqrcwM1ZEFBd4Q1emQXdkI1dXUVdqrkH
6EVf251f+pXc+m1U3VWO7h0DAAhP0C2KQOhfYpWgPbMDNEADpjCBPcMRDCADE7gFTTiDPDOCIMAA
8RLgA36zVQDgKzBYvw0HSyPdBg4HwHWGPBPgsA2EBz5dFV6U9TKEMAiEcLADlF0E9VKAMDDgMaiG
hV1dUOCcPKuBIBBdSH3eNuACRjsBv8WbX40gHFBfVnVfwCUEFz4HLgACULAANHgCPSADPHDgRTlb
v1XgQADbTS2BX0Xj/88NAtINhCzugSQ4YLB9XEQq432QYAS+4zyjgJjN4Mc94gDmAWcYOYatMwrO
gCBmgvtdZEbe20aO3C3u4og92xWeA0tdYWhoA7JNLK61hyt2iRhGWUs2BDQohLNVqxFogS+YAvXN
XI8QAEI4CkQYAQ4gBADQZDPYhg0ohE+t3A2xgwLYgMIJ5WCg4hHYXthVpgHuVJbrB8B9XDUYhMwV
3zQekCuAKzVQguaNBwJmDpKF4gGh5GMT4gAWgGVe4WdahbOtXE4OhepVApiYA1tuAUD4B9J1A17W
AzruARHu4CaoXNIVCH3wZXx41UrGgDDQ5A9Y5+MAmhGg4iEwg0OAXf8KAAX5bWSMzmifLVfcJeJV
PmZhMIAstoRMhWSuReI6VoIiIQNFhmU3gAfzNV/cBV3wUuCPLYCdKApAyABJPl06qAczsACezlc1
IAQuWILtcpHc9d8z/ubjmOb+ogJQKACEzlylDc/M5Qdj1gHQjYZrblhFQN7tDeeT9ukOsmQtzte6
pWbdrVusTQEtFpWbzultWeNPfujuEGN+mDiUngIBQOJ5aIJaPmqIbd62VoWHBmnn1Wbu1eq8RmHs
1ehqlWzK/tf8VWz1rdxZtofLvmzY5QIAMOPQXoEpqGhV5IF7eOZ/KBJBaGYM0motOAFz1oFCvmK5
hu1/oGIaXoWt5h7/PUBqTuaUvb60Vh4OIsYALv4HIubaFfEB2PWUXBYEE3bqqulfNDCD5D5dXd7q
Ho7ZjDXmUeFUN2hngHYBJMDppUaDDwjtTc3niDVqexbfKW7Y6u1f0cWl9q6Gc8nl/qUHica0L/AQ
657Xe27oqg5hVVBfXoLs3q1sB39w+z3XzB3jxdYICziDhf3vBHfeDxCaMeDlLDaKvUZiZ+ABaQ5q
g3izEjmEogZtmbOIXbboEWCDPUsBtTJqDM9wEoeHijMAdcbqM34DjzXg0XizMeDj/rLhS5uHMUhi
IzBqUMjqI564qpBnVjUAQiPplJVi3wAmSnXrA6Zgvv5s86UABcZnyVbliNbOcbfO1L92cTpG4jvY
ABMhgzn+2h6uuKf4b929BVER8sRugrotAAtApALQ5IuGcEVfdMoOkdUehI1mWJdN2Gid3n5whnpm
3OVl1RXa9ORtcE1HbNbNWz0Agt2VXUrf2MtO9EhndFd/9dnFYiSm7U+3vclmdfY1XnWdAlCBYFCv
ViL33az1ZrWtdFQP9ar19PPV1KxuV13GdVhv9WifdmUfdcXFVPh1ZG2X9mJ31mrHdmr39nDn9nEv
d3Pf9lg/d3Vf94INCAA7
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=tmobilespace.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=tmobilespace.gif
Content-ID: <tmobilespace.gif>
R0lGODlhAQABAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwMDcwKbK8AAAMzMAADMAMwAzMxYW
FhwcHCIiIikpKVVVVU1NTUJCQjk5Of98gP9QUNYAk8zs/+/Wxufn1q2pkDP/AGYAAJkAAMwAAAAz
ADMzAGYzAJkzAMwzAP8zAABmADNmAGZmAJlmAMxmAP9mAACZADOZAGaZAJmZAMyZAP+ZAADMADPM
AGbMAJnMAMzMAP/MAGb/AJn/AMz/AAD/MzMA/2YAM5kAM8wAM/8AMwAz/zMzM2YzM5kzM8wzM/8z
MwBmMzNmM2ZmM5lmM8xmM/9mMwCZMzOZM2aZM5mZM8yZM/+ZMwDMMzPMM2bMM5nMM8zMM//MMzP/
M2b/M5n/M8z/M///MwAAZjMAZmYAZpkAZswAZv8AZgAzZjMzZmYzZpkzZswzZv8zZgBmZjNmZmZm
ZplmZsxmZgCZZjOZZmaZZpmZZsyZZv+ZZgDMZjPMZpnMZszMZv/MZgD/ZjP/Zpn/Zsz/Zv8AzMwA
/wCZmZkzmZkAmcwAmQAAmTMzmWYAmcwzmf8AmQBmmTNmmWYzmZlmmcxmmf8zmTOZmWaZmZmZmcyZ
mf+ZmQDMmTPMmWbMZpnMmczMmf/MmQD/mTP/mWbMmZn/mcz/mf//mQAAzDMAmWYAzJkAzMwAzAAz
mTMzzGYzzJkzzMwzzP8zzABmzDNmzGZmmZlmzMxmzP9mmQCZzDOZzGaZzJmZzMyZzP+ZzADMzDPM
zGbMzJnMzMzMzP/MzAD/zDP/zGb/mZn/zMz/zP//zDMAzGYA/5kA/wAzzDMz/2Yz/5kz/8wz//8z
/wBm/zNm/2ZmzJlm/8xm//9mzACZ/zOZ/2aZ/5mZ/8yZ//+Z/wDM/zPM/2bM/5nM/8zM///M/zP/
/2b/zJn//8z///9mZmb/Zv//ZmZm//9m/2b//6UAIV9fX3d3d4aGhpaWlsvLy7KystfX193d3ePj
4+rq6vHx8fj4+P/78KCgpICAgP8AAAD/AP//AAAA//8A/wD//////ywAAAAAAQABAAAIBAD/BQQA
Ow==
------=_Part_1240237_22156211.1212802713213--

12
test/fixtures/sample_sms.txt vendored Normal file
View file

@ -0,0 +1,12 @@
Return-Path: <5555555555@tmomail.net>
Date: Tue, 3 Jun 2008 23:11:26 -0400
From: 5555555555@tmomail.net
To: gtd@tracks.com
Message-ID: <6100602.65827251212549086388.JavaMail.imb@mgwatl02.cns.mms.com>
Subject:
MIME-Version: 1.0
Content-Type: text/plain;charset=utf-8
Content-Transfer-Encoding: 7bit
Importance: Normal
message_content

View file

@ -28,3 +28,13 @@ ldap_user:
first_name: John
last_name: Deere
auth_type: ldap
sms_user:
id: 4
login: sms_user
crypted_password: <%= Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--") %>
token: <%= Digest::SHA1.hexdigest("sms_userSun Feb 19 14:42:45 GMT 20060.408173979260027") %>
is_admin: false
first_name: SMS
last_name: Tester
auth_type: database

View file

@ -25,7 +25,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
assert_equal 1, assigns['deferred'].size
t = p.not_done_todos[0]
t.show_from = 1.days.from_now.utc.to_date
t.show_from = 1.days.from_now.utc
t.save!
get :show, :id => p.to_param

View file

@ -114,7 +114,7 @@ class RecurringTodosControllerTest < ActionController::TestCase
new_todo = Todo.find_by_recurring_todo_id 5
# due date should be the target_date
assert_equal Time.utc(target_date.year, target_date.month, target_date.day), new_todo.due
assert_equal users(:admin_user).at_midnight(Date.new(target_date.year, target_date.month, target_date.day)), new_todo.due
# show_from should be nil since now+4.days-10.days is in the past
assert_equal nil, new_todo.show_from

View file

@ -5,7 +5,7 @@ require 'todos_controller'
class TodosController; def rescue_action(e) raise e end; end
class TodosControllerTest < Test::Rails::TestCase
fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings
fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos
def setup
@controller = TodosController.new
@ -137,7 +137,7 @@ class TodosControllerTest < Test::Rails::TestCase
assert_equal "Call Warren Buffet to find out how much he makes per day", t.description
assert_equal "foo, bar", t.tag_list
expected = Date.new(2006,11,30)
actual = t.due
actual = t.due.to_date
assert_equal expected, actual, "Expected #{expected.to_s(:db)}, was #{actual.to_s(:db)}"
end
@ -328,7 +328,7 @@ class TodosControllerTest < Test::Rails::TestCase
assert t.active?
assert_equal 'test notes', t.notes
assert_nil t.show_from
assert_equal Date.new(2007,1,2).to_s, t.due.to_s
assert_equal Date.new(2007,1,2), t.due.to_date
end
def test_mobile_create_action_redirects_to_mobile_home_page_when_successful
@ -374,8 +374,26 @@ class TodosControllerTest < Test::Rails::TestCase
assert todo_1.completed?
# check there is a new todo linked to the recurring pattern
next_todo = Todo.find_by_recurring_todo_id(recurring_todo_1.id)
next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'})
assert_equal "Call Bill Gates every day", next_todo.description
# change recurrence pattern to weekly and set show_from 2 days befor due date
# this forces the next todo to be put in the tickler
recurring_todo_1.show_from_delta = 2
recurring_todo_1.recurring_period = 'weekly'
recurring_todo_1.every_day = 'smtwtfs'
recurring_todo_1.save
# mark next_todo as complete by toggle_check
xhr :post, :toggle_check, :id => next_todo.id, :_source_view => 'todo'
next_todo.reload
assert next_todo.completed?
# check there is a new todo linked to the recurring pattern in the tickler
next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'})
assert_equal "Call Bill Gates every day", next_todo.description
# check that the todo is in the tickler
assert !next_todo.show_from.nil?
end
end

View file

@ -31,7 +31,7 @@ class UsersControllerTest < Test::Rails::TestCase
get :index
assert_response :success
assert_equal "TRACKS::Manage Users", assigns['page_title']
assert_equal 3, assigns['total_users']
assert_equal 4, assigns['total_users']
assert_equal "/users", session['return-to']
end

View file

@ -78,7 +78,7 @@ class UsersXmlApiTest < ActionController::IntegrationTest
get '/users.xml', {}, basic_auth_headers()
assert_response :success
assert_tag :tag => "users",
:children => { :count => 3, :only => { :tag => "user" } }
:children => { :count => 4, :only => { :tag => "user" } }
assert_no_tag :tag => "password"
end

View file

@ -9,7 +9,7 @@ assert_element_present "todo_9"
# add new action to existing context
type "todo_description", "a new action"
type "todo_context_name", "Agenda"
type "todo_context_name", "agenda"
click "css=#todo-form-new-action .submit_box button"
wait_for_visible "flash"

View file

@ -0,0 +1,10 @@
setup :fixtures => :all
login :as => 'admin'
open "/"
click "edit_icon_todo_5"
wait_for_element_present "show_from_todo_5"
type "show_from_todo_5", "1/1/2030"
click "css=#submit_todo_5"
wait_for_element_not_present "todo_5"
assert_text 'badge_count', '9'
wait_for_not_visible "c5"

View file

@ -0,0 +1,8 @@
setup :fixtures => :all
login :as => 'admin'
open "/"
wait_for_element_present '//div[@id="line_todo_5"]//img[@alt="Defer_1"]/..'
click '//div[@id="line_todo_5"]//img[@alt="Defer_1"]/..'
wait_for_element_not_present "todo_5"
assert_text 'badge_count', '9'
wait_for_not_visible "c5"

View file

@ -2,4 +2,4 @@ setup :fixtures => :all
login :as => 'admin'
open '/'
click "xpath=//div[@id='c1'] //div[@id='todo_9'] //input[@class='item-checkbox']"
wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_9']"
wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_9']"

View file

@ -2,5 +2,5 @@ setup :fixtures => :all
login :as => 'admin'
open '/'
click "xpath=//div[@id='c5'] //div[@id='todo_5'] //input[@class='item-checkbox']"
wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_5']"
wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_5']"
wait_for_not_visible 'c5'

View file

@ -1,6 +1,6 @@
setup :fixtures => :all
login :as => 'admin'
open '/'
click "xpath=//div[@id='completed'] //div[@id='todo_3'] //input[@class='item-checkbox']"
click "xpath=//div[@id='completed_container'] //div[@id='todo_3'] //input[@class='item-checkbox']"
wait_for_element_present "xpath=//div[@id='c4'] //div[@id='todo_3']"
assert_not_visible "c4empty-nd"

View file

@ -4,7 +4,7 @@ login :as => 'admin'
open '/m'
wait_for_text 'css=h1 span.count', '10'
click_and_wait "link=Add new action"
click_and_wait "link=0-Add new action"
type "todo_notes", "test notes"
type "todo_description", "test name"

View file

@ -7,7 +7,7 @@ wait_for_title "All actions"
wait_for_text 'css=h1 span.count', '10'
# open context page
click_and_wait "link=Contexts"
click_and_wait "link=2-Contexts"
# verify_title "All actions in context agenda"
# choose agenda context
click_and_wait "link=agenda"
@ -18,7 +18,7 @@ click_and_wait "link=foo"
verify_title "TRACKS::Tagged with 'foo'"
wait_for_text 'css=h1 span.count', '2'
click_and_wait "link=Projects"
click_and_wait "link=3-Projects"
wait_for_text 'css=h1 span.count', '3'
click_and_wait "link=Build a working time machine"
wait_for_text 'css=h1 span.count', '3'

View file

@ -3,5 +3,5 @@ login :as => 'admin'
open "/projects/1"
include_partial 'project_detail/add_deferred_todo'
click "xpath=//div[@id='tickler'] //div[@id='todo_15'] //input[@class='item-checkbox']"
wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_15']"
wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_15']"
assert_not_visible "tickler-empty-nd"

View file

@ -11,6 +11,6 @@ assert_text 'badge_count', '1'
# mark one complete
click "xpath=//div[@id='c1'] //div[@id='todo_1'] //input[@class='item-checkbox']"
wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_1']"
wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_1']"
assert_text 'badge_count', '0'

View file

@ -71,7 +71,7 @@ class Test::Rails::TestCase < Test::Unit::TestCase
end
def next_week
1.week.from_now.utc.to_date
1.week.from_now.utc
end
# Courtesy of http://habtm.com/articles/2006/02/20/assert-yourself-man-redirecting-with-rjs

View file

@ -0,0 +1,76 @@
require File.dirname(__FILE__) + '/../test_helper'
class MessageGatewayTest < Test::Rails::TestCase
fixtures :users, :contexts
def setup
@user = users(:sms_user)
@inbox = contexts(:inbox)
end
def load_message(filename)
MessageGateway.receive(File.read(File.join(RAILS_ROOT, 'test', 'fixtures', filename)))
end
def test_sms_with_no_subject
todo_count = Todo.count
load_message('sample_sms.txt')
# assert some stuff about it being created
assert_equal(todo_count+1, Todo.count)
message_todo = Todo.find(:first, :conditions => {:description => "message_content"})
assert_not_nil(message_todo)
assert_equal(@inbox, message_todo.context)
assert_equal(@user, message_todo.user)
end
def test_double_sms
todo_count = Todo.count
load_message('sample_sms.txt')
load_message('sample_sms.txt')
assert_equal(todo_count+1, Todo.count)
end
def test_mms_with_subject
todo_count = Todo.count
load_message('sample_mms.txt')
# assert some stuff about it being created
assert_equal(todo_count+1, Todo.count)
message_todo = Todo.find(:first, :conditions => {:description => "This is the subject"})
assert_not_nil(message_todo)
assert_equal(@inbox, message_todo.context)
assert_equal(@user, message_todo.user)
assert_equal("This is the message body", message_todo.notes)
end
def test_no_user
todo_count = Todo.count
badmessage = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt'))
badmessage.gsub!("5555555555", "notauser")
MessageGateway.receive(badmessage)
assert_equal(todo_count, Todo.count)
end
def test_direct_to_context
message = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt'))
valid_context_msg = message.gsub('message_content', 'this is a task @ anothercontext')
invalid_context_msg = message.gsub('message_content', 'this is also a task @ notacontext')
MessageGateway.receive(valid_context_msg)
valid_context_todo = Todo.find(:first, :conditions => {:description => "this is a task"})
assert_not_nil(valid_context_todo)
assert_equal(contexts(:anothercontext), valid_context_todo.context)
MessageGateway.receive(invalid_context_msg)
invalid_context_todo = Todo.find(:first, :conditions => {:description => 'this is also a task'})
assert_not_nil(invalid_context_todo)
assert_equal(@inbox, invalid_context_todo.context)
end
end

View file

@ -20,7 +20,7 @@ class PreferenceTest < Test::Rails::TestCase
end
def test_parse_date
assert_equal Date.new(2007, 5, 20).to_s, @admin_user.preference.parse_date('20/5/2007').to_s
assert_equal @admin_user.at_midnight(Date.new(2007, 5, 20)).to_s, @admin_user.preference.parse_date('20/5/2007').to_s
end
def test_parse_date_returns_nil_if_string_is_empty

View file

@ -118,7 +118,7 @@ class ProjectTest < Test::Rails::TestCase
def test_deferred_todos
assert_equal 1, @timemachine.deferred_todos.size
t = @timemachine.not_done_todos[0]
t.show_from = 1.days.from_now.utc.to_date
t.show_from = 1.days.from_now.utc
t.save!
assert_equal 2, Project.find(@timemachine.id).deferred_todos.size
end

View file

@ -15,13 +15,13 @@ class RecurringTodoTest < Test::Rails::TestCase
@in_three_days = Time.now.utc + 3.days
@in_four_days = @in_three_days + 1.day # need a day after start_from
@friday = Time.utc(2008,6,6)
@saturday = Time.utc(2008,6,7)
@sunday = Time.utc(2008,6,8) # june 8, 2008 was a sunday
@monday = Time.utc(2008,6,9)
@tuesday = Time.utc(2008,6,10)
@wednesday = Time.utc(2008,6,11)
@thursday = Time.utc(2008,6,12)
@friday = Time.zone.local(2008,6,6)
@saturday = Time.zone.local(2008,6,7)
@sunday = Time.zone.local(2008,6,8) # june 8, 2008 was a sunday
@monday = Time.zone.local(2008,6,9)
@tuesday = Time.zone.local(2008,6,10)
@wednesday = Time.zone.local(2008,6,11)
@thursday = Time.zone.local(2008,6,12)
end
def test_pattern_text
@ -134,19 +134,19 @@ class RecurringTodoTest < Test::Rails::TestCase
def test_monthly_pattern
due_date = @monthly_every_last_friday.get_due_date(@sunday)
assert_equal Time.utc(2008,6,27), due_date
assert_equal Time.zone.local(2008,6,27), due_date
friday_is_last_day_of_month = Time.utc(2008,10,31)
friday_is_last_day_of_month = Time.zone.local(2008,10,31)
due_date = @monthly_every_last_friday.get_due_date(friday_is_last_day_of_month-1.day )
assert_equal friday_is_last_day_of_month , due_date
@monthly_every_third_friday = @monthly_every_last_friday
@monthly_every_third_friday.every_other3=3 #third
due_date = @monthly_every_last_friday.get_due_date(@sunday) # june 8th 2008
assert_equal Time.utc(2008, 6, 20), due_date
assert_equal Time.zone.local(2008, 6, 20), due_date
# set date past third friday of this month
due_date = @monthly_every_last_friday.get_due_date(Time.utc(2008,6,21)) # june 21th 2008
assert_equal Time.utc(2008, 8, 15), due_date # every 2 months, so aug
due_date = @monthly_every_last_friday.get_due_date(Time.zone.local(2008,6,21)) # june 21th 2008
assert_equal Time.zone.local(2008, 8, 15), due_date # every 2 months, so aug
@monthly = @monthly_every_last_friday
@monthly.recurrence_selector=0
@ -157,12 +157,12 @@ class RecurringTodoTest < Test::Rails::TestCase
assert_equal @sunday, due_date # june 8th
due_date = @monthly.get_due_date(@sunday) # june 8th
assert_equal Time.utc(2008,8,8), due_date # aug 8th
assert_equal Time.zone.local(2008,8,8), due_date # aug 8th
end
def test_yearly_pattern
# beginning of same year
due_date = @yearly.get_due_date(Time.utc(2008,2,10)) # feb 10th
due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th
assert_equal @sunday, due_date # june 8th
# same month, previous date
@ -173,20 +173,20 @@ class RecurringTodoTest < Test::Rails::TestCase
# same month, day after
due_date = @yearly.get_due_date(@monday) # june 9th
assert_equal Time.utc(2009,6,8), due_date # june 8th next year
assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year
@yearly.recurrence_selector = 1
@yearly.every_other3 = 2 # second
@yearly.every_count = 3 # wednesday
# beginning of same year
due_date = @yearly.get_due_date(Time.utc(2008,2,10)) # feb 10th
assert_equal Time.utc(2008,6,11), due_date # june 11th
due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th
assert_equal Time.zone.local(2008,6,11), due_date # june 11th
# same month, before second wednesday
due_date = @yearly.get_due_date(@saturday) # june 7th
assert_equal Time.utc(2008,6,11), due_date # june 11th
assert_equal Time.zone.local(2008,6,11), due_date # june 11th
# same month, after second wednesday
due_date = @yearly.get_due_date(Time.utc(2008,6,12)) # june 7th
assert_equal Time.utc(2009,6,10), due_date # june 10th
due_date = @yearly.get_due_date(Time.zone.local(2008,6,12)) # june 7th
assert_equal Time.zone.local(2009,6,10), due_date # june 10th
# test handling of nil
due_date1 = @yearly.get_due_date(nil)
@ -207,24 +207,24 @@ class RecurringTodoTest < Test::Rails::TestCase
due_date = @every_day.get_due_date(@in_four_days)
assert_equal @in_four_days+1.day, due_date
@weekly_every_day.start_from = Time.utc(2020,1,1)
assert_equal Time.utc(2020,1,1), @weekly_every_day.get_due_date(nil)
assert_equal Time.utc(2020,1,1), @weekly_every_day.get_due_date(Time.utc(2019,10,1))
assert_equal Time.utc(2020,1,10), @weekly_every_day.get_due_date(Time.utc(2020,1,9))
@weekly_every_day.start_from = Time.zone.local(2020,1,1)
assert_equal Time.zone.local(2020,1,1), @weekly_every_day.get_due_date(nil)
assert_equal Time.zone.local(2020,1,1), @weekly_every_day.get_due_date(Time.zone.local(2019,10,1))
assert_equal Time.zone.local(2020,1,10), @weekly_every_day.get_due_date(Time.zone.local(2020,1,9))
@monthly_every_last_friday.start_from = Time.utc(2020,1,1)
assert_equal Time.utc(2020,1,31), @monthly_every_last_friday.get_due_date(nil) # last friday of jan
assert_equal Time.utc(2020,1,31), @monthly_every_last_friday.get_due_date(Time.utc(2019,12,1)) # last friday of jan
assert_equal Time.utc(2020,2,28), @monthly_every_last_friday.get_due_date(Time.utc(2020,2,1)) # last friday of feb
@monthly_every_last_friday.start_from = Time.zone.local(2020,1,1)
assert_equal Time.zone.local(2020,1,31), @monthly_every_last_friday.get_due_date(nil) # last friday of jan
assert_equal Time.zone.local(2020,1,31), @monthly_every_last_friday.get_due_date(Time.zone.local(2019,12,1)) # last friday of jan
assert_equal Time.zone.local(2020,2,28), @monthly_every_last_friday.get_due_date(Time.zone.local(2020,2,1)) # last friday of feb
# start from after june 8th 2008
@yearly.start_from = Time.utc(2020,6,12)
assert_equal Time.utc(2021,6,8), @yearly.get_due_date(nil) # jun 8th next year
assert_equal Time.utc(2021,6,8), @yearly.get_due_date(Time.utc(2019,6,1)) # also next year
assert_equal Time.utc(2021,6,8), @yearly.get_due_date(Time.utc(2020,6,15)) # also next year
@yearly.start_from = Time.zone.local(2020,6,12)
assert_equal Time.zone.local(2021,6,8), @yearly.get_due_date(nil) # jun 8th next year
assert_equal Time.zone.local(2021,6,8), @yearly.get_due_date(Time.zone.local(2019,6,1)) # also next year
assert_equal Time.zone.local(2021,6,8), @yearly.get_due_date(Time.zone.local(2020,6,15)) # also next year
this_year = Time.now.utc.year
@yearly.start_from = Time.utc(this_year+1,6,12)
@yearly.start_from = Time.zone.local(this_year+1,6,12)
due_date = @yearly.get_due_date(nil)
assert_equal due_date.year, this_year+2
end

View file

@ -18,7 +18,7 @@ class TodoCreateParamsHelperTest < Test::Rails::TestCase
end
def test_show_from_accessor
expected_date = Time.now.to_date
expected_date = Time.now
params = { 'todo' => { 'show_from' => expected_date}}
prefs = flexmock()
params_helper = TodosController::TodoCreateParamsHelper.new(params, prefs)
@ -26,7 +26,7 @@ class TodoCreateParamsHelperTest < Test::Rails::TestCase
end
def test_due_accessor
expected_date = Time.now.to_date
expected_date = Time.now
params = { 'todo' => { 'due' => expected_date}}
prefs = flexmock()
params_helper = TodosController::TodoCreateParamsHelper.new(params, prefs)

View file

@ -68,9 +68,8 @@ class TodoTest < Test::Rails::TestCase
def test_validate_show_from_must_be_a_date_in_the_future
t = @not_completed2
t[:show_from] = 1.week.ago.to_date # we have to set this via the indexer because show_from=() updates the state
t[:show_from] = 1.week.ago # we have to set this via the indexer because show_from=() updates the state
# and actual show_from value appropriately based on the date
assert_equal 1.week.ago.to_date, t.show_from
assert !t.save
assert_equal 1, t.errors.count
assert_equal "must be a date in the future", t.errors.on(:show_from)
@ -118,7 +117,7 @@ class TodoTest < Test::Rails::TestCase
def test_activate_also_saves
t = @not_completed1
t.show_from = 1.week.from_now.to_date
t.show_from = 1.week.from_now
t.save!
assert t.deferred?
t.reload

View file

@ -235,7 +235,7 @@ class UserTest < Test::Rails::TestCase
def test_find_and_activate_deferred_todos_that_are_ready
assert_equal 1, @admin_user.deferred_todos.count
@admin_user.deferred_todos[0].show_from = @admin_user.time.to_date
@admin_user.deferred_todos[0].show_from = Time.now.utc - 5.seconds
@admin_user.deferred_todos[0].save
@admin_user.deferred_todos.reload
@admin_user.deferred_todos.find_and_activate_ready

View file

@ -20,28 +20,28 @@ class TodosHelperTest < Test::Rails::HelperTestCase
end
def test_show_date_in_past
date = 3.days.ago.to_date
date = 3.days.ago
html = show_date(date)
formatted_date = format_date(date)
assert_equal %Q{<a title="#{formatted_date}"><span class="red">Scheduled to show 3 days ago</span></a> }, html
end
def test_show_date_today
date = Time.zone.now.to_date
date = Time.zone.now
html = show_date(date)
formatted_date = format_date(date)
assert_equal %Q{<a title="#{formatted_date}"><span class="amber">Show Today</span></a> }, html
end
def test_show_date_tomorrow
date = 1.day.from_now.to_date
date = 1.day.from_now
html = show_date(date)
formatted_date = format_date(date)
assert_equal %Q{<a title="#{formatted_date}"><span class="amber">Show Tomorrow</span></a> }, html
end
def test_show_date_future
date = 10.days.from_now.to_date
date = 10.days.from_now
html = show_date(date)
formatted_date = format_date(date)
assert_equal %Q{<a title="#{formatted_date}"><span class="green">Show in 10 days</span></a> }, html
@ -49,20 +49,22 @@ class TodosHelperTest < Test::Rails::HelperTestCase
def test_remote_star_icon_unstarred
@todo = flexmock(:id => 1, :to_param => 1, :description => 'Get gas', :starred? => false)
assert_remote_star_icon_helper_matches %r{<a href="/todos/1/toggle_star" class="icon star_item" title="star the action 'Get gas'"><img alt="Blank" class="unstarred_todo" src="/images/blank.png[?0-9]*" title="Star action" /></a>}
# added dot (.) to regexp because somehouw the extra dot is added in the tests while its not in the rendered html
assert_remote_star_icon_helper_matches %r{<a href="/todos/1/toggle_star" class="icon star_item" title="star the action 'Get gas'"><img alt="Blank" class="unstarred_todo" src="/images/blank.png[.?0-9]*" title="Star action" /></a>}
assert_behavior_registered
end
def test_remote_star_icon_starred
@todo = flexmock(:id => 1, :to_param => 1, :description => 'Get gas', :starred? => true)
assert_remote_star_icon_helper_matches %r{<a href="/todos/1/toggle_star" class="icon star_item" title="star the action 'Get gas'"><img alt="Blank" class="starred_todo" src="/images/blank.png[?0-9]*" title="Star action" /></a>}
# added dot (.) to regexp because somehouw the extra dot is added in the tests while its not in the rendered html
assert_remote_star_icon_helper_matches %r{<a href="/todos/1/toggle_star" class="icon star_item" title="star the action 'Get gas'"><img alt="Blank" class="starred_todo" src="/images/blank.png[.?0-9]*" title="Star action" /></a>}
assert_behavior_registered
end
def assert_remote_star_icon_helper_matches(regex)
@controller.send :initialise_js_behaviours #simulate before filter
output = remote_star_icon
#puts output
# puts output
assert output =~ regex
@controller.send :store_js_behaviours #simulate after filter
end
@ -74,7 +76,7 @@ class TodosHelperTest < Test::Rails::HelperTestCase
rule = behaviors[:rules][0]
assert_equal ".item-container a.star_item:click", rule[0]
assert_equal "new Ajax.Request(this.href, {asynchronous:true, evalScripts:true, method:'put', parameters:{ _source_view : '' }})\n; return false;",
rule[1]
rule[1]
end
def protect_against_forgery?