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