diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 55c89a60..b08dc0be 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -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 diff --git a/app/controllers/backend_controller.rb b/app/controllers/backend_controller.rb index cc4c605e..206f2e81 100644 --- a/app/controllers/backend_controller.rb +++ b/app/controllers/backend_controller.rb @@ -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 diff --git a/app/controllers/data_controller.rb b/app/controllers/data_controller.rb index a0514945..ba997607 100644 --- a/app/controllers/data_controller.rb +++ b/app/controllers/data_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index ac0c251a..ada43168 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -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 diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index ec70762e..b989b432 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -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 diff --git a/app/models/message_gateway.rb b/app/models/message_gateway.rb new file mode 100644 index 00000000..8fcb78d3 --- /dev/null +++ b/app/models/message_gateway.rb @@ -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 diff --git a/app/models/preference.rb b/app/models/preference.rb index 6b76fd97..d4530f3f 100644 --- a/app/models/preference.rb +++ b/app/models/preference.rb @@ -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 \ No newline at end of file diff --git a/app/models/project.rb b/app/models/project.rb index de0ad2c6..396d49cc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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] diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index e39a15e3..86b24dae 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -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 diff --git a/app/models/todo.rb b/app/models/todo.rb index 7a6f3892..972b032f 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -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 \ No newline at end of file + # 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 diff --git a/app/models/user.rb b/app/models/user.rb index b49db26c..bf84bbd3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/contexts/index.html.erb b/app/views/contexts/index.html.erb index 846d01ca..ef915ac1 100644 --- a/app/views/contexts/index.html.erb +++ b/app/views/contexts/index.html.erb @@ -53,4 +53,9 @@ <% sortable_element 'list-contexts', get_listing_sortable_options --%> \ No newline at end of file +-%> + diff --git a/app/views/data/index.html.erb b/app/views/data/index.html.erb index 5fb93294..5ed8adaf 100644 --- a/app/views/data/index.html.erb +++ b/app/views/data/index.html.erb @@ -1,37 +1,49 @@ -
-
-

Exporting data

-

You can choose between the following formats:

- -
+
+
+
+

Importing data

+

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. +

+

<%= link_to "Start import", :controller => 'data', :action => 'yaml_form' %>.

+

Exporting data

+

You can choose between the following formats:

+
    +
  • YAML: Best for exporting data.
    Please note that importing YAML files is currently supported only in experimentally. Do not rely on it for backing up critical data.
  • +
  • CSV: Best for importing into spreadsheet or data analysis software
  • +
  • XML: Best for importing or repurposing the data
  • +
+
+

+ + + + + + + + + + + + + + + + + + + + + +
DescriptionDownload link
YAML file containing all your actions, contexts, projects, tags and notes<%= link_to "YAML file", :controller => 'data', :action => 'yaml_export' %>
CSV file containing all of your actions, with named contexts and projects<%= link_to "CSV file (actions, contexts and projects)", :controller => 'data', :action => 'csv_actions' %>
CSV file containing all your notes<%= link_to "CSV file (notes only)", :controller => 'data', :action => 'csv_notes' %>
XML file containing all your actions, contexts, projects, tags and notes<%= link_to "XML file (actions only)", :controller => 'data', :action => 'xml_export' %>
+
+
-

- - - - - - - - - - - - - - - - - - - - - -
DescriptionDownload link
YAML file containing all your actions, contexts, projects, tags and notes<%= link_to "YAML file", :controller => 'data', :action => 'yaml_export' %>
CSV file containing all of your actions, with named contexts and projects<%= link_to "CSV file (actions, contexts and projects)", :controller => 'data', :action => 'csv_actions' %>
CSV file containing all your notes<%= link_to "CSV file (notes only)", :controller => 'data', :action => 'csv_notes' %>
XML file containing all your actions, contexts, projects, tags and notes<%= link_to "XML file (actions only)", :controller => 'data', :action => 'xml_export' %>
-

- -
\ No newline at end of file + diff --git a/app/views/data/yaml_form.html.erb b/app/views/data/yaml_form.html.erb index 444613d7..edc13098 100644 --- a/app/views/data/yaml_form.html.erb +++ b/app/views/data/yaml_form.html.erb @@ -1,19 +1,23 @@
-
-
-

Paste the contents of the YAML file you exported into the text box below:

