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

This commit is contained in:
Jakub A.Tesinsky 2008-12-02 02:24:08 +01:00
commit 8aa573a73e
106 changed files with 2221 additions and 585 deletions

View file

@ -9,12 +9,14 @@ require "redcloth"
require 'date'
require 'time'
# Commented the following line because of #744. It prevented rake db:migrate to
# Commented the following line because of #744. It prevented rake db:migrate to
# run because this tag went looking for the taggings table that did not exist
# when you feshly create a new database
# Old comment: We need this in development mode, or you get 'method missing' errors
# when you feshly create a new database Old comment: We need this in development
# mode, or you get 'method missing' errors
#
# Tag
# Tag
class CannotAccessContext < RuntimeError; end
class ApplicationController < ActionController::Base
@ -113,7 +115,7 @@ class ApplicationController < ActionController::Base
def format_date(date)
if date
date_format = prefs.date_format
formatted_date = date.strftime("#{date_format}")
formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}")
else
formatted_date = ''
end
@ -160,7 +162,39 @@ class ApplicationController < ActionController::Base
response.content_type = 'text/html'
end
end
def create_todo_from_recurring_todo(rt, date=nil)
# create todo and initialize with data from recurring_todo rt
todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id})
# set dates
todo.recurring_todo_id = rt.id
todo.due = rt.get_due_date(date)
show_from_date = rt.get_show_from_date(date)
if show_from_date.nil?
todo.show_from=nil
else
# make sure that show_from is not in the past
todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date
end
saved = todo.save
if saved
todo.tag_with(rt.tag_list, current_user)
todo.tags.reload
end
# increate number of occurences created from recurring todo
rt.inc_occurences
# mark recurring todo complete if there are no next actions left
checkdate = todo.due.nil? ? todo.show_from : todo.due
rt.toggle_completion! unless rt.has_next_todo(checkdate)
return saved ? todo : nil
end
protected
def admin_login_required
@ -192,7 +226,7 @@ class ApplicationController < ActionController::Base
def openid_enabled?
self.class.openid_enabled?
end
private
def parse_date_per_user_prefs( s )
@ -231,29 +265,5 @@ class ApplicationController < ActionController::Base
def set_time_zone
Time.zone = current_user.prefs.time_zone if logged_in?
end
def create_todo_from_recurring_todo(rt, date=nil)
# create todo and initialize with data from recurring_todo rt
todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id})
# set dates
todo.due = rt.get_due_date(date)
todo.show_from = rt.get_show_from_date(date)
todo.recurring_todo_id = rt.id
saved = todo.save
if saved
todo.tag_with(rt.tag_list, current_user)
todo.tags.reload
end
# increate number of occurences created from recurring todo
rt.inc_occurences
# mark recurring todo complete if there are no next actions left
checkdate = todo.due.nil? ? todo.show_from : todo.due
rt.toggle_completion! unless rt.has_next_todo(checkdate)
return saved ? todo : nil
end
end

View file

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

View file

@ -83,10 +83,14 @@ class ContextsController < ApplicationController
end
@context.attributes = params["context"]
if @context.save
if params['wants_render']
if boolean_param('wants_render')
respond_to do |format|
format.js
end
elsif boolean_param('update_context_name')
@contexts = current_user.projects
render :template => 'contexts/update_context_name.js.rjs'
return
else
render :text => success_text || 'Success'
end
@ -130,10 +134,10 @@ class ContextsController < ApplicationController
def render_contexts_mobile
lambda do
@page_title = "TRACKS::List Contexts"
@active_contexts = @contexts.find(:all, { :conditions => ["hide = ?", false]})
@hidden_contexts = @contexts.find(:all, { :conditions => ["hide = ?", true]})
@active_contexts = @contexts.active
@hidden_contexts = @contexts.hidden
@down_count = @active_contexts.size + @hidden_contexts.size
cookies[:mobile_url]=request.request_uri
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
render :action => 'index_mobile'
end
end
@ -143,7 +147,7 @@ class ContextsController < ApplicationController
@page_title = "TRACKS::List actions in "+@context.name
@not_done = @not_done_todos.select {|t| t.context_id == @context.id }
@down_count = @not_done.size
cookies[:mobile_url]=request.request_uri
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
@mobile_from_context = @context.id
render :action => 'mobile_show_context'
end

View file

@ -3,7 +3,7 @@ class DataController < ApplicationController
require 'csv'
def index
@page_title = "TRACKS::Export"
@page_title = "TRACKS::Export"
end
def import
@ -24,6 +24,7 @@ class DataController < ApplicationController
all_tables['tags'] = current_user.tags.find(:all)
all_tables['taggings'] = current_user.taggings.find(:all)
all_tables['notes'] = current_user.notes.find(:all)
all_tables['recurring_todos'] = current_user.recurring_todos.find(:all)
result = all_tables.to_yaml
result.gsub!(/\n/, "\r\n") # TODO: general functionality for line endings
@ -34,21 +35,21 @@ class DataController < ApplicationController
content_type = 'text/csv'
CSV::Writer.generate(result = "") do |csv|
csv << ["id", "Context", "Project", "Description", "Notes", "Tags",
"Created at", "Due", "Completed at", "User ID", "Show from",
"state"]
"Created at", "Due", "Completed at", "User ID", "Show from",
"state"]
current_user.todos.find(:all, :include => [:context, :project]).each do |todo|
# Format dates in ISO format for easy sorting in spreadsheet
# Print context and project names for easy viewing
# Format dates in ISO format for easy sorting in spreadsheet Print
# context and project names for easy viewing
csv << [todo.id, todo.context.name,
todo.project_id = todo.project_id.nil? ? "" : todo.project.name,
todo.description,
todo.notes, todo.tags.collect{|t| t.name}.join(', '),
todo.created_at.to_formatted_s(:db),
todo.due = todo.due? ? todo.due.to_formatted_s(:db) : "",
todo.completed_at = todo.completed_at? ? todo.completed_at.to_formatted_s(:db) : "",
todo.user_id,
todo.show_from = todo.show_from? ? todo.show_from.to_formatted_s(:db) : "",
todo.state]
todo.project_id = todo.project_id.nil? ? "" : todo.project.name,
todo.description,
todo.notes, todo.tags.collect{|t| t.name}.join(', '),
todo.created_at.to_formatted_s(:db),
todo.due = todo.due? ? todo.due.to_formatted_s(:db) : "",
todo.completed_at = todo.completed_at? ? todo.completed_at.to_formatted_s(:db) : "",
todo.user_id,
todo.show_from = todo.show_from? ? todo.show_from.to_formatted_s(:db) : "",
todo.state]
end
end
send_data(result, :filename => "todos.csv", :type => content_type)
@ -58,16 +59,17 @@ class DataController < ApplicationController
content_type = 'text/csv'
CSV::Writer.generate(result = "") do |csv|
csv << ["id", "User ID", "Project", "Note",
"Created at", "Updated at"]
# had to remove project include because it's association order is leaking through
# and causing an ambiguous column ref even with_exclusive_scope didn't seem to help -JamesKebinger
"Created at", "Updated at"]
# had to remove project include because it's association order is leaking
# through and causing an ambiguous column ref even with_exclusive_scope
# didn't seem to help -JamesKebinger
current_user.notes.find(:all,:order=>"notes.created_at").each do |note|
# Format dates in ISO format for easy sorting in spreadsheet
# Print context and project names for easy viewing
# Format dates in ISO format for easy sorting in spreadsheet Print
# context and project names for easy viewing
csv << [note.id, note.user_id,
note.project_id = note.project_id.nil? ? "" : note.project.name,
note.body, note.created_at.to_formatted_s(:db),
note.updated_at.to_formatted_s(:db)]
note.project_id = note.project_id.nil? ? "" : note.project.name,
note.body, note.created_at.to_formatted_s(:db),
note.updated_at.to_formatted_s(:db)]
end
end
send_data(result, :filename => "notes.csv", :type => content_type)
@ -81,6 +83,7 @@ class DataController < ApplicationController
result << current_user.tags.find(:all).to_xml(:skip_instruct => true)
result << current_user.taggings.find(:all).to_xml(:skip_instruct => true)
result << current_user.notes.find(:all).to_xml(:skip_instruct => true)
result << current_user.recurring_todos.find(:all).to_xml(:skip_instruct => true)
send_data(result, :filename => "tracks_backup.xml", :type => 'text/xml')
end
@ -102,7 +105,6 @@ class DataController < ApplicationController
@inarray = YAML::load(params['import']['yaml'])
# arrays to handle id translations
# contexts
translate_context = Hash.new
translate_context[nil] = nil
@ -151,18 +153,18 @@ class DataController < ApplicationController
# state + dates
case item.ivars['attributes']['state']
when 'active' : newitem.activate!
when 'project_hidden' : newitem.hide!
when 'completed'
newitem.complete!
newitem.completed_at = adjust_time(item.ivars['attributes']['completed_at'])
when 'deferred' : newitem.defer!
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
# tags
translate_tag = Hash.new
translate_tag[nil] = nil
current_user.tags.each { |item| item.destroy }
@ -180,8 +182,8 @@ class DataController < ApplicationController
newitem.user_id = current_user.id
newitem.tag_id = translate_tag[newitem.tag_id]
case newitem.taggable_type
when 'Todo' : newitem.taggable_id = translate_todo[newitem.taggable_id]
else newitem.taggable_id = 0
when 'Todo' then newitem.taggable_id = translate_todo[newitem.taggable_id]
else newitem.taggable_id = 0
end
newitem.save
}
@ -196,7 +198,6 @@ class DataController < ApplicationController
newitem.created_at = adjust_time(item.ivars['attributes']['created_at'])
newitem.save
}
end
end
end

View file

@ -12,12 +12,12 @@ class FeedlistController < ApplicationController
@contexts = current_user.contexts
end
@active_projects = @projects.select{ |p| p.active? }
@hidden_projects = @projects.select{ |p| p.hidden? }
@completed_projects = @projects.select{ |p| p.completed? }
@active_projects = current_user.projects.active
@hidden_projects = current_user.projects.hidden
@completed_projects = current_user.projects.completed
@active_contexts = @contexts.select{ |c| !c.hidden? }
@hidden_contexts = @contexts.select{ |c| c.hidden? }
@active_contexts = current_user.contexts.active
@hidden_contexts = current_user.contexts.hidden
respond_to do |format|
format.html { render :layout => 'standard' }

View file

@ -20,10 +20,10 @@ class LoginController < ApplicationController
session['noexpiry'] = params['user_noexpiry']
msg = (should_expire_sessions?) ? "will expire after 1 hour of inactivity." : "will not expire."
notify :notice, "Login successful: session #{msg}"
cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year }
cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at }
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
end
redirect_back_or_home
return
@ -94,12 +94,12 @@ class LoginController < ApplicationController
session['noexpiry'] = session['user_noexpiry']
msg = (should_expire_sessions?) ? "will expire after 1 hour of inactivity." : "will not expire."
notify :notice, "You have successfully verified #{openid_url} as your identity. Login successful: session #{msg}"
cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year }
cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at }
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
end
cookies[:openid_url] = { :value => openid_url, :expires => Time.now + 1.year }
cookies[:openid_url] = { :value => openid_url, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE }
redirect_back_or_home
else
notify :warning, "You have successfully verified #{openid_url} as your identity, but you do not have a Tracks account. Please ask your administrator to sign you up."

View file