-
- -

-<% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %> - <%= f.text_area :yaml %>
- -<% end %> -

- -
+
+
+

Beware: 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. +

+

Paste the contents of the YAML file you exported into the text box below:

+
+

+ <% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %> + <%= f.text_area :yaml %>
+ + <% end %> +

+
-
- -
\ No newline at end of file + diff --git a/app/views/data/yaml_import.html.erb b/app/views/data/yaml_import.html.erb index cf79230b..2e9fcd5f 100644 --- a/app/views/data/yaml_import.html.erb +++ b/app/views/data/yaml_import.html.erb @@ -1 +1,7 @@ -

Import was successful

\ No newline at end of file +<% if !(@errmessage == '') %> +

There were these errors: +

<%= @errmessage  %>
+

+<% else %> +

Import was successful.

+<% end %> diff --git a/app/views/integrations/index.html.erb b/app/views/integrations/index.html.erb index d200b12c..be2a5a0c 100644 --- a/app/views/integrations/index.html.erb +++ b/app/views/integrations/index.html.erb @@ -12,6 +12,18 @@

Do you have one of your own to add? Tell us about it in our Tips and Tricks forum and we may include it on this page in a future versions of Tracks.

+ +

Integrated email/SMS receiver

+

+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: +

+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. +

+

Add an Action with Applescript

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.

diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb index 59f34550..42ea20d2 100644 --- a/app/views/layouts/standard.html.erb +++ b/app/views/layouts/standard.html.erb @@ -16,9 +16,6 @@ @@ -57,6 +54,7 @@ window.onload=function(){ <% if current_user.is_admin? -%>
  • <%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %>
  • <% end -%> +
  • <%= navigation_link(image_tag("x-office-calendar.png", :size => "16X16", :border => 0), calendar_path, :title => "Calendar of due actions" ) %>
  • <%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %>
  • <%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %>
  • <%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %>
  • diff --git a/app/views/notes/_notes.rhtml b/app/views/notes/_notes.rhtml index 9391d108..de9068b8 100644 --- a/app/views/notes/_notes.rhtml +++ b/app/views/notes/_notes.rhtml @@ -2,7 +2,7 @@

    <%= link_to("Note #{note.id}", note_path(note), :title => "Show note #{note.id}" ) %>

    - <%= sanitize(textilize(note.body.gsub(/((https?:\/\/[^ \n\t]*))/, '"\1":\2'))) %> + <%= sanitize(markdown(auto_link(note.body))) %> @@ -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]}) } %> <%= submit_tag "Update" %> <%= link_to "Cancel", :action => 'index' %> diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb index 985834dd..05c90a86 100644 --- a/app/views/preferences/index.html.erb +++ b/app/views/preferences/index.html.erb @@ -28,6 +28,8 @@
  • Refresh interval (in minutes): <%= prefs.refresh %>
  • Verbose action descriptors: <%= prefs.verbose_action_descriptors %>
  • Actions per page (Mobile View): <%= prefs.mobile_todos_per_page %>
  • +
  • From email: <%= prefs.sms_email %>
  • +
  • Default email context: <%= prefs.sms_context.nil? ? "None" : prefs.sms_context.name %>
  • <%= link_to "Edit preferences »", { :controller => 'preferences', :action => 'edit'}, :class => 'edit_link' %> diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index b2108140..e8c7ec54 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -61,4 +61,10 @@
    - \ No newline at end of file + + + diff --git a/app/views/recurring_todos/_recurring_todo_form.erb b/app/views/recurring_todos/_recurring_todo_form.erb index 4a767661..a9709e45 100644 --- a/app/views/recurring_todos/_recurring_todo_form.erb +++ b/app/views/recurring_todos/_recurring_todo_form.erb @@ -101,22 +101,22 @@

    diff --git a/app/views/recurring_todos/index.html.erb b/app/views/recurring_todos/index.html.erb index 936773d8..40b586c5 100644 --- a/app/views/recurring_todos/index.html.erb +++ b/app/views/recurring_todos/index.html.erb @@ -40,3 +40,9 @@ apply_behaviour "#recurring_edit_period:click", "TracksForm.hide_all_edit_recurring(); $('recurring_edit_'+TracksForm.get_edit_period()).show();" -%> + + diff --git a/app/views/todos/_todo.html.erb b/app/views/todos/_todo.html.erb index c03e64e6..288325a0 100644 --- a/app/views/todos/_todo.html.erb +++ b/app/views/todos/_todo.html.erb @@ -11,6 +11,7 @@ <%= remote_star_icon %> <%= remote_toggle_checkbox unless source_view_is :deferred %>
    + <% unless @todo.completed? %><%= defer_link(1) %> <%= defer_link(7) %><% end %> <%= date_span -%> <%= h sanitize(todo.description) %> <%= link_to(image_tag("recurring16x16.png"), {:controller => "recurring_todos", :action => "index"}, :class => "recurring_icon") if @todo.from_recurring_todo? %> diff --git a/app/views/todos/_toggle_notes.rhtml b/app/views/todos/_toggle_notes.rhtml index 704894b3..52f279a1 100644 --- a/app/views/todos/_toggle_notes.rhtml +++ b/app/views/todos/_toggle_notes.rhtml @@ -3,5 +3,5 @@ element.next('.todo_notes').toggle end -%> \ No newline at end of file diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb new file mode 100644 index 00000000..056c0cff --- /dev/null +++ b/app/views/todos/calendar.html.erb @@ -0,0 +1,68 @@ +
    + +
    +

    Due today

    +
    > + No actions due today +
    +
    + <%= render :partial => "todos/todo", :collection => @due_today %> +
    +
    + +
    +

    Due in rest of this week

    +
    > + No actions due in rest of this week +
    +
    + <%= render :partial => "todos/todo", :collection => @due_this_week %> +
    +
    + +
    +

    Due next week

    +
    > + No actions due in next week +
    +
    + <%= render :partial => "todos/todo", :collection => @due_next_week %> +
    +
    + +
    +

    Due in rest of this month

    +
    > + No actions due in rest of this month +
    +
    + <%= render :partial => "todos/todo", :collection => @due_this_month %> +
    +
    + +
    +

    Due next month and later

    +
    > + No actions due after this month +
    +
    + <%= render :partial => "todos/todo", :collection => @due_after_this_month %> +
    +
    + +
    +
    + +

    <%= link_to('iCal', {:format => 'ics', :token => current_user.token}, :title => "iCal feed" ) %> + - Get this calendar in iCal format

    +
    + +<% +apply_behavior 'input.hide_tickler:click', :prevent_default => true do |page| + page << "alert('hiding action in tickler from calendar is not yet implemented');" +end +%> \ No newline at end of file diff --git a/app/views/todos/calendar.ics.erb b/app/views/todos/calendar.ics.erb new file mode 100644 index 00000000..46a02f60 --- /dev/null +++ b/app/views/todos/calendar.ics.erb @@ -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 diff --git a/app/views/todos/destroy.js.rjs b/app/views/todos/destroy.js.rjs index 7f37f87d..ac1ba3d4 100644 --- a/app/views/todos/destroy.js.rjs +++ b/app/views/todos/destroy.js.rjs @@ -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 diff --git a/app/views/todos/toggle_check.js.rjs b/app/views/todos/toggle_check.js.rjs index 1462162a..d862862c 100644 --- a/app/views/todos/toggle_check.js.rjs +++ b/app/views/todos/toggle_check.js.rjs @@ -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) diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index 582460ff..2f263b54 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -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 diff --git a/config/routes.rb b/config/routes.rb index a4eb29e3..50e29ff7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/db/migrate/041_add_sms_to_preference.rb b/db/migrate/041_add_sms_to_preference.rb new file mode 100644 index 00000000..61f7e05a --- /dev/null +++ b/db/migrate/041_add_sms_to_preference.rb @@ -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 diff --git a/db/migrate/042_change_dates_to_datetimes.rb b/db/migrate/042_change_dates_to_datetimes.rb new file mode 100644 index 00000000..99e75179 --- /dev/null +++ b/db/migrate/042_change_dates_to_datetimes.rb @@ -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 diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 1dd8e775..05f59965 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -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) diff --git a/public/.htaccess b/public/.htaccess index d579b5cc..3b66fccd 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -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 diff --git a/public/images/defer_1.png b/public/images/defer_1.png new file mode 100644 index 00000000..d40a9c01 Binary files /dev/null and b/public/images/defer_1.png differ diff --git a/public/images/defer_7.png b/public/images/defer_7.png new file mode 100644 index 00000000..7e80ff15 Binary files /dev/null and b/public/images/defer_7.png differ diff --git a/public/images/x-office-calendar.png b/public/images/x-office-calendar.png new file mode 100644 index 00000000..f8607e33 Binary files /dev/null and b/public/images/x-office-calendar.png differ diff --git a/public/javascripts/tracks_1219672692.js b/public/javascripts/tracks_1220755978.js similarity index 100% rename from public/javascripts/tracks_1219672692.js rename to public/javascripts/tracks_1220755978.js diff --git a/public/stylesheets/standard.css b/public/stylesheets/standard.css index 0a5b9516..3b31fcd7 100644 --- a/public/stylesheets/standard.css +++ b/public/stylesheets/standard.css @@ -1200,4 +1200,10 @@ body.integrations textarea { padding:3px; width:80%; background-color:#ddd; -} \ No newline at end of file +} +.defer-container { + float:right; +} +.defer-container a:hover { + background-color: inherit; +} diff --git a/public/stylesheets/tracks_1219763287.css b/public/stylesheets/tracks_1220796176.css similarity index 99% rename from public/stylesheets/tracks_1219763287.css rename to public/stylesheets/tracks_1220796176.css index c2d06198..446d9b87 100644 --- a/public/stylesheets/tracks_1219763287.css +++ b/public/stylesheets/tracks_1220796176.css @@ -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} diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 410860a1..5cc542f0 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7f915fbd..7b4eb222 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/views/notes/_notes.rhtml_spec.rb b/spec/views/notes/_notes.rhtml_spec.rb new file mode 100644 index 00000000..3f07ead3 --- /dev/null +++ b/spec/views/notes/_notes.rhtml_spec.rb @@ -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 diff --git a/spec/views/todos/_toggle_notes.rhtml_spec.rb b/spec/views/todos/_toggle_notes.rhtml_spec.rb new file mode 100644 index 00000000..d00d38a0 --- /dev/null +++ b/spec/views/todos/_toggle_notes.rhtml_spec.rb @@ -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 diff --git a/test/fixtures/contexts.yml b/test/fixtures/contexts.yml index 0991767f..49ce9d2f 100644 --- a/test/fixtures/contexts.yml +++ b/test/fixtures/contexts.yml @@ -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 %> diff --git a/test/fixtures/preferences.yml b/test/fixtures/preferences.yml index 380d1dc7..cad91d12 100644 --- a/test/fixtures/preferences.yml +++ b/test/fixtures/preferences.yml @@ -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 \ No newline at end of file diff --git a/test/fixtures/sample_mms.txt b/test/fixtures/sample_mms.txt new file mode 100644 index 00000000..a532e3ab --- /dev/null +++ b/test/fixtures/sample_mms.txt @@ -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 + + + + T-Mobile=20 + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    This is the message body
    =20 + +

    <= +/td> +
    + + + +------=_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: + +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: + +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: + +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: + +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-- diff --git a/test/fixtures/sample_sms.txt b/test/fixtures/sample_sms.txt new file mode 100644 index 00000000..86b449a3 --- /dev/null +++ b/test/fixtures/sample_sms.txt @@ -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 \ No newline at end of file diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index ff277ba2..ef6f82c5 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -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 \ No newline at end of file diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 4644b9f3..f97a892a 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -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 diff --git a/test/functional/recurring_todos_controller_test.rb b/test/functional/recurring_todos_controller_test.rb index a1e40b77..e9485c5b 100644 --- a/test/functional/recurring_todos_controller_test.rb +++ b/test/functional/recurring_todos_controller_test.rb @@ -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 diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index b99b537c..cd4fba01 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -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 diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index 47677eb8..daad85d9 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -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 diff --git a/test/integration/users_xml_api_test.rb b/test/integration/users_xml_api_test.rb index 57d14d9f..ed26d194 100644 --- a/test/integration/users_xml_api_test.rb +++ b/test/integration/users_xml_api_test.rb @@ -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 diff --git a/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel b/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel index 64ff3ed9..4b8adbe7 100644 --- a/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel +++ b/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel @@ -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" diff --git a/test/selenium/home/defer_todo_empty_context.rsel b/test/selenium/home/defer_todo_empty_context.rsel new file mode 100644 index 00000000..afb56fd7 --- /dev/null +++ b/test/selenium/home/defer_todo_empty_context.rsel @@ -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" diff --git a/test/selenium/home/defer_todo_with_button_empty_context.rsel b/test/selenium/home/defer_todo_with_button_empty_context.rsel new file mode 100644 index 00000000..3997c0fb --- /dev/null +++ b/test/selenium/home/defer_todo_with_button_empty_context.rsel @@ -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" diff --git a/test/selenium/home/mark_todo_complete_1.rsel b/test/selenium/home/mark_todo_complete_1.rsel index 35194bde..06ce62b5 100644 --- a/test/selenium/home/mark_todo_complete_1.rsel +++ b/test/selenium/home/mark_todo_complete_1.rsel @@ -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']" diff --git a/test/selenium/home/mark_todo_complete_2.rsel b/test/selenium/home/mark_todo_complete_2.rsel index dcb66769..b6cf4de9 100644 --- a/test/selenium/home/mark_todo_complete_2.rsel +++ b/test/selenium/home/mark_todo_complete_2.rsel @@ -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' diff --git a/test/selenium/home/mark_todo_incomplete.rsel b/test/selenium/home/mark_todo_incomplete.rsel index d0a159ac..86c31088 100644 --- a/test/selenium/home/mark_todo_incomplete.rsel +++ b/test/selenium/home/mark_todo_incomplete.rsel @@ -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" diff --git a/test/selenium/mobile/create_new_action.rsel b/test/selenium/mobile/create_new_action.rsel index d475a799..a8501da1 100644 --- a/test/selenium/mobile/create_new_action.rsel +++ b/test/selenium/mobile/create_new_action.rsel @@ -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" diff --git a/test/selenium/mobile/navigation.rsel b/test/selenium/mobile/navigation.rsel index bf459466..44bde92c 100644 --- a/test/selenium/mobile/navigation.rsel +++ b/test/selenium/mobile/navigation.rsel @@ -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' diff --git a/test/selenium/project_detail/mark_deferred_todo_complete.rsel b/test/selenium/project_detail/mark_deferred_todo_complete.rsel index 893192f8..0b07d445 100644 --- a/test/selenium/project_detail/mark_deferred_todo_complete.rsel +++ b/test/selenium/project_detail/mark_deferred_todo_complete.rsel @@ -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" \ No newline at end of file diff --git a/test/selenium/tags/badge_count.rsel b/test/selenium/tags/badge_count.rsel index 56cd9f84..4359059d 100644 --- a/test/selenium/tags/badge_count.rsel +++ b/test/selenium/tags/badge_count.rsel @@ -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' diff --git a/test/test_helper.rb b/test/test_helper.rb index 85392073..c34d707f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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 diff --git a/test/unit/message_gateway_test.rb b/test/unit/message_gateway_test.rb new file mode 100644 index 00000000..32f77159 --- /dev/null +++ b/test/unit/message_gateway_test.rb @@ -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 diff --git a/test/unit/preference_test.rb b/test/unit/preference_test.rb index 30daace5..18595ef7 100644 --- a/test/unit/preference_test.rb +++ b/test/unit/preference_test.rb @@ -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 diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index e8954937..966c8c6d 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -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 diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index b3982d4e..bd256eba 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -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 diff --git a/test/unit/todo_create_params_helper_test.rb b/test/unit/todo_create_params_helper_test.rb index 4b38b60d..ade5cbfd 100644 --- a/test/unit/todo_create_params_helper_test.rb +++ b/test/unit/todo_create_params_helper_test.rb @@ -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) diff --git a/test/unit/todo_test.rb b/test/unit/todo_test.rb index 9227f40b..85c69b39 100644 --- a/test/unit/todo_test.rb +++ b/test/unit/todo_test.rb @@ -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 diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 3245099c..efadc277 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -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 diff --git a/test/views/todos_helper_test.rb b/test/views/todos_helper_test.rb index 843d859e..3ef4a81c 100644 --- a/test/views/todos_helper_test.rb +++ b/test/views/todos_helper_test.rb @@ -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{Scheduled to show 3 days ago }, 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{Show Today }, 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{Show Tomorrow }, 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{Show in 10 days }, 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{Blank} + # 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{Blank} 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{Blank} + # 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{Blank} 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?