@ -31,7 +31,7 @@ class ProjectsController < ApplicationController
end
def projects_and_actions
@projects = @projects.select { |p| p.active? }
@projects = @projects.active
respond_to do |format|
format.text {
render :action => 'index_text_projects_and_actions', :layout => false, :content_type => Mime::TEXT
@ -43,7 +43,7 @@ class ProjectsController < ApplicationController
init_data_for_sidebar unless mobile?
@projects = current_user.projects
@page_title = "TRACKS::Project: #{@project.name}"
@project.todos.send :with_scope, :find => { :include => [:context, :tags] } do
@project.todos.send :with_scope, :find => { :include => [:context] } do
@not_done = @project.not_done_todos(:include_project_hidden_todos => true)
@deferred = @project.deferred_todos.sort_by { |todo| todo.show_from }
@done = @project.done_todos
@ -83,7 +83,7 @@ class ProjectsController < ApplicationController
@go_to_project = params['go_to_project']
@saved = @project.save
@project_not_done_counts = { @project.id => 0 }
@active_projects_count = current_user.projects.count(:conditions => "state = 'active'")
@active_projects_count = current_user.projects.active.count
@contexts = current_user.contexts
respond_to do |format|
format.js { @down_count = current_user.projects.size }
@ -124,9 +124,9 @@ class ProjectsController < ApplicationController
@project_not_done_counts[@project.id] = @project.reload().not_done_todo_count(:include_project_hidden_todos => true)
end
@contexts = current_user.contexts
@active_projects_count = current_user.projects.count(:conditions => "state = 'active'")
@hidden_projects_count = current_user.projects.count(:conditions => "state = 'hidden'")
@completed_projects_count = current_user.projects.count(:conditions => "state = 'completed'")
@active_projects_count = current_user.projects.active.count
@hidden_projects_count = current_user.projects.hidden.count
@completed_projects_count = current_user.projects.completed.count
render :template => 'projects/update.js.rjs'
return
elsif boolean_param('update_status')
@ -136,6 +136,10 @@ class ProjectsController < ApplicationController
@initial_context_name = @project.default_context.name
render :template => 'projects/update_default_context.js.rjs'
return
elsif boolean_param('update_project_name')
@projects = current_user.projects
render :template => 'projects/update_project_name.js.rjs'
return
else
render :text => success_text || 'Success'
return
@ -157,9 +161,9 @@ class ProjectsController < ApplicationController
def destroy
@project.destroy
@active_projects_count = current_user.projects.count(:conditions => "state = 'active'")
@hidden_projects_count = current_user.projects.count(:conditions => "state = 'hidden'")
@completed_projects_count = current_user.projects.count(:conditions => "state = 'completed'")
@active_projects_count = current_user.projects.active.count
@hidden_projects_count = current_user.projects.hidden.count
@completed_projects_count = current_user.projects.completed.count
respond_to do |format|
format.js { @down_count = current_user.projects.size }
format.xml { render :text => "Deleted project #{@project.name}" }
@ -182,15 +186,22 @@ class ProjectsController < ApplicationController
init_not_done_counts(['project'])
end
def actionize
@state = params['state']
@projects = current_user.projects.actionize(current_user.id, :state => @state) if @state
@contexts = current_user.contexts
init_not_done_counts(['project'])
end
protected
def render_projects_html
lambda do
@page_title = "TRACKS::List Projects"
@count = current_user.projects.size
@active_projects = @projects.select{ |p| p.active? }
@hidden_projects = @projects.select{ |p| p.hidden? }
@completed_projects = @projects.select{ |p| p.completed? }
@active_projects = @projects.active
@hidden_projects = @projects.hidden
@completed_projects = @projects.completed
@no_projects = @projects.empty?
@projects.cache_note_counts
@new_project = current_user.projects.build
@ -200,11 +211,11 @@ class ProjectsController < ApplicationController
def render_projects_mobile
lambda do
@active_projects = @projects.select{ |p| p.active? }
@hidden_projects = @projects.select{ |p| p.hidden? }
@completed_projects = @projects.select{ |p| p.completed? }
@active_projects = @projects.active
@hidden_projects = @projects.hidden
@completed_projects = @projects.completed
@down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size
cookies[:mobile_url]=request.request_uri
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
render :action => 'index_mobile'
end
end
@ -217,7 +228,7 @@ class ProjectsController < ApplicationController
@project_default_context = "The default context for this project is "+
@project.default_context.name
end
cookies[:mobile_url]=request.request_uri
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
@mobile_from_project = @project.id
render :action => 'project_mobile'
end

View file

@ -6,8 +6,10 @@ class RecurringTodosController < ApplicationController
append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update]
def index
@recurring_todos = current_user.recurring_todos.find(:all, :conditions => ["state = ?", "active"])
@completed_recurring_todos = current_user.recurring_todos.find(:all, :conditions => ["state = ?", "completed"])
find_and_inactivate
@recurring_todos = current_user.recurring_todos.active
@completed_recurring_todos = current_user.recurring_todos.completed
@no_recurring_todos = @recurring_todos.size == 0
@no_completed_recurring_todos = @completed_recurring_todos.size == 0
@count = @recurring_todos.size
@ -36,8 +38,8 @@ class RecurringTodosController < ApplicationController
# the form for a new recurring todo and the edit form are on the same page.
# Same goes for start_from and end_date
params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period']
params['recurring_todo']['end_date']=params['recurring_todo_edit_end_date']
params['recurring_todo']['start_from']=params['recurring_todo_edit_start_from']
params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date'])
params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from'])
# update project
if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil?
@ -84,6 +86,9 @@ class RecurringTodosController < ApplicationController
def create
p = RecurringTodoCreateParamsHelper.new(params)
p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date'])
p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from'])
@recurring_todo = current_user.recurring_todos.build(p.selector_attributes)
@recurring_todo.update_attributes(p.attributes)
@ -113,7 +118,7 @@ class RecurringTodosController < ApplicationController
else
@message += " / did not create todo"
end
@count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"])
@count = current_user.recurring_todos.active.count
else
@message = "Error saving recurring todo"
end
@ -126,7 +131,7 @@ class RecurringTodosController < ApplicationController
def destroy
# remove all references to this recurring todo
@todos = current_user.todos.find(:all, {:conditions => ["recurring_todo_id = ?", params[:id]]})
@todos = @recurring_todo.todos
@number_of_todos = @todos.size
@todos.each do |t|
t.recurring_todo_id = nil
@ -135,7 +140,7 @@ class RecurringTodosController < ApplicationController
# delete the recurring todo
@saved = @recurring_todo.destroy
@remaining = current_user.recurring_todos.count(:all)
@remaining = current_user.recurring_todos.count
respond_to do |format|
@ -158,14 +163,15 @@ class RecurringTodosController < ApplicationController
def toggle_check
@saved = @recurring_todo.toggle_completion!
@count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"])
@count = current_user.recurring_todos.active.count
@remaining = @count
if @recurring_todo.active?
@remaining = current_user.recurring_todos.count(:all, :conditions => ["state = ?", 'completed'])
@remaining = current_user.recurring_todos.completed.count
# from completed back to active -> check if there is an active todo
@active_todos = current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]})
# current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]})
@active_todos = @recurring_todo.todos.active.count
# create todo if there is no active todo belonging to the activated
# recurring_todo
@new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0
@ -252,5 +258,11 @@ class RecurringTodosController < ApplicationController
def get_recurring_todo_from_param
@recurring_todo = current_user.recurring_todos.find(params[:id])
end
def find_and_inactivate
# find active recurring todos without active todos and inactivate them
recurring_todos = current_user.recurring_todos.active
recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0}
end
end

View file

@ -8,7 +8,7 @@ class StatsController < ApplicationController
@page_title = 'TRACKS::Statistics'
@unique_tags = @tags.count(:all, {:group=>"tag_id"})
@hidden_contexts = @contexts.find(:all, {:conditions => ["hide = ? ", true]})
@hidden_contexts = @contexts.hidden
@first_action = @actions.find(:first, :order => "created_at ASC")
get_stats_actions

View file

@ -2,18 +2,19 @@ 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) }
def index
current_user.deferred_todos.find_and_activate_ready
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
@contexts_to_show = @contexts.reject {|x| x.hide? }
@contexts_to_show = current_user.contexts.active
respond_to do |format|
format.html &render_todos_html
@ -27,7 +28,7 @@ class TodosController < ApplicationController
end
def new
@projects = current_user.projects.select { |p| p.active? }
@projects = current_user.projects.active
@contexts = current_user.contexts.find(:all)
respond_to do |format|
format.m {
@ -89,6 +90,7 @@ class TodosController < ApplicationController
@contexts = current_user.contexts.find(:all) if @new_context_created
@projects = current_user.projects.find(:all) if @new_project_created
@initial_context_name = params['default_context_name']
@initial_project_name = params['default_project_name']
render :action => 'create'
end
format.xml do
@ -105,6 +107,7 @@ class TodosController < ApplicationController
@projects = current_user.projects.find(:all)
@contexts = current_user.contexts.find(:all)
@source_view = params['_source_view'] || 'todo'
@tag_name = params['_tag_name']
respond_to do |format|
format.js
end
@ -113,7 +116,7 @@ class TodosController < ApplicationController
def show
respond_to do |format|
format.m do
@projects = current_user.projects.select { |p| p.active? }
@projects = current_user.projects.active
@contexts = current_user.contexts.find(:all)
@edit_mobile = true
@return_path=cookies[:mobile_url]
@ -124,12 +127,14 @@ class TodosController < ApplicationController
end
# 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
@new_recurring_todo = check_for_next_todo(@todo) if @saved
respond_to do |format|
format.js do
@ -137,6 +142,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
@ -170,6 +179,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
@ -211,7 +223,7 @@ class TodosController < ApplicationController
@todo.complete!
end
# strange. if checkbox is not checked, there is no 'done' in params.
# Therfore I've used the negation
# Therefore I've used the negation
if !(params['done'] == '1') && @todo.completed?
@todo.activate!
end
@ -219,7 +231,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
@ -229,7 +260,7 @@ class TodosController < ApplicationController
format.m do
if @saved
if cookies[:mobile_url]
cookies[:mobile_url] = nil
cookies[:mobile_url] = {:value => nil, :secure => TRACKS_COOKIES_SECURE}
redirect_to cookies[:mobile_url]
else
redirect_to formatted_todos_path(:m)
@ -243,13 +274,14 @@ 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
@saved = @todo.destroy
# check if this todo has a related recurring_todo. If so, create next todo
check_for_next_todo
@saved = @todo.destroy
@new_recurring_todo = check_for_next_todo(@todo) if @saved
respond_to do |format|
@ -268,6 +300,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
@ -337,46 +372,109 @@ class TodosController < ApplicationController
def tag
@source_view = params['_source_view'] || 'tag'
@tag_name = params[:name]
@page_title = "TRACKS::Tagged with \'#{@tag_name}\'"
# mobile tags are routed with :name ending on .m. So we need to chomp it
@tag_name = @tag_name.chomp('.m') if mobile?
@tag = Tag.find_by_name(@tag_name)
@tag = Tag.new(:name => @tag_name) if @tag.nil?
tag_collection = @tag.todos
@not_done_todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active'])
@not_done_todos = tag_collection.find(:all,
:conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active'],
:order => 'todos.due IS NULL, todos.due ASC, todos.created_at ASC')
@hidden_todos = current_user.todos.find(:all,
:include => [:taggings, :tags, :context],
:conditions => ['tags.name = ? AND (todos.state = ? OR (contexts.hide = ? AND todos.state = ?))', @tag_name, 'project_hidden', true, 'active'])
:conditions => ['tags.name = ? AND (todos.state = ? OR (contexts.hide = ? AND todos.state = ?))', @tag_name, 'project_hidden', true, 'active'],
:order => 'todos.completed_at DESC, todos.created_at DESC')
@deferred = tag_collection.find(:all,
:conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'deferred'],
:order => 'show_from ASC, todos.created_at DESC')
# If you've set no_completed to zero, the completed items box isn't shown on
# the tag page
max_completed = current_user.prefs.show_number_completed
@done = tag_collection.find(:all,
:limit => max_completed,
:conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'completed'],
:order => 'todos.completed_at DESC')
@contexts = current_user.contexts.find(:all)
@contexts_to_show = @contexts.reject {|x| x.hide? }
@deferred = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'deferred'])
@page_title = "TRACKS::Tagged with \'#{@tag_name}\'"
# If you've set no_completed to zero, the completed items box isn't shown on
# the home page
max_completed = current_user.prefs.show_number_completed
@done = tag_collection.find(:all, :limit => max_completed, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'completed'])
# Set count badge to number of items with this tag
@not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size
@down_count = @count
# @default_project_context_name_map =
# build_default_project_context_name_map(@projects).to_json
respond_to do |format|
format.html {
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
}
format.m {
cookies[:mobile_url]=request.request_uri
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
render :action => "mobile_tag"
}
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.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['todos.due <= ?', due_today_date],
:order => "due")
@due_this_week = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date],
:order => "due")
@due_next_week = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date],
:order => "due")
@due_this_month = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date],
:order => "due")
@due_after_this_month = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['todos.due > ?', due_this_month_date],
:order => "due")
@count = current_user.todos.not_completed.are_due.count
respond_to do |format|
format.html
format.ics {
@due_all = current_user.todos.not_completed.are_due.find(:all, :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'])
@ -438,13 +536,17 @@ class TodosController < ApplicationController
end
def with_parent_resource_scope(&block)
@feed_title = "Actions "
if (params[:context_id])
@context = current_user.contexts.find_by_params(params)
@feed_title = @feed_title + "in context '#{@context.name}'"
Todo.send :with_scope, :find => {:conditions => ['todos.context_id = ?', @context.id]} do
yield
end
elsif (params[:project_id])
@project = current_user.projects.find_by_params(params)
@feed_title = @feed_title + "in project '#{@project.name}'"
@project_feed = true
Todo.send :with_scope, :find => {:conditions => ['todos.project_id = ?', @project.id]} do
yield
end
@ -608,7 +710,7 @@ class TodosController < ApplicationController
lambda do
@page_title = "All actions"
@home = true
cookies[:mobile_url]=request.request_uri
cookies[:mobile_url]= { :value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
determine_down_count
render :action => 'index'
@ -620,7 +722,7 @@ class TodosController < ApplicationController
render_rss_feed_for @todos, :feed => todo_feed_options,
:item => {
:title => :description,
:link => lambda { |t| context_url(t.context) },
:link => lambda { |t| @project_feed.nil? ? context_url(t.context) : project_url(t.project) },
:guid => lambda { |t| todo_url(t) },
:description => todo_feed_content
}
@ -628,7 +730,9 @@ class TodosController < ApplicationController
end
def todo_feed_options
Todo.feed_options(current_user)
options = Todo.feed_options(current_user)
options[:title] = @feed_title
return options
end
def todo_feed_content
@ -674,18 +778,85 @@ class TodosController < ApplicationController
['rss','atom','txt','ics'].include?(req.parameters[:format])
end
def check_for_next_todo
def check_for_next_todo(todo)
# check if this todo has a related recurring_todo. If so, create next todo
@new_recurring_todo = nil
@recurring_todo = nil
if @todo.from_recurring_todo?
@recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id)
date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due
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
@new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date)
new_recurring_todo = nil
recurring_todo = nil
if todo.from_recurring_todo?
recurring_todo = todo.recurring_todo
# check if there are active todos belonging to this recurring todo. only
# add new one if all active todos are completed
if recurring_todo.todos.active.count == 0
# 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
# if both due and show_from are nil, check for a next todo from now
date_to_check = Time.zone.now if date_to_check.nil?
if recurring_todo.active? && recurring_todo.has_next_todo(date_to_check)
# 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. We pick yesterday so
# that new todos due for today will be created instead of new todos
# for tomorrow.
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
end
return new_recurring_todo
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.not_completed.count(:all,
:conditions => ['todos.due <= ?', due_today_date])
when "due_this_week"
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date])
when "due_next_week"
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date])
when "due_this_month"
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date])
when "due_after_this_month"
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ?', due_this_month_date])
else
raise Exception.new, "unknown due id for calendar: '#{id}'"
end
end
class FindConditionBuilder

View file

@ -3,7 +3,7 @@ module RecurringTodosHelper
def recurrence_time_span(rt)
case rt.ends_on
when "no_end_date"
return ""
return rt.start_from.nil? ? "" : "from " + format_date(rt.start_from)
when "ends_on_number_of_times"
return "for "+rt.number_of_occurences.to_s + " times"
when "ends_on_end_date"
@ -11,7 +11,7 @@ module RecurringTodosHelper
ends = rt.end_date.nil? ? "" : " until " + format_date(rt.end_date)
return starts+ends
else
raise Exception.new, "unknown recurrence time span selection (#{self.ends_on})"
raise Exception.new, "unknown recurrence time span selection (#{self.ends_on})"
end
end
@ -28,10 +28,10 @@ module RecurringTodosHelper
def recurring_todo_tag_list
tags_except_starred = @recurring_todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
tag_list = tags_except_starred.collect{|t| "<span class=\"tag #{t.name.gsub(' ','-')}\">" +
tag_list = tags_except_starred.collect{|t| "<span class=\"tag #{t.name.gsub(' ','-')}\">" +
# link_to(t.name, :controller => "todos", :action => "tag", :id =>
# t.name) + TODO: tag view for recurring_todos (yet?)
t.name +
t.name +
"</span>"}.join('')
"<span class='tags'>#{tag_list}</span>"
end
@ -44,7 +44,7 @@ module RecurringTodosHelper
str
end
def recurring_todo_remote_star_icon
def recurring_todo_remote_star_icon
str = link_to( image_tag_for_star(@recurring_todo),
toggle_star_recurring_todo_path(@recurring_todo),
:class => "icon star_item", :title => "star the action '#{@recurring_todo.description}'")

View file

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

View file

@ -2,7 +2,10 @@ class Context < ActiveRecord::Base
has_many :todos, :dependent => :delete_all, :include => :project, :order => "todos.completed_at DESC"
belongs_to :user
named_scope :active, :conditions => { :hide => false }
named_scope :hidden, :conditions => { :hide => true }
acts_as_list :scope => :user
extend NamePartFinder
include Tracks::TodoList

View file

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

View file

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

View file

@ -3,6 +3,10 @@ class Project < ActiveRecord::Base
has_many :notes, :dependent => :delete_all, :order => "created_at DESC"
belongs_to :default_context, :class_name => "Context", :foreign_key => "default_context_id"
belongs_to :user
named_scope :active, :conditions => { :state => 'active' }
named_scope :hidden, :conditions => { :state => 'hidden' }
named_scope :completed, :conditions => { :state => 'completed'}
validates_presence_of :name, :message => "project must have a name"
validates_length_of :name, :maximum => 255, :message => "project name must be less than 256 characters"
@ -16,7 +20,7 @@ class Project < ActiveRecord::Base
state :active
state :hidden, :enter => :hide_todos, :exit => :unhide_todos
state :completed, :enter => Proc.new { |p| p.completed_at = Time.now.utc }, :exit => Proc.new { |p| p.completed_at = nil }
state :completed, :enter => Proc.new { |p| p.completed_at = Time.zone.now }, :exit => Proc.new { |p| p.completed_at = nil }
event :activate do
transitions :to => :active, :from => [:hidden, :completed]

View file

@ -4,6 +4,8 @@ class RecurringTodo < ActiveRecord::Base
belongs_to :project
belongs_to :user
has_many :todos
attr_protected :user
acts_as_state_machine :initial => :active, :column => 'state'
@ -12,7 +14,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
@ -20,6 +22,9 @@ class RecurringTodo < ActiveRecord::Base
validates_presence_of :context
named_scope :active, :conditions => { :state => 'active'}
named_scope :completed, :conditions => { :state => 'completed'}
event :complete do
transitions :to => :completed, :from => [:active]
end
@ -29,7 +34,7 @@ class RecurringTodo < ActiveRecord::Base
end
# the following recurrence patterns can be stored:
#
#
# daily todos - recurrence_period = 'daily'
# every nth day - nth stored in every_other1
# every work day - only_work_days = true
@ -243,7 +248,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 +262,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
@ -392,13 +397,21 @@ class RecurringTodo < ActiveRecord::Base
# previous is the due date of the previous todo or it is the completed_at
# date when the completed_at date is after due_date (i.e. you did not make
# the due date in time)
#
#
# assumes self.recurring_period == 'daily'
# 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
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
end
if self.only_work_days
@ -419,8 +432,9 @@ class RecurringTodo < ActiveRecord::Base
end
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
@ -428,7 +442,13 @@ class RecurringTodo < ActiveRecord::Base
# that week
start += self.every_other1.week
end
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
end
# check if there are any days left this week for the next todo
start.wday().upto 6 do |i|
return start + (i-start.wday()).days unless self.every_day[i,1] == ' '
@ -447,29 +467,44 @@ class RecurringTodo < ActiveRecord::Base
end
def get_monthly_date(previous)
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
else
start = previous
end
start = determine_start(previous)
day = self.every_other1
n = self.every_other2
case self.recurrence_selector
when 0 # specific day of the month
if start.mday >= day
if start.mday >= day
# there is no next day n in this month, search in next month
start += n.months
#
# start += n.months
#
# The above seems to not work. Fiddle with timezone. Looks like we hit a
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
# local timezone.
#
# TODO: recheck if future rails versions have this problem too
start = Time.utc(start.year, start.month, start.day)+n.months
start = Time.zone.local(start.year, start.month, start.day)
# 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)
if the_next.nil? || the_next <= start
# the nth day is already passed in this month, go to next month and try
# again
the_next = the_next+n.months
# fiddle with timezone. Looks like we hit a bug in rails here where
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
# calculate in UTC and convert back to local timezone.
# TODO: recheck if future rails versions have this problem too
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
# TODO: if there is still no match, start will be set to nil. if we ever
# support 5th day of the month, we need to handle this case
the_next = get_xth_day_of_month(self.every_other3, self.every_count, the_next.month, the_next.year)
@ -483,14 +518,18 @@ class RecurringTodo < ActiveRecord::Base
def get_xth_day_of_month(x, weekday, month, year)
if x == 5
# last -> count backwards
# last -> count backwards. use UTC to avoid strange timezone oddities
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
last_day = Time.utc(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
return last_day
# convert back to local timezone
return Time.zone.local(last_day.year, last_day.month, last_day.day)
else
# 1-4th -> count upwards
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
# tz+0000
start = Time.utc(year,month,1)
n = x
while n > 0
@ -500,32 +539,31 @@ class RecurringTodo < ActiveRecord::Base
n -= 1
start += 1.day unless n==0
end
return start
# convert back to local timezone
return Time.zone.local(start.year, start.month, start.day)
end
end
def get_yearly_date(previous)
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
else
start = previous
end
start = determine_start(previous)
day = self.every_other1
month = self.every_other2
case self.recurrence_selector
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
if start.month > month || (start.month == month && start.day >= day)
# if there is no next month n and day m in this year, search in next
# year
start = Time.zone.local(start.year+1, month, 1)
else
# if there is a next month n, stay in this year
start = Time.zone.local(start.year, month, 1)
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)
@ -592,4 +630,19 @@ class RecurringTodo < ActiveRecord::Base
errors.add("", "At least one day must be selected in the weekly pattern") if self.every_day == ' '
end
def determine_start(previous)
if previous.nil?
start = self.start_from.nil? ? Time.zone.now : self.start_from
else
start = previous
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
end
return start
end
end

View file

@ -4,7 +4,11 @@ class Todo < ActiveRecord::Base
belongs_to :project
belongs_to :user
belongs_to :recurring_todo
named_scope :active, :conditions => { :state => 'active' }
named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed']
named_scope :are_due, :conditions => ['NOT todos.due IS NULL']
STARRED_TAG_NAME = "starred"
acts_as_state_machine :initial => :active, :column => 'state'
@ -13,7 +17,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 +72,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 +131,46 @@ class Todo < ActiveRecord::Base
return self.recurring_todo_id != nil
end
end
# Rich Todo API
def self.from_rich_message(user, default_context_id, description, notes)
fields = description.match /([^>@]*)@?([^>]*)>?(.*)/
description = fields[1].strip
context = fields[2].strip
project = fields[3].strip
context = nil if context == ""
project = nil if project == ""
context_id = default_context_id
unless(context.nil?)
found_context = user.active_contexts.find_by_namepart(context)
found_context = user.contexts.find_by_namepart(context) if found_context.nil?
context_id = found_context.id unless found_context.nil?
end
unless user.contexts.exists? context_id
raise(CannotAccessContext, "Cannot access a context that does not belong to this user.")
end
project_id = nil
unless(project.blank?)
if(project[0..3].downcase == "new:")
found_project = user.projects.build
found_project.name = project[4..255+4].strip
found_project.save!
else
found_project = user.active_projects.find_by_namepart(project)
found_project = user.projects.find_by_namepart(project) if found_project.nil?
end
project_id = found_project.id unless found_project.nil?
end
todo = user.todos.build
todo.description = description
todo.notes = notes
todo.context_id = context_id
todo.project_id = project_id unless project_id.nil?
return todo
end
end

View file

@ -51,6 +51,21 @@ class User < ActiveRecord::Base
self.update_positions(projects.map{ |p| p.id })
return projects
end
def actionize(user_id, scope_conditions = {})
@state = scope_conditions[:state]
query_state = ""
query_state = "AND project.state = '" + @state +"' "if @state
projects = Project.find_by_sql([
"SELECT project.id, count(todo.id) as p_count " +
"FROM projects as project " +
"LEFT OUTER JOIN todos as todo ON todo.project_id = project.id "+
"WHERE project.user_id = ? AND NOT todo.state='completed' " +
query_state +
" GROUP BY project.id ORDER by p_count DESC",user_id])
self.update_positions(projects.map{ |p| p.id })
projects = find(:all, :conditions => scope_conditions)
return projects
end
end
has_many :active_projects,
:class_name => 'Project',
@ -59,7 +74,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,7 +86,7 @@ class User < ActiveRecord::Base
:conditions => [ 'state = ?', 'deferred' ],
:order => 'show_from ASC, todos.created_at DESC' do
def find_and_activate_ready
find(:all, :conditions => ['show_from <= ?', proxy_owner.time ]).collect { |t| t.activate! }
find(:all, :conditions => ['show_from <= ?', Time.zone.now ]).collect { |t| t.activate! }
end
end
has_many :completed_todos,
@ -169,7 +184,11 @@ class User < ActiveRecord::Base
end
def date
time.to_date
time.midnight
end
def at_midnight(date)
return TimeZone[prefs.time_zone].local(date.year, date.month, date.day, 0, 0, 0)
end
def generate_token

View file

@ -25,7 +25,9 @@
<% end -%>
<% if source_view_is :context %>
<span class="in_place_editor_field" id="context_name_in_place_editor"><%= context.name %></span>
<%= in_place_editor 'context_name_in_place_editor', { :url => { :controller => 'contexts', :action => 'update', :id => context.id, :field => 'name', :wants_render => false, :escape => false} , :options=>"{method:'put'}" } %>
<%= in_place_editor 'context_name_in_place_editor', {
:url => { :controller => 'contexts', :action => 'update', :id => context.id, :field => 'name', :update_context_name => true, :escape => false},
:options=>"{method:'put'}", :script => true } %>
<% else %>
<%= link_to_context( context ) %>
<% end %>

View file

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

View file

@ -0,0 +1,10 @@
page['context_name_in_place_editor'].replace_html @context.name
page['default_context_name_id'].value = @context.name
page['todo_context_name'].value = @context.name
# renew context auto complete array
page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true"
status_message = "Name of context was changed"
page.notify :notice, status_message, 5.0

View file

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

View file

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

View file

@ -64,10 +64,13 @@
Active projects with their actions
</li>
<li><h4>Feeds for incomplete actions in a specific context:</h4>
<% if @active_contexts.empty? && @hidden_contexts.empty? -%>
<ul><li>There need to be at least one context before you can request a feed</li></ul>
<% else -%>
<ul>
<li>Step 1 - Choose the context you want a feed of:
<select name="feed-contexts" id="feed-contexts">
<%= options_from_collection_for_select(@active_contexts, "id", "name", @active_contexts.first.id) -%>
<%= options_from_collection_for_select(@active_contexts, "id", "name", @active_contexts.first.id) unless @active_projects.empty?-%>
<%= options_from_collection_for_select(@hidden_contexts, "id", "name") -%>
</select>
<%= observe_field "feed-contexts", :update => "feeds-for-context",
@ -80,17 +83,21 @@
<li>Step 2 - Select the feed for this context
<div id="feedicons-context">
<div id="feeds-for-context">
<%= render :partial => 'feed_for_context', :locals => { :context => @active_contexts.first } %>
<%= render :partial => 'feed_for_context', :locals => { :context => @active_contexts.empty? ? @hidden_contexts.first : @active_contexts.first } %>
</div>
</div>
</li>
</ul>
<% end -%>
</li>
<li><h4>Feeds for incomplete actions in a specific project:</h4>
<% if @active_projects.empty? && @hidden_projects.empty? -%>
<ul><li>There need to be at least one project before you can request a feed</li></ul>
<% else -%>
<ul>
<li>Step 1 - Choose the project you want a feed of:
<select name="feed-projects" id="feed-projects">
<%= options_from_collection_for_select(@active_projects, "id", "name", @active_projects.first.id) -%>
<%= options_from_collection_for_select(@active_projects, "id", "name", @active_projects.first.id) unless @active_projects.empty?-%>
<%= options_from_collection_for_select(@hidden_projects, "id", "name") -%>
<%= options_from_collection_for_select(@completed_projects, "id", "name") -%>
</select>
@ -104,11 +111,12 @@
<li>Step 2 - Select the feed for this project
<div id="feedicons-project">
<div id="feeds-for-project">
<%= render :partial => 'feed_for_project', :locals => { :project => @active_projects.first } %>
<%= render :partial => 'feed_for_project', :locals => { :project => @active_projects.empty? ? @hidden_projects.first : @active_projects.first } %>
</div>
</div>
</li>
</ul>
<% end -%>
</li>
</ul>
</div>

View file

@ -8,9 +8,9 @@ the newly created action.
*)
(* Edit appropriately for your setup *)
property myUsername to "<%= current_user.login %>"
property myToken to "<%= current_user.token %>"
property myContextID to <%= context.id %> (* <%= context.name %> *)
property myUsername : "<%= current_user.login %>"
property myToken : "<%= current_user.token %>"
property myContextID : <%= context.id %> (* <%= context.name %> *)
-- this string is used when the message subject is empty
property emptySubject : "No Subject Specified"

View file

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

View file

@ -73,7 +73,7 @@
<pre>
<code>
$ curl -u username:p4ssw0rd -H "Content-Type: text/xml" \
-d "project[name]=Build a treehouse for the kids" \
-d "&lt;project&gt;&lt;name&gt;Build a treehouse for the kids&lt;/name&gt;&lt;/project&gt;" \
<%= home_url %>projects.xml -i
&gt;&gt; HTTP/1.1 201 Created
Location: <%= home_url %>projects/65.xml
@ -86,7 +86,7 @@ Location: <%= home_url %>projects/65.xml
<pre>
<code>
$ curl -u username:p4ssw0rd -H "Content-Type: text/xml" \
-d "todo[description]=Model treehouse in SketchUp&#38;todo[context_id]=2&#38;todo[project_id]=65" \
-d "&lt;todo&gt;&lt;description&gt;Model treehouse in SketchUp&lt;/description&gt;&lt;context_id&gt;2&lt;/context_id&gt;&lt;project_id&gt;65&lt;project_id&gt;" \
<%= home_url %>todos.xml -i
&gt;&gt; HTTP/1.1 201 Created
Location: <%= home_url %>todos/452.xml
@ -99,7 +99,7 @@ Location: <%= home_url %>todos/452.xml
<pre>
<code>
$ curl -u username:p4ssw0rd -H "Content-Type: text/xml" -X PUT \
-d "todo[notes]=use maple texture" \
-d "&lt;todo&gt;&lt;notes&gt;use maple texture&lt;/notes&gt;&lt;/todos&gt;" \
<%= home_url %>todos/452.xml -i
&gt;&gt; HTTP/1.1 200 OK
...

View file

@ -16,9 +16,6 @@
<script type="text/javascript">
window.onload=function(){
Nifty("div#todo_new_action_container","normal");
Nifty("div#project_new_project_container","normal");
Nifty("div#context_new_container","normal");
Nifty("div#recurring_new_container","normal");
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
}
</script>
@ -49,7 +46,7 @@ window.onload=function(){
<li><%= navigation_link("Home", home_path, {:accesskey => "t", :title => "Home"} ) %></li>
<li><%= navigation_link( "Contexts", contexts_path, {:accesskey=>"c", :title=>"Contexts"} ) %></li>
<li><%= navigation_link( "Projects", projects_path, {:accesskey=>"p", :title=>"Projects"} ) %></li>
<li><%= navigation_link( "Tickler", tickler_path, :title => "Tickler" ) %></li>
<li><%= navigation_link( "Tickler", tickler_path, {:accesskey =>"k", :title => "Tickler"} ) %></li>
<li><%= navigation_link( "Done", done_path, {:accesskey=>"d", :title=>"Completed"} ) %></li>
<li><%= navigation_link( "Notes", notes_path, {:accesskey => "o", :title => "Show all notes"} ) %></li>
<li><%= navigation_link( "Preferences", preferences_path, {:accesskey => "u", :title => "Show my preferences"} ) %></li>
@ -57,6 +54,7 @@ window.onload=function(){
<% if current_user.is_admin? -%>
<li><%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %></li>
<% end -%>
<li><%= navigation_link(image_tag("x-office-calendar.png", :size => "16X16", :border => 0), calendar_path, :title => "Calendar of due actions" ) %></li>
<li><%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %></li>
<li><%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %></li>
<li><%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %></li>

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<% @not_done = project.not_done_todos -%>
<% #@not_done = project.not_done_todos -%>
<div id="p<%= project.id %>" class="container project">
<h2>
@ -6,7 +6,9 @@
<a href="#" class="container_toggle" id="toggle_p<%= project.id %>"><%= image_tag("collapse.png") %></a>
<% end %>
<span class="in_place_editor_field" id="project_name_in_place_editor"><%= project.name %></span>
<%= in_place_editor 'project_name_in_place_editor', { :url => { :controller => 'projects', :action => 'update', :id => project.id, :field => 'name', :wants_render => false, :escape => false} , :options=>"{method:'put'}" } %>
<%= in_place_editor 'project_name_in_place_editor', {
:url => { :controller => 'projects', :action => 'update', :id => project.id, :field => 'name', :update_project_name => true, :escape => false} ,
:options=>"{method:'put'}", :script => true} %>
</h2>
<% unless project.description.blank? -%>
<div class="project_description"><%= sanitize(project.description) %></div>

View file

@ -1,7 +1,8 @@
<div class="project-state-group" id="list-<%= state %>-projects-container" <%= " style=\"display:none\"" if project_state_group.empty? %>>
<h2><span id="<%= state %>-projects-count" class="badge"><%= project_state_group.length %></span><%= state.titlecase %> Projects</h2>
<div class="menu_sort"><span class="sort_separator">Sort&nbsp;</span>
<div class="alpha_sort">
<%= link_to("Sort Alphabetically", alphabetize_projects_path(:state => state),
<%= link_to("Alphabetically", alphabetize_projects_path(:state => state),
:class => "alphabetize_link", :title => "Sort these projects alphabetically") %>
<% apply_behavior '.alphabetize_link:click', :prevent_default => true do |page, element|
page.confirming 'Are you sure that you want to sort these projects alphabetically? This will replace the existing sort order.' do
@ -10,10 +11,21 @@
page << remote_to_href(:complete => "alphaSort.stopWaiting()")
end
end
%></div><span class="sort_separator">&nbsp;|&nbsp;</span><div class="tasks_sort">
<%= link_to("By number of tasks", actionize_projects_path(:state => state),
:class => "actionize_link", :title => "Sort these projects by number of tasks") %>
<% apply_behavior '.actionize_link:click', :prevent_default => true do |page, element|
page.confirming 'Are you sure that you want to sort these projects by the number of tasks? This will replace the existing sort order.' do
page << "tasksSort = this.up('.tasks_sort');
tasksSort.startWaiting();"
page << remote_to_href(:complete => "tasksSort.stopWaiting()")
end
end
%></div>
</div>
<div id="list-<%= state %>-projects" class="project-list">
<%= render :partial => 'project_listing', :collection => project_state_group %>
</div>
<%= sortable_element "list-#{state}-projects", get_listing_sortable_options("list-#{state}-projects") %>
</div>
</div>

View file

@ -0,0 +1,6 @@
list_id = "list-#{@state}-projects"
page.replace_html list_id,
:partial => 'project_listing',
:collection => @projects
page.sortable list_id, get_listing_sortable_options(list_id)

View file

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

View file

@ -0,0 +1,10 @@
page['project_name_in_place_editor'].replace_html @project.name
page['default_project_name_id'].value = @project.name
page['todo_project_name'].value = @project.name
# renew project auto complete array
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true"
status_message = "Name of project was changed"
page.notify :notice, status_message, 5.0

View file

@ -6,7 +6,16 @@
<div class="description">
<span class="todo.descr"><%= sanitize(recurring_todo.description) %></span> <%= recurring_todo_tag_list %>
<span class='recurrence_pattern'>
[<%=recurrence_target(recurring_todo)%> <%= recurring_todo.recurrence_pattern %> <%= recurrence_time_span(recurring_todo) %>]
<%
rt = recurrence_target(recurring_todo)
rp = recurring_todo.recurrence_pattern
# only add space if recurrence_pattern has content
rp = " " + rp if !rp.nil?
rts = recurrence_time_span(recurring_todo)
# only add space if recurrence_time_span has content
rts = " " + rts if !(rts == "")
%>
[<%=rt%><%=rp%><%=rts%>]
</span>
</div>
</div>

View file

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

View file

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

View file

@ -8,7 +8,7 @@
<div id="todo_new_action_container">
<div id="toggle_action_new" class="hide_form">
<a title="Hide new action form" accesskey="n">&laquo; Hide form</a>
<a title="Hide new action form" accesskey="n" href="#">&laquo; Hide form</a>
<% apply_behavior '#toggle_action_new a:click', :prevent_default => true do |page|
page << "TracksForm.toggle('toggle_action_new', 'todo_new_action', 'todo-form-new-action',
'&laquo; Hide form', 'Hide next action form',
@ -34,6 +34,7 @@
<label for="todo_notes">Notes</label>
<%= text_area( "todo", "notes", "cols" => 29, "rows" => 6, "tabindex" => 2) %>
<input id="default_project_name_id" name="default_project_name" type="hidden" value="<%=@initial_project_name-%>" />
<label for="todo_project_name">Project</label>
<input id="todo_project_name" name="project_name" autocomplete="off" tabindex="3" size="30" type="text" value="<%= @initial_project_name %>" />
<div class="page_name_auto_complete" id="project_list" style="display:none"></div>

View file

@ -1,7 +1,8 @@
<div id="error_status"><%= error_messages_for("todo", :object_name => 'action') %></div>
<%= hidden_field( "todo", "id" ) %>
<%= source_view_tag( @source_view ) %>
<%= hidden_field( "todo", "id" ) -%>
<%= source_view_tag( @source_view ) -%>
<%= "<INPUT TYPE=\"hidden\" name=\"_tag_name\" value=\""+ @tag_name+"\">" unless @tag_name.nil? -%>
<label for="<%= dom_id(@todo, 'description') %>">Description</label>
<%= text_field( "todo", "description", "size" => 30, "tabindex" => 8) %>
@ -48,16 +49,16 @@ Event.observe($('<%= dom_id(@todo, 'context_name') %>'), "click", <%= dom_id(@to
<div class="due_input">
<label for="<%= dom_id(@todo, 'due_label') %>">Due</label>
<%= date_field_tag("todo[due]", dom_id(@todo, 'due'), format_date(@todo.due), "tabindex" => 13) %>
<a href="#" id="<%= dom_id(@todo, 'due_x') %>" class="date_clear">
<%= image_tag("cancel.png", :alt => "") %>
<a href="#" id="<%= dom_id(@todo, 'due_x') %>" class="date_clear" title="Clear due date">
<%= image_tag("delete_off.png", :alt => "Clear due date") %>
</a>
</div>
<div class="show_from_input">
<label for="<%= dom_id(@todo, 'show_from') %>">Show from</label>
<%= date_field_tag("todo[show_from]", dom_id(@todo, 'show_from'), format_date(@todo.show_from), "tabindex" => 14) %>
<a href="#" id="<%= dom_id(@todo, 'show_from_x') %>" class="date_clear">
<%= image_tag("cancel.png", :alt => "") %>
<a href="#" id="<%= dom_id(@todo, 'show_from_x') %>" class="date_clear" title="Clear show from date">
<%= image_tag("delete_off.png", :alt => "Clear show from date") %>
</a>
</div>

View file

@ -33,8 +33,8 @@ end %>
<h2><label for="tag_list">Tags (separate with commas)</label></h2>
<%= text_field_tag "tag_list", @tag_list_text, :size => 30, :tabindex => 6 %>
<h2><label for="todo_due">Due</label></h2>
<%= date_select("todo", "due", :order => [:day, :month, :year],
:start_year => this_year, :include_blank => true) %>
<%= date_select("todo", "due", {:order => [:day, :month, :year],
:start_year => this_year, :include_blank => true}, :tabindex => 7) %>
<h2><label for="todo_show_from">Show from</label></h2>
<%= date_select("todo", "show_from", :order => [:day, :month, :year],
:start_year => this_year, :include_blank => true) %>
<%= date_select("todo", "show_from", {:order => [:day, :month, :year],
:start_year => this_year, :include_blank => true}, :tabindex => 8) %>

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ if @saved
page['badge_count'].replace_html @down_count
page.send :record, "Form.reset('todo-form-new-action');Form.focusFirstElement('todo-form-new-action')"
page['todo_context_name'].value = @initial_context_name
page['todo_project_name'].value = @initial_project_name
page << "updateContextNamesForAutoComplete(#{context_names_for_autocomplete})" if @new_context_created
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}" if @new_project_created
if should_show_new_item()

View file

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

View file

@ -1,5 +1,5 @@
<% form_tag formatted_todos_path(:m), :method => :post do %>
<%= render :partial => 'edit_mobile' %>
<p><input type="submit" value="Create" tabindex="6" /></p>
<p><input type="submit" value="Create" tabindex="12" accesskey="#" /></p>
<% end -%>
<%= link_to "Back", @return_path %>

View file

@ -1,5 +1,5 @@
<% form_tag formatted_todo_path(@todo, :m), :method => :put do %>
<%= render :partial => 'edit_mobile', :locals => { :parent_container_type => "show_mobile" } %>
<p><input type="submit" value="Update" tabindex="6" /></p>
<p><input type="submit" value="Update" tabindex="6" accesskey="#" /></p>
<% end -%>
<%= link_to "Cancel", @return_path %>

View file

@ -1,5 +1,6 @@
if @saved
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
if @todo.completed?
# completed todos move from their context to the completed container
@ -23,10 +24,12 @@ if @saved
page.insert_html :bottom, item_container_id(@new_recurring_todo), :partial => 'todos/todo', :locals => { :todo => @new_recurring_todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@new_recurring_todo, 'line'), {'startcolor' => "'#99ff99'"}
else
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?
if @todo.recurring_todo.todos.active.count == 0
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
end
else
# todo is activated from completed container
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo)

View file

@ -6,19 +6,28 @@ if @saved
status_message = 'Added new context / ' + status_message if @new_context_created
page.notify :notice, status_message, 5.0
# #update auto completer arrays for context and project
# update auto completer arrays for context and project
page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true" if @new_context_created
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created
if source_view_is_one_of(:todo, :context, :tag)
if @context_changed || @todo.deferred?
page[@todo].remove
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
@ -92,6 +101,23 @@ if @saved
elsif source_view_is :stats
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
elsif source_view_is :calendar
if @due_date_changed
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
page.hide "empty_"+@new_due_id
page.insert_html :bottom, @new_due_id, :partial => 'todos/todo'
page.visual_effect :highlight, dom_id(@todo), :duration => 3
else
if @todo.due.nil?
# due date removed
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
end
else
logger.error "unexpected source_view '#{params[:_source_view]}'"
end

View file

@ -93,8 +93,17 @@ if (AUTHENTICATION_SCHEMES.include? 'open_id')
#requires ruby-openid gem to be installed
end
# setting this to true will make the cookies only available over HTTPS
TRACKS_COOKIES_SECURE = false
MOBILE_CONTENT_TYPE = 'tracks/mobile'
Mime::Type.register(MOBILE_CONTENT_TYPE, :m)
TRACKS_VERSION='1.7-devel'
tracks_version='1.7-devel'
# comment out next two lines if you do not want (or can not) the date of the
# last git commit in the footer
info=`git log --pretty=format:"%ai" -1`
tracks_version=tracks_version + ' ('+info+')'
TRACKS_VERSION=tracks_version

View file

@ -28,6 +28,10 @@ ActionController::Routing::Routes.draw do |map|
projects.resources :todos, :name_prefix => "project_"
end
map.resources :projects, :collection => {:order => :post, :actionize => :post} do |projects|
projects.resources :todos, :name_prefix => "project_"
end
map.resources :todos,
:member => {:toggle_check => :put, :toggle_star => :put},
:collection => {:check_deferred => :post, :filter_to_context => :post, :filter_to_project => :post}
@ -43,9 +47,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 +62,10 @@ ActionController::Routing::Routes.draw do |map|
map.feeds 'feeds', :controller => 'feedlist', :action => 'index'
map.feeds 'feeds.m', :controller => 'feedlist', :action => 'index', :format => 'm'
if Rails.env == 'test'
map.connect '/selenium_helper/login', :controller => 'selenium_helper', :action => 'login'
end
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
map.integrations 'integrations', :controller => 'integrations', :action => 'index'

View file

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

View file

@ -0,0 +1,39 @@
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|
if !user.prefs ## ugly hack for strange edge-case of not having preferences object
user.instance_eval do
def at_midnight(date)
return Time.zone.local(date.year, date.month, date.day, 0, 0, 0)
end
def time
Time.zone.now
end
end
end
user.todos.each do |todo|
todo[:show_from] = user.at_midnight(todo.show_from) unless todo.show_from.nil?
todo[:due] = user.at_midnight(todo.due) unless todo.due.nil?
todo.save_with_validation(false)
end
user.recurring_todos.each do |todo|
todo[:start_from] = user.at_midnight(todo.start_from) unless todo.start_from.nil?
todo[:end_date] = user.at_midnight(todo.end_date) unless todo.end_date.nil?
todo.save_with_validation(false)
end
end
end
def self.down
change_column :todos, :show_from, :date
change_column :todos, :due, :date
change_column :recurring_todos, :start_from, :date
change_column :recurring_todos, :end_date, :date
end
end

View file

@ -0,0 +1,10 @@
class AddUpdatedAtToTodos < ActiveRecord::Migration
def self.up
add_column :todos, :updated_at, :timestamp
execute 'update todos set updated_at = created_at where completed_at IS NULL'
execute 'update todos set updated_at = completed_at where NOT completed_at IS NULL'
end
def self.down
remove_column :todos, :updated_at
end
end

Binary file not shown.

Binary file not shown.

View file

@ -2,7 +2,7 @@
* Homepage: http://www.rousette.org.uk/projects/
* Author: bsag (http://www.rousette.org.uk/)
* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson
* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson, Eric Allen
* Version: 1.6
* Copyright: (cc) 2004-2008 rousette.org.uk
* License: GNU GPL
@ -13,6 +13,21 @@ 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)
7. You can now sort projects on number of active todos
Under the hood:
1. Move selenium tests to RSpec
2. Bugfixes
== Version 1.6
1. upgrade to rails 2.0.2
2. new mobile interface (with some iPhone compatibility fixes)

View file

@ -1,113 +1,113 @@
module AuthenticatedTestHelper
# Sets the current user in the session from the user fixtures.
def login_as(user)
@request.session['user_id'] = user ? users(user).id : nil
end
def content_type(type)
@request.env['Content-Type'] = type
end
def accept(accept)
@request.env["HTTP_ACCEPT"] = accept
end
def authorize_as(user)
if user
@request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}"
accept 'application/xml'
content_type 'application/xml'
else
@request.env["HTTP_AUTHORIZATION"] = nil
accept nil
content_type nil
end
end
# http://project.ioni.st/post/217#post-217
#
# def test_new_publication
# assert_difference(Publication, :count) do
# post :create, :publication => {...}
# # ...
# end
# end
#
def assert_difference(object, method = nil, difference = 1)
initial_value = object.send(method)
yield
assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
end
def assert_no_difference(object, method, &block)
assert_difference object, method, 0, &block
end
# Assert the block redirects to the login
#
# assert_requires_login(:bob) { |c| c.get :edit, :id => 1 }
#
def assert_requires_login(login = nil)
yield HttpLoginProxy.new(self, login)
end
def assert_http_authentication_required(login = nil)
yield XmlLoginProxy.new(self, login)
end
def reset!(*instance_vars)
instance_vars = [:controller, :request, :response] unless instance_vars.any?
instance_vars.collect! { |v| "@#{v}".to_sym }
instance_vars.each do |var|
instance_variable_set(var, instance_variable_get(var).class.new)
end
end
end
class BaseLoginProxy
attr_reader :controller
attr_reader :options
def initialize(controller, login)
@controller = controller
@login = login
end
private
def authenticated
raise NotImplementedError
end
def check
raise NotImplementedError
end
def method_missing(method, *args)
@controller.reset!
authenticate
@controller.send(method, *args)
check
end
end
class HttpLoginProxy < BaseLoginProxy
protected
def authenticate
@controller.login_as @login if @login
end
def check
@controller.assert_redirected_to :controller => 'account', :action => 'login'
end
end
class XmlLoginProxy < BaseLoginProxy
protected
def authenticate
@controller.accept 'application/xml'
@controller.authorize_as @login if @login
end
def check
@controller.assert_response 401
end
module AuthenticatedTestHelper
# Sets the current user in the session from the user fixtures.
def login_as(user)
@request.session['user_id'] = user ? users(user).id : nil
end
def content_type(type)
@request.env['Content-Type'] = type
end
def accept(accept)
@request.env["HTTP_ACCEPT"] = accept
end
def authorize_as(user)
if user
@request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}"
accept 'application/xml'
content_type 'application/xml'
else
@request.env["HTTP_AUTHORIZATION"] = nil
accept nil
content_type nil
end
end
# http://project.ioni.st/post/217#post-217
#
# def test_new_publication
# assert_difference(Publication, :count) do
# post :create, :publication => {...}
# # ...
# end
# end
#
def assert_difference(object, method = nil, difference = 1)
initial_value = object.send(method)
yield
assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
end
def assert_no_difference(object, method, &block)
assert_difference object, method, 0, &block
end
# Assert the block redirects to the login
#
# assert_requires_login(:bob) { |c| c.get :edit, :id => 1 }
#
def assert_requires_login(login = nil)
yield HttpLoginProxy.new(self, login)
end
def assert_http_authentication_required(login = nil)
yield XmlLoginProxy.new(self, login)
end
def reset!(*instance_vars)
instance_vars = [:controller, :request, :response] unless instance_vars.any?
instance_vars.collect! { |v| "@#{v}".to_sym }
instance_vars.each do |var|
instance_variable_set(var, instance_variable_get(var).class.new)
end
end
end
class BaseLoginProxy
attr_reader :controller
attr_reader :options
def initialize(controller, login)
@controller = controller
@login = login
end
private
def authenticated
raise NotImplementedError
end
def check
raise NotImplementedError
end
def method_missing(method, *args)
@controller.reset!
authenticate
@controller.send(method, *args)
check
end
end
class HttpLoginProxy < BaseLoginProxy
protected
def authenticate
@controller.login_as @login if @login
end
def check
@controller.assert_redirected_to :controller => 'account', :action => 'login'
end
end
class XmlLoginProxy < BaseLoginProxy
protected
def authenticate
@controller.accept 'application/xml'
@controller.authorize_as @login if @login
end
def check
@controller.assert_response 401
end
end

View file

@ -48,7 +48,7 @@ module LoginSystem
session['user_id'] = user.id
set_current_user(user)
current_user.remember_me
cookies[:auth_token] = { :value => current_user.remember_token , :expires => current_user.remember_token_expires_at }
cookies[:auth_token] = { :value => current_user.remember_token , :expires => current_user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
flash[:notice] = "Logged in successfully. Welcome back!"
end
end

BIN
public/images/defer_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

BIN
public/images/defer_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

View file

@ -211,7 +211,7 @@ Autocompleter.Base = Class.create({
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(true);
this.getEntry(this.index).scrollIntoView(false);
},
markNext: function() {

View file

@ -407,7 +407,7 @@ if(event.keyCode==Event.KEY_TAB||event.keyCode==Event.KEY_RETURN||(Prototype.Bro
{this.index=element.autocompleteIndex;this.render();}
Event.stop(event);},onClick:function(event){var element=Event.findElement(event,'LI');this.index=element.autocompleteIndex;this.selectEntry();this.hide();},onBlur:function(event){setTimeout(this.hide.bind(this),250);this.hasFocus=false;this.active=false;},render:function(){if(this.entryCount>0){for(var i=0;i<this.entryCount;i++)
this.index==i?Element.addClassName(this.getEntry(i),"selected"):Element.removeClassName(this.getEntry(i),"selected");if(this.hasFocus){this.show();this.active=true;}}else{this.active=false;this.hide();}},markPrevious:function(){if(this.index>0)this.index--
else this.index=this.entryCount-1;this.getEntry(this.index).scrollIntoView(true);},markNext:function(){if(this.index<this.entryCount-1)this.index++
else this.index=this.entryCount-1;this.getEntry(this.index).scrollIntoView(false);},markNext:function(){if(this.index<this.entryCount-1)this.index++
else this.index=0;this.getEntry(this.index).scrollIntoView(false);},getEntry:function(index){return this.update.firstChild.childNodes[index];},getCurrentEntry:function(){return this.getEntry(this.index);},selectEntry:function(){this.active=false;this.updateElement(this.getCurrentEntry());},updateElement:function(selectedElement){if(this.options.updateElement){this.options.updateElement(selectedElement);return;}
var value='';if(this.options.select){var nodes=$(selectedElement).select('.'+this.options.select)||[];if(nodes.length>0)value=Element.collectTextNodes(nodes[0],this.options.select);}else
value=Element.collectTextNodesIgnoreClass(selectedElement,'informal');var bounds=this.getTokenBounds();if(bounds[0]!=-1){var newValue=this.element.value.substr(0,bounds[0]);var whitespace=this.element.value.substr(bounds[0]).match(/^\s+/);if(whitespace)

View file

@ -692,11 +692,14 @@ div#list-active-projects, div#list-hidden-projects, div#list-completed-projects,
margin:20px 0px 8px 13px
}
div.alpha_sort {
div.menu_sort {
margin-top:-20px;
float:right;
}
div.alpha_sort, div.tasks_sort,span.sort_separator {
float:left;
}
.container td {
border: none;
@ -808,6 +811,14 @@ input#go_to_project, input#context_hide {
width:100%;
}
.edit_todo_form .Date {
width:89%;
}
.edit_todo_form a.date_clear:hover {
background: #CCCCCC;
}
.edit_todo_form .tag_list_label {
clear:both;
}
@ -1193,8 +1204,9 @@ body.integrations textarea {
width:80%;
background-color:#ddd;
}
.date_clear
{
float: right;
}
.defer-container {
float:right;
}
.defer-container a:hover {
background-color: inherit;
}

View file

@ -129,7 +129,8 @@ div.buttons, div.buttons a, div.buttons a:hover {text-align: right; margin-right
div#list-active-projects, div#list-hidden-projects, div#list-completed-projects, div#list-contexts, div#projects-empty-nd {clear:right; border: 1px solid #999}
.project-state-group h2 {margin:20px 0px 8px 13px}
.search-result-group h2 {margin:20px 0px 8px 13px }
div.alpha_sort {margin-top:-20px; float:right}
div.menu_sort {margin-top:-20px; float:right}
div.alpha_sort, div.tasks_sort,span.sort_separator {float:left}
.container td {border: none; padding-bottom: 5px}
.container form {border: none}
div.project_description {background: #eee; padding: 5px; margin-top: 0px; margin-left: -5px; margin-right: -5px; color: #666; font-style: italic; font-size: 12px; font-weight: normal}
@ -149,6 +150,8 @@ input#go_to_project, input#context_hide {width: 5%}
#todo-form-new-action .submit_box, #project_form .submit_box, #context_form .submit_box {height: 25px; padding: 5px 0; text-align: center; clear: right}
.edit_todo_form .submit_box {height: 25px; padding: 5px 0; text-align: center; clear: right}
.edit_todo_form input, .edit_todo_form textarea {width:100%}
.edit_todo_form .Date {width:89%}
.edit_todo_form a.date_clear:hover {background: #CCCCCC}
.edit_todo_form .tag_list_label {clear:both}
.edit_todo_form .due_input, .edit_todo_form .show_from_input, .edit_todo_form .project_input, .edit_todo_form .context_input {width:48%}
.edit_todo_form .show_from_input, .edit_todo_form .context_input {float: right}
@ -218,7 +221,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}
.date_clear {float: right}
.defer-container {float:right}
.defer-container a:hover {background-color: inherit}
div.calendar {position: relative}
.calendar, .calendar table {border: 1px solid #556; font-size: 11px; color: #000; cursor: default; background: #eef; z-index: 110; font-family: tahoma,verdana,sans-serif}
.calendar .button {text-align: center; padding: 2px}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

@ -0,0 +1,374 @@
Return-Path: <15555555555/TYPE=PLMN@tmomail.net>
Date: Fri, 6 Jun 2008 21:38:26 -0400
From: 15555555555@tmomail.net
To: gtd@tracks.com
Message-ID: <3645873.13759311212802713215.JavaMail.mms@rlyatl28>
Subject: This is the subject
MIME-Version: 1.0
Content-Type: multipart/related; type="text/html";
boundary="----=_Part_1240237_22156211.1212802713213"
X-UA: treo_600
Importance: Normal
X-Mms-Sender-Visibility: Show
X-MMS-Message-Type: MM4_forward.REQ
X-Priority: 3
X-Proofpoint-Spam-Reason: safe
------=_Part_1240237_22156211.1212802713213
Content-Type: text/html
Content-Transfer-Encoding: quoted-printable
Content-ID: <0000>
Content-Location:mms.smil
Content-Disposition: inline
<html>
<head>
<title>T-Mobile</title>=20
<!--
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
If you can read this text, but much of the message below se=
ems unreadable, you might be using an e-mail program that does not work wit=
h HTML.
=20
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D-->
<style type=3D"text/css">
<!--
=20
.footer {
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
color: #555555;
text-decoration: none;
}
.normal {
font-family: Arial, Helvetica, sans-serif;
font-size: 10px;
color: #555555;
text-decoration: none;
}
=20
-->
</style>
</head>
<body marginwidth=3D"0" marginheight=3D"0" leftmargin=3D"0" topmarg=
in=3D"0" bgcolor=3D"#ffffff">
<table border=3D"0" width=3D"600" cellspacing=3D"0" cellpad=
ding=3D"0">
<tr>
<td width=3D"20" rowspan=3D"8"><img src=3D"=
cid:tmobilespace.gif" width=3D"20" height=3D"20"></td>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:tmobilespace.gif" width=3D"600" height=3D"20"></td>
<td width=3D"20" rowspan=3D"8"><img src=3D"=
cid:tmobilespace.gif" width=3D"20" height=3D"20"></td>
</tr>
<tr>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:dottedline600.gif" width=3D"600"></td>
</tr>
<tr>
<td width=3D"370">
<!-- presentation starts here -->
<table border=3D0><tr><tr><td colspan=3D1=
align=3D"Left">This is the message body</td></tr></tr><TR><TD width=3D350 =
colSpan=3D1><IMG height=3D30 src=3D"cid:tmobilespace.gif" width=3D350></TD=
></TR><TR><TD width=3D350 colSpan=3D4><IMG src=3D"cid:dottedline350.gif" w=
idth=3D350></TD></TR><TR><TR><TD width=3D350 colSpan=3D4><IMG height=3D30 s=
rc=3D"cid:tmobilespace.gif" width=3D350></TD></TR></table> =20
<!-- presentation ends here -->
</td>
<td width=3D"240" bgcolor=3D"#f2f2f2"><BR><=
/td>
</tr>
<tr>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:tmobilelogo.gif" width=3D"600" height=3D"105"></td>
</tr>
</span></td>
</tr>
<tr>
<td width=3D"600" colspan=3D"2"><img src=3D=
"cid:tmobilespace.gif" width=3D"600" height=3D"40"></td>
</tr>
</table>
</body></html>
------=_Part_1240237_22156211.1212802713213
Content-Type: text/plain; charset=utf-8; name=text.txt
Content-Transfer-Encoding: 7bit
Content-ID: <133>
Content-Location: text.txt
Content-Disposition: inline
This is the message body
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=dottedline350.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=dottedline350.gif
Content-ID: <dottedline350.gif>
R0lGODlhXgEBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBm
AABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/
MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNm
ZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/
mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZm
zGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb/
/5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZ
AJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwA
M8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZ
ZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8A
mf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+Z
zP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAABeAQEA
AAg1AFP9+yeQ4MCCCA8qNMgwYcOFDiNCnPiwokSLFC9qzMgRo8eNHzuCHCmyZMiTJFGaTMlSYUAA
Ow==
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=dottedline600.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=dottedline600.gif
Content-ID: <dottedline600.gif>
R0lGODlhWAIBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBm
AABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/
MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNm
ZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/
mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZm
zGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb/
/5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZ
AJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwA
M8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZ
ZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8A
mf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+Z
zP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAABYAgEA
AAhFAFP9+yeQ4MCCCA8qNMgwYcOFDiNCnPiwokSLFC9qzMgRo8eNHzuCHCmyZMiTJFGaTMlypUuV
MFvGfCmzJs2bM3Pa9BgQADs=
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=tmobilelogo.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=tmobilelogo.gif
Content-ID: <tmobilelogo.gif>
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
d3Pf9lg/d3Vf94INCAA7
------=_Part_1240237_22156211.1212802713213
Content-Type: image/gif; name=tmobilespace.gif
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=tmobilespace.gif
Content-ID: <tmobilespace.gif>
R0lGODlhAQABAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwMDcwKbK8AAAMzMAADMAMwAzMxYW
FhwcHCIiIikpKVVVVU1NTUJCQjk5Of98gP9QUNYAk8zs/+/Wxufn1q2pkDP/AGYAAJkAAMwAAAAz
ADMzAGYzAJkzAMwzAP8zAABmADNmAGZmAJlmAMxmAP9mAACZADOZAGaZAJmZAMyZAP+ZAADMADPM
AGbMAJnMAMzMAP/MAGb/AJn/AMz/AAD/MzMA/2YAM5kAM8wAM/8AMwAz/zMzM2YzM5kzM8wzM/8z
MwBmMzNmM2ZmM5lmM8xmM/9mMwCZMzOZM2aZM5mZM8yZM/+ZMwDMMzPMM2bMM5nMM8zMM//MMzP/
M2b/M5n/M8z/M///MwAAZjMAZmYAZpkAZswAZv8AZgAzZjMzZmYzZpkzZswzZv8zZgBmZjNmZmZm
ZplmZsxmZgCZZjOZZmaZZpmZZsyZZv+ZZgDMZjPMZpnMZszMZv/MZgD/ZjP/Zpn/Zsz/Zv8AzMwA
/wCZmZkzmZkAmcwAmQAAmTMzmWYAmcwzmf8AmQBmmTNmmWYzmZlmmcxmmf8zmTOZmWaZmZmZmcyZ
mf+ZmQDMmTPMmWbMZpnMmczMmf/MmQD/mTP/mWbMmZn/mcz/mf//mQAAzDMAmWYAzJkAzMwAzAAz
mTMzzGYzzJkzzMwzzP8zzABmzDNmzGZmmZlmzMxmzP9mmQCZzDOZzGaZzJmZzMyZzP+ZzADMzDPM
zGbMzJnMzMzMzP/MzAD/zDP/zGb/mZn/zMz/zP//zDMAzGYA/5kA/wAzzDMz/2Yz/5kz/8wz//8z
/wBm/zNm/2ZmzJlm/8xm//9mzACZ/zOZ/2aZ/5mZ/8yZ//+Z/wDM/zPM/2bM/5nM/8zM///M/zP/
/2b/zJn//8z///9mZmb/Zv//ZmZm//9m/2b//6UAIV9fX3d3d4aGhpaWlsvLy7KystfX193d3ePj
4+rq6vHx8fj4+P/78KCgpICAgP8AAAD/AP//AAAA//8A/wD//////ywAAAAAAQABAAAIBAD/BQQA
Ow==
------=_Part_1240237_22156211.1212802713213--

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

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

View file

@ -95,7 +95,7 @@ end
completed_at: ~
user_id: 1
7:
book:
id: 7
context_id: 6
project_id: 3
@ -230,3 +230,17 @@ end
completed_at: ~
show_from: <%= next_week %>
user_id: 2
18:
id: 18
user_id: 1
context_id: 1
project_id: 2
description: Call Bill Gates every day
notes: ~
state: active
created_at: <%= last_week %>
due: <%= last_week %>
completed_at: ~
show_from: ~
recurring_todo_id: 1

View file

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

View file

@ -25,7 +25,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
assert_equal 1, assigns['deferred'].size
t = p.not_done_todos[0]
t.show_from = 1.days.from_now.utc.to_date
t.show_from = 1.days.from_now.utc
t.save!
get :show, :id => p.to_param
@ -218,6 +218,15 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
get :index, { :format => "txt", :token => users(:admin_user).token }
assert_response :ok
end
def test_actionize_sorts_active_projects_by_number_of_tasks
login_as :admin_user
u = users(:admin_user)
post :actionize, :state => "active", :format => 'js'
assert_equal 1, projects(:gardenclean).position
assert_equal 2, projects(:moremoney).position
assert_equal 3, projects(:timemachine).position
end
def test_alphabetize_sorts_active_projects_alphabetically
login_as :admin_user

View file

@ -17,6 +17,167 @@ class RecurringTodosControllerTest < ActionController::TestCase
login_as(:admin_user)
xhr :post, :destroy, :id => 1, :_source_view => 'todo'
assert_rjs :page, "recurring_todo_1", :remove
begin
rc = RecurringTodo.find(1)
rescue
rc = nil
end
assert_nil rc
end
def test_new_recurring_todo
login_as(:admin_user)
orig_rt_count = RecurringTodo.count
orig_todo_count = Todo.count
put :create,
"context_name"=>"library",
"project_name"=>"Build a working time machine",
"recurring_todo" =>
{
"daily_every_x_days"=>"1",
"daily_selector"=>"daily_every_x_day",
"description"=>"new recurring pattern",
"end_date" => "31/08/2010",
"ends_on" => "ends_on_end_date",
"monthly_day_of_week" => "1",
"monthly_every_x_day" => "18",
"monthly_every_x_month2" => "1",
"monthly_every_x_month" => "1",
"monthly_every_xth_day"=>"1",
"monthly_selector"=>"monthly_every_x_day",
"notes"=>"with some notes",
"number_of_occurences" => "",
"recurring_period"=>"yearly",
"recurring_show_days_before"=>"10",
"recurring_target"=>"due_date",
"start_from"=>"18/08/2008",
"weekly_every_x_week"=>"1",
"weekly_return_monday"=>"m",
"yearly_day_of_week"=>"1",
"yearly_every_x_day"=>"8",
"yearly_every_xth_day"=>"1",
"yearly_month_of_year2"=>"8",
"yearly_month_of_year"=>"6",
"yearly_selector"=>"yearly_every_x_day"
},
"tag_list"=>"one, two, three, four"
# check new recurring todo added
assert_equal orig_rt_count+1, RecurringTodo.count
# check new todo added
assert_equal orig_todo_count+1, Todo.count
end
def test_recurring_todo_toggle_check
# the test fixtures did add recurring_todos but not the corresponding todos,
# so we check complete and uncheck to force creation of a todo from the
# pattern
login_as(:admin_user)
# mark as complete
xhr :post, :toggle_check, :id=>1, :_source_view=>""
recurring_todo_1 = RecurringTodo.find(1)
assert recurring_todo_1.completed?
# remove remaining todo
todo = Todo.find_by_recurring_todo_id(1)
todo.recurring_todo_id = 2
todo.save
todo_count = Todo.count
# mark as active
xhr :post, :toggle_check, :id=>1, :_source_view=>""
recurring_todo_1.reload
assert recurring_todo_1.active?
# by making active, a new todo should be created from the pattern
assert_equal todo_count+1, Todo.count
# find the new todo and check its description
new_todo = Todo.find_by_recurring_todo_id 1
assert_equal "Call Bill Gates every day", new_todo.description
end
def test_creating_recurring_todo_with_show_from_in_past
login_as(:admin_user)
@yearly = RecurringTodo.find(5) # yearly on june 8th
# change due date in four days from now and show from 10 days before, i.e. 6
# days ago
target_date = Time.now.utc + 4.days
@yearly.every_other1 = target_date.day
@yearly.every_other2 = target_date.month
@yearly.show_from_delta = 10
assert @yearly.save
# toggle twice to force generation of new todo
xhr :post, :toggle_check, :id=>5, :_source_view=>""
xhr :post, :toggle_check, :id=>5, :_source_view=>""
new_todo = Todo.find_by_recurring_todo_id 5
# due date should be the target_date
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
end
def test_last_sunday_of_march
# this test is a duplicate of the unit test. Only this test covers the
# codepath in the controllers
login_as(:admin_user)
orig_rt_count = RecurringTodo.count
orig_todo_count = Todo.count
put :create,
"context_name"=>"library",
"project_name"=>"Build a working time machine",
"recurring_todo" =>
{
"daily_every_x_days"=>"1",
"daily_selector"=>"daily_every_x_day",
"description"=>"new recurring pattern",
"end_date" => "",
"ends_on" => "no_end_date",
"monthly_day_of_week" => "1",
"monthly_every_x_day" => "22",
"monthly_every_x_month2" => "1",
"monthly_every_x_month" => "1",
"monthly_every_xth_day"=>"1",
"monthly_selector"=>"monthly_every_x_day",
"notes"=>"with some notes",
"number_of_occurences" => "",
"recurring_period"=>"yearly",
"recurring_show_days_before"=>"0",
"recurring_target"=>"due_date",
"start_from"=>"",
"weekly_every_x_week"=>"1",
"weekly_return_monday"=>"w",
"yearly_day_of_week"=>"0",
"yearly_every_x_day"=>"22",
"yearly_every_xth_day"=>"5",
"yearly_month_of_year2"=>"3",
"yearly_month_of_year"=>"10",
"yearly_selector"=>"yearly_every_xth_day"
},
"tag_list"=>"one, two, three, four"
# check new recurring todo added
assert_equal orig_rt_count+1, RecurringTodo.count
# check new todo added
assert_equal orig_todo_count+1, Todo.count
# find the newly created todo
new_todo = Todo.find_by_description("new recurring pattern")
assert !new_todo.nil?
# the date should be 29 march 2009
assert_equal Time.zone.local(2009,3,29), new_todo.due
end
end

View file

@ -56,7 +56,7 @@ class StatsControllerTest < Test::Unit::TestCase
assert_equal 3, assigns['projects'].count
assert_equal 3, assigns['projects'].count(:conditions => "state = 'active'")
assert_equal 10, assigns['contexts'].count
assert_equal 15, assigns['actions'].count
assert_equal 16, assigns['actions'].count
assert_equal 4, assigns['tags'].count
assert_equal 2, assigns['unique_tags'].size
assert_equal 2.week.ago.utc.beginning_of_day, assigns['first_action'].created_at

View file

@ -5,7 +5,7 @@ require 'todos_controller'
class TodosController; def rescue_action(e) raise e end; end
class TodosControllerTest < Test::Rails::TestCase
fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings
fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos
def setup
@controller = TodosController.new
@ -70,7 +70,7 @@ class TodosControllerTest < Test::Rails::TestCase
login_as(:admin_user)
xhr :post, :destroy, :id => 1, :_source_view => 'todo'
assert_rjs :page, "todo_1", :remove
#assert_rjs :replace_html, "badge-count", '9'
# #assert_rjs :replace_html, "badge-count", '9'
end
def test_create_todo
@ -90,11 +90,11 @@ class TodosControllerTest < Test::Rails::TestCase
def test_fail_to_create_todo_via_xml
login_as(:admin_user)
#try to create with no context, which is not valid
# #try to create with no context, which is not valid
put :create, :format => "xml", "request" => { "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" }
assert_response 422
assert_xml_select "errors" do
assert_xml_select "error", "Context can't be blank"
assert_xml_select "error", "Context can't be blank"
end
end
@ -124,9 +124,9 @@ class TodosControllerTest < Test::Rails::TestCase
def test_update_todo_to_deferred_is_reflected_in_badge_count
login_as(:admin_user)
get :index
assert_equal 10, assigns['count']
assert_equal 11, assigns['count']
xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Make more money than Billy Gates", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006", "show_from"=>"30/11/2030"}, "tag_list"=>"foo bar"
assert_equal 9, assigns['down_count']
assert_equal 10, assigns['down_count']
end
def test_update_todo
@ -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
@ -180,7 +180,7 @@ class TodosControllerTest < Test::Rails::TestCase
login_as(:admin_user)
get :index, { :format => "rss" }
assert_equal 'application/rss+xml', @response.content_type
#puts @response.body
# puts @response.body
assert_xml_select 'rss[version="2.0"]' do
assert_select 'channel' do
@ -188,12 +188,12 @@ class TodosControllerTest < Test::Rails::TestCase
assert_select '>description', "Actions for #{users(:admin_user).display_name}"
assert_select 'language', 'en-us'
assert_select 'ttl', '40'
assert_select 'item', 10 do
assert_select 'item', 11 do
assert_select 'title', /.+/
assert_select 'description', /.*/
assert_select 'link', %r{http://test.host/contexts/.+}
assert_select 'guid', %r{http://test.host/todos/.+}
assert_select 'pubDate', projects(:timemachine).updated_at.to_s(:rfc822)
assert_select 'pubDate', todos(:book).updated_at.to_s(:rfc822)
end
end
end
@ -237,15 +237,15 @@ class TodosControllerTest < Test::Rails::TestCase
login_as :admin_user
get :index, { :format => "atom" }
assert_equal 'application/atom+xml', @response.content_type
#puts @response.body
# #puts @response.body
assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do
assert_xml_select '>title', 'Tracks Actions'
assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}"
assert_xml_select 'entry', 10 do
assert_xml_select 'entry', 11 do
assert_xml_select 'title', /.+/
assert_xml_select 'content[type="html"]', /.*/
assert_xml_select 'published', /(#{Regexp.escape(projects(:timemachine).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/
assert_xml_select 'published', /(#{Regexp.escape(todos(:book).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/
end
end
end
@ -273,7 +273,7 @@ class TodosControllerTest < Test::Rails::TestCase
get :index, { :format => "txt" }
assert_equal 'text/plain', @response.content_type
assert !(/&nbsp;/.match(@response.body))
#puts @response.body
# #puts @response.body
end
def test_text_feed_not_accessible_to_anonymous_user_without_token
@ -299,7 +299,7 @@ class TodosControllerTest < Test::Rails::TestCase
get :index, { :format => "ics" }
assert_equal 'text/calendar', @response.content_type
assert !(/&nbsp;/.match(@response.body))
#puts @response.body
# #puts @response.body
end
def test_mobile_index_uses_text_html_content_type
@ -311,16 +311,16 @@ class TodosControllerTest < Test::Rails::TestCase
def test_mobile_index_assigns_down_count
login_as(:admin_user)
get :index, { :format => "m" }
assert_equal 10, assigns['down_count']
assert_equal 11, assigns['down_count']
end
def test_mobile_create_action_creates_a_new_todo
login_as(:admin_user)
post :create, {"format"=>"m", "todo"=>{"context_id"=>"2",
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
"project_id"=>"1",
"notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}}
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
"project_id"=>"1",
"notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}}
t = Todo.find_by_description("test_mobile_create_action")
assert_not_nil t
assert_equal 2, t.context_id
@ -328,26 +328,26 @@ 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
login_as(:admin_user)
post :create, {"format"=>"m", "todo"=>{"context_id"=>"2",
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
"project_id"=>"1",
"notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}}
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
"project_id"=>"1",
"notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}}
assert_redirected_to '/m'
end
def test_mobile_create_action_renders_new_template_when_save_fails
login_as(:admin_user)
post :create, {"format"=>"m", "todo"=>{"context_id"=>"2",
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
"project_id"=>"1",
"notes"=>"test notes", "state"=>"0"}, "tag_list"=>"test, test2"}
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
"project_id"=>"1",
"notes"=>"test notes", "state"=>"0"}, "tag_list"=>"test, test2"}
assert_template 'todos/new'
end
@ -357,4 +357,132 @@ class TodosControllerTest < Test::Rails::TestCase
assert_equal '"{\\"Build a working time machine\\": \\"lab\\"}"', assigns(:default_project_context_name_map)
end
def test_toggle_check_on_recurring_todo
login_as(:admin_user)
# link todo_1 and recurring_todo_1
recurring_todo_1 = RecurringTodo.find(1)
todo_1 = Todo.find_by_recurring_todo_id(1)
# mark todo_1 as complete by toggle_check
xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo'
todo_1.reload
assert todo_1.completed?
# check that there is only one active todo belonging to recurring_todo
count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'})
assert_equal 1, count
# check there is a new todo linked to the recurring pattern
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
# check that the new todo is not the same as todo_1
assert_not_equal todo_1.id, next_todo.id
# change recurrence pattern to monthly and set show_from 2 days before 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 = 'monthly'
recurring_todo_1.recurrence_selector = 0
recurring_todo_1.every_other1 = 1
recurring_todo_1.every_other2 = 2
recurring_todo_1.every_other3 = 5
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 that there are three todos belonging to recurring_todo: two
# completed and one deferred
count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id})
assert_equal 3, count
# 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 !next_todo.nil?
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
def test_toggle_check_on_rec_todo_show_from_today
login_as(:admin_user)
# link todo_1 and recurring_todo_1
recurring_todo_1 = RecurringTodo.find(1)
todo_1 = Todo.find_by_recurring_todo_id(1)
today = Time.now.utc.at_midnight
# change recurrence pattern to monthly and set show_from to today
recurring_todo_1.target = 'show_from_date'
recurring_todo_1.recurring_period = 'monthly'
recurring_todo_1.recurrence_selector = 0
recurring_todo_1.every_other1 = today.day
recurring_todo_1.every_other2 = 1
recurring_todo_1.save
# mark todo_1 as complete by toggle_check, this gets rid of todo_1 that was
# not correctly created from the adjusted recurring pattern we defined
# above.
xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo'
todo_1.reload
assert todo_1.completed?
# locate the new todo. This todo is created from the adjusted recurring
# pattern defined in this test
new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'})
assert !new_todo.nil?
# mark new_todo as complete by toggle_check
xhr :post, :toggle_check, :id => new_todo.id, :_source_view => 'todo'
new_todo.reload
assert todo_1.completed?
# locate the new todo in tickler
new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'})
assert !new_todo.nil?
assert_equal "Call Bill Gates every day", new_todo.description
# check that the new todo is not the same as todo_1
assert_not_equal todo_1.id, new_todo.id
# check that the new_todo is in the tickler to show next month
assert !new_todo.show_from.nil?
assert_equal Time.utc(today.year, today.month+1, today.day), new_todo.show_from
end
def test_check_for_next_todo
login_as :admin_user
recurring_todo_1 = RecurringTodo.find(5)
@todo = Todo.find_by_recurring_todo_id(1)
assert @todo.from_recurring_todo?
# rewire @todo to yearly recurring todo
@todo.recurring_todo_id = 5
# make todo due tomorrow and change recurring date also to tomorrow
@todo.due = Time.zone.now + 1.day
@todo.save
recurring_todo_1.every_other1 = @todo.due.day
recurring_todo_1.every_other2 = @todo.due.month
recurring_todo_1.save
# mark todo complete
xhr :post, :toggle_check, :id => @todo.id, :_source_view => 'todo'
@todo.reload
assert @todo.completed?
# check that there is no active todo
next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'})
assert next_todo.nil?
# check for new deferred todo
next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'})
assert !next_todo.nil?
# check that the due date of the new todo is later than tomorrow
assert next_todo.due > @todo.due
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,7 +53,7 @@ class ContextTest < Test::Rails::TestCase
end
def test_delete_context_deletes_todos_within_it
assert_equal 6, @agenda.todos.count
assert_equal 7, @agenda.todos.count
agenda_todo_ids = @agenda.todos.collect{|t| t.id }
@agenda.destroy
agenda_todo_ids.each do |todo_id|
@ -62,11 +62,11 @@ class ContextTest < Test::Rails::TestCase
end
def test_not_done_todos
assert_equal 5, @agenda.not_done_todos.size
assert_equal 6, @agenda.not_done_todos.size
t = @agenda.not_done_todos[0]
t.complete!
t.save!
assert_equal 4, Context.find(@agenda.id).not_done_todos.size
assert_equal 5, Context.find(@agenda.id).not_done_todos.size
end
def test_done_todos

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more