Removed superfluous 'tracks' directory at the root of the repository.

Testing commits to github.
This commit is contained in:
bsag 2008-05-20 21:28:26 +01:00
parent 6a42901514
commit 4cbf5a34d3
2269 changed files with 0 additions and 0 deletions

18
app/apis/todo_api.rb Normal file
View file

@ -0,0 +1,18 @@
class TodoApi < ActionWebService::API::Base
api_method :new_todo,
:expects => [{:username => :string}, {:token => :string}, {:context_id => :int}, {:description => :string}, {:notes => :string}],
:returns => [:int]
api_method :new_rich_todo,
:expects => [{:username => :string}, {:token => :string}, {:default_context_id => :int}, {:description => :string}, {:notes => :string}],
:returns => [:int]
api_method :list_contexts,
:expects => [{:username => :string}, {:token => :string}],
:returns => [[Context]]
api_method :list_projects,
:expects => [{:username => :string}, {:token => :string}],
:returns => [[Project]]
end

View file

@ -0,0 +1,216 @@
# The filters added to this controller will be run for all controllers in the application.
# Likewise will all the methods added be available for all controllers.
require_dependency "login_system"
require_dependency "source_view"
require "redcloth"
require 'date'
require 'time'
Tag # We need this in development mode, or you get 'method missing' errors
class ApplicationController < ActionController::Base
protect_from_forgery :secret => SALT
helper :application
include LoginSystem
helper_method :current_user, :prefs
layout proc{ |controller| controller.mobile? ? "mobile" : "standard" }
before_filter :set_session_expiration
prepend_before_filter :login_required
prepend_before_filter :enable_mobile_content_negotiation
after_filter :restore_content_type_for_mobile
after_filter :set_charset
include ActionView::Helpers::TextHelper
include ActionView::Helpers::SanitizeHelper
helper_method :format_date, :markdown
# By default, sets the charset to UTF-8 if it isn't already set
def set_charset
headers["Content-Type"] ||= "text/html; charset=UTF-8"
end
def set_session_expiration
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
unless session == nil
return if @controller_name == 'feed' or session['noexpiry'] == "on"
# If the method is called by the feed controller (which we don't have under session control)
# or if we checked the box to keep logged in on login
# don't set the session expiry time.
if session
# Get expiry time (allow ten seconds window for the case where we have none)
expiry_time = session['expiry_time'] || Time.now + 10
if expiry_time < Time.now
# Too late, matey... bang goes your session!
reset_session
else
# Okay, you get another hour
session['expiry_time'] = Time.now + (60*60)
end
end
end
end
def render_failure message, status = 404
render :text => message, :status => status
end
# def rescue_action(exception)
# log_error(exception) if logger
# respond_to do |format|
# format.html do
# notify :warning, "An error occurred on the server."
# render :action => "index"
# end
# format.js { render :action => 'error' }
# format.xml { render :text => 'An error occurred on the server.' + $! }
# end
# end
# Returns a count of next actions in the given context or project
# The result is count and a string descriptor, correctly pluralised if there are no
# actions or multiple actions
#
def count_undone_todos_phrase(todos_parent, string="actions")
count = count_undone_todos(todos_parent)
if count == 1
word = string.singularize
else
word = string.pluralize
end
return count.to_s + "&nbsp;" + word
end
def count_undone_todos(todos_parent)
if todos_parent.nil?
count = 0
elsif (todos_parent.is_a?(Project) && todos_parent.hidden?)
count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]"
else
count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]"
end
count || 0
end
# Convert a date object to the format specified in the user's preferences
# in config/settings.yml
#
def format_date(date)
if date
date_format = prefs.date_format
formatted_date = date.strftime("#{date_format}")
else
formatted_date = ''
end
formatted_date
end
# Uses RedCloth to transform text using either Textile or Markdown
# Need to require redcloth above
# RedCloth 3.0 or greater is needed to use Markdown, otherwise it only handles Textile
#
def markdown(text)
RedCloth.new(text).to_html
end
def build_default_project_context_name_map(projects)
Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
end
# Here's the concept behind this "mobile content negotiation" hack:
# In addition to the main, AJAXy Web UI, Tracks has a lightweight
# low-feature 'mobile' version designed to be suitablef or use
# from a phone or PDA. It makes some sense that tne pages of that
# mobile version are simply alternate representations of the same
# Todo resources. The implementation goal was to treat mobile
# as another format and be able to use respond_to to render both
# versions. Unfortunately, I ran into a lot of trouble simply
# registering a new mime type 'text/html' with format :m because
# :html already is linked to that mime type and the new
# registration was forcing all html requests to be rendered in
# the mobile view. The before_filter and after_filter hackery
# below accomplishs that implementation goal by using a 'fake'
# mime type during the processing and then setting it to
# 'text/html' in an 'after_filter' -LKM 2007-04-01
def mobile?
return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
end
def enable_mobile_content_negotiation
if mobile?
request.accepts.unshift(Mime::Type::lookup(MOBILE_CONTENT_TYPE))
end
end
def restore_content_type_for_mobile
if mobile?
response.content_type = 'text/html'
end
end
protected
def admin_login_required
unless User.find_by_id_and_is_admin(session['user_id'], true)
render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401
return false
end
end
def redirect_back_or_home
respond_to do |format|
format.html { redirect_back_or_default home_url }
format.m { redirect_back_or_default mobile_url }
end
end
def boolean_param(param_name)
return false if param_name.blank?
s = params[param_name]
return false if s.blank? || s == false || s =~ /^false$/i
return true if s == true || s =~ /^true$/i
raise ArgumentError.new("invalid value for Boolean: \"#{s}\"")
end
private
def parse_date_per_user_prefs( s )
prefs.parse_date(s)
end
def init_data_for_sidebar
@projects = @projects || current_user.projects.find(:all, :include => [:default_context ])
@contexts = @contexts || current_user.contexts
init_not_done_counts
if prefs.show_hidden_projects_in_sidebar
init_project_hidden_todo_counts(['project'])
end
end
def init_not_done_counts(parents = ['project','context'])
parents.each do |parent|
eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || Todo.count(:conditions => ['user_id = ? and state = ?', current_user.id, 'active'], :group => :#{parent}_id)")
end
end
def init_project_hidden_todo_counts(parents = ['project','context'])
parents.each do |parent|
eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || Todo.count(:conditions => ['user_id = ? and (state = ? or state = ?)', current_user.id, 'project_hidden', 'active'], :group => :#{parent}_id)")
end
end
# Set the contents of the flash message from a controller
# Usage: notify :warning, "This is the message"
# Sets the flash of type 'warning' to "This is the message"
def notify(type, message)
flash[type] = message
logger.error("ERROR: #{message}") if type == :error
end
end

View file

@ -0,0 +1,108 @@
class BackendController < ApplicationController
wsdl_service_name 'Backend'
web_service_api TodoApi
web_service_scaffold :invoke
skip_before_filter :login_required
def new_todo(username, token, context_id, description, notes)
check_token(username, token)
check_context_belongs_to_user(context_id)
item = create_todo(description, context_id, nil, notes)
item.id
end
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
end
def list_contexts(username, token)
check_token(username, token)
@user.contexts
end
def list_projects(username, token)
check_token(username, token)
@user.projects
end
private
# Check whether the token in the URL matches the token in the User's table
def check_token(username, token)
@user = User.find_by_login( username )
unless (token == @user.token)
raise(InvalidToken, "Sorry, you don't have permission to perform this action.")
end
end
def check_context_belongs_to_user(context_id)
unless @user.contexts.exists? context_id
raise(CannotAccessContext, "Cannot access a context that does not belong to this user.")
end
end
def create_todo(description, context_id, project_id = nil, notes="")
item = @user.todos.build
item.description = description
item.notes = notes
item.context_id = context_id
item.project_id = project_id unless project_id.nil?
item.save
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

@ -0,0 +1,204 @@
class ContextsController < ApplicationController
helper :todos
before_filter :init, :except => [:index, :create, :destroy, :order]
before_filter :init_todos, :only => :show
before_filter :set_context_from_params, :only => [:update, :destroy]
skip_before_filter :login_required, :only => [:index]
prepend_before_filter :login_or_feed_token_required, :only => [:index]
session :off, :only => :index, :if => Proc.new { |req| ['rss','atom','txt'].include?(req.parameters[:format]) }
def index
@contexts = current_user.contexts(true) #true is passed here to force an immediate load so that size and empty? checks later don't result in separate SQL queries
init_not_done_counts(['context'])
respond_to do |format|
format.html &render_contexts_html
format.m &render_contexts_mobile
format.xml { render :xml => @contexts.to_xml( :except => :user_id ) }
format.rss &render_contexts_rss_feed
format.atom &render_contexts_atom_feed
format.text { render :action => 'index', :layout => false, :content_type => Mime::TEXT }
end
end
def show
if (@context.nil?)
respond_to do |format|
format.html { render :text => 'Context not found', :status => 404 }
format.xml { render :xml => '<error>Context not found</error>', :status => 404 }
end
else
@page_title = "TRACKS::Context: #{@context.name}"
respond_to do |format|
format.html
format.m &render_context_mobile
format.xml { render :xml => @context.to_xml( :except => :user_id ) }
end
end
end
# Example XML usage: curl -H 'Accept: application/xml' -H 'Content-Type:
# application/xml'
# -u username:password
# -d '<request><context><name>new context_name</name></context></request>'
# http://our.tracks.host/contexts
#
def create
if params[:format] == 'application/xml' && params['exception']
render_failure "Expected post format is valid xml like so: <request><context><name>context name</name></context></request>.", 400
return
end
@context = current_user.contexts.build
params_are_invalid = true
if (params['context'] || (params['request'] && params['request']['context']))
@context.attributes = params['context'] || params['request']['context']
params_are_invalid = false
end
@saved = @context.save
@context_not_done_counts = { @context.id => 0 }
respond_to do |format|
format.js do
@down_count = current_user.contexts.size
end
format.xml do
if @context.new_record? && params_are_invalid
render_failure "Expected post format is valid xml like so: <request><context><name>context name</name></context></request>.", 400
elsif @context.new_record?
render_failure @context.errors.to_xml, 409
else
head :created, :location => context_url(@context)
end
end
end
end
# Edit the details of the context
#
def update
params['context'] ||= {}
success_text = if params['field'] == 'name' && params['value']
params['context']['id'] = params['id']
params['context']['name'] = params['value']
end
@context.attributes = params["context"]
if @context.save
if params['wants_render']
respond_to do |format|
format.js
end
else
render :text => success_text || 'Success'
end
else
notify :warning, "Couldn't update new context"
render :text => ""
end
end
# Fairly self-explanatory; deletes the context If the context contains
# actions, you'll get a warning dialogue. If you choose to go ahead, any
# actions in the context will also be deleted.
def destroy
@context.destroy
respond_to do |format|
format.js { @down_count = current_user.contexts.size }
format.xml { render :text => "Deleted context #{@context.name}" }
end
end
# Methods for changing the sort order of the contexts in the list
#
def order
params["list-contexts"].each_with_index do |id, position|
current_user.contexts.update(id, :position => position + 1)
end
render :nothing => true
end
protected
def render_contexts_html
lambda do
@page_title = "TRACKS::List Contexts"
@no_contexts = @contexts.empty?
@count = @contexts.size
render
end
end
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]})
@down_count = @active_contexts.size + @hidden_contexts.size
cookies[:mobile_url]=request.request_uri
render :action => 'index_mobile'
end
end
def render_context_mobile
lambda do
@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
render :action => 'mobile_show_context'
end
end
def render_contexts_rss_feed
lambda do
render_rss_feed_for @contexts, :feed => feed_options,
:item => { :description => lambda { |c| c.summary(count_undone_todos_phrase(c)) } }
end
end
def render_contexts_atom_feed
lambda do
render_atom_feed_for @contexts, :feed => feed_options,
:item => { :description => lambda { |c| c.summary(count_undone_todos_phrase(c)) },
:author => lambda { |c| nil } }
end
end
def feed_options
Context.feed_options(current_user)
end
def set_context_from_params
@context = current_user.contexts.find_by_params(params)
rescue
@context = nil
end
def init
@source_view = params['_source_view'] || 'context'
init_data_for_sidebar
end
def init_todos
set_context_from_params
unless @context.nil?
@context.todos.send :with_scope, :find => { :include => [:project, :tags] } do
@done = @context.done_todos
end
@max_completed = current_user.prefs.show_number_completed
# @not_done_todos = @context.not_done_todos TODO: Temporarily doing this
# search manually until I can work out a way to do the same thing using
# not_done_todos acts_as_todo_container method Hides actions in hidden
# projects from context.
@not_done_todos = @context.todos.find(
:all,
:conditions => ['todos.state = ? AND (todos.project_id IS ? OR projects.state = ?)', 'active', nil, 'active'],
:order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC",
:include => [:project, :tags])
@count = @not_done_todos.size
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end
end
end

View file

@ -0,0 +1,95 @@
class DataController < ApplicationController
require 'csv'
def index
@page_title = "TRACKS::Export"
end
def import
end
def export
# Show list of formats for export
end
# Thanks to a tip by Gleb Arshinov
# <http://lists.rubyonrails.org/pipermail/rails/2004-November/000199.html>
def yaml_export
all_tables = {}
all_tables['todos'] = current_user.todos.find(:all)
all_tables['contexts'] = current_user.contexts.find(:all)
all_tables['projects'] = current_user.projects.find(:all)
all_tables['tags'] = current_user.tags.find(:all)
all_tables['taggings'] = current_user.taggings.find(:all)
all_tables['notes'] = current_user.notes.find(:all)
result = all_tables.to_yaml
result.gsub!(/\n/, "\r\n") # TODO: general functionality for line endings
send_data(result, :filename => "tracks_backup.yml", :type => 'text/plain')
end
def csv_actions
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"]
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
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]
end
end
send_data(result, :filename => "todos.csv", :type => content_type)
end
def csv_notes
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
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
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)]
end
end
send_data(result, :filename => "notes.csv", :type => content_type)
end
def xml_export
result = ""
result << current_user.todos.find(:all).to_xml
result << current_user.contexts.find(:all).to_xml(:skip_instruct => true)
result << current_user.projects.find(:all).to_xml(:skip_instruct => true)
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)
send_data(result, :filename => "tracks_backup.xml", :type => 'text/xml')
end
def yaml_form
# Draw the form to input the YAML text data
end
def yaml_import
# Logic to load the YAML text file and create new records from data
end
end

View file

@ -0,0 +1,18 @@
class FeedlistController < ApplicationController
helper :feedlist
def index
@page_title = 'TRACKS::Feeds'
init_data_for_sidebar unless mobile?
respond_to do |format|
format.html { render :layout => 'standard' }
format.m {
# @projects = @projects || current_user.projects.find(:all, :include => [:default_context ])
# @contexts = @contexts || current_user.contexts
render :action => 'mobile_index'
}
end
end
end

View file

@ -0,0 +1,25 @@
class IntegrationsController < ApplicationController
def index
@page_title = 'TRACKS::Integrations'
end
def rest_api
@page_title = 'TRACKS::REST API Documentation'
end
def get_quicksilver_applescript
context = current_user.contexts.find params[:context_id]
render :partial => 'quicksilver_applescript', :locals => { :context => context }
end
def get_applescript1
context = current_user.contexts.find params[:context_id]
render :partial => 'applescript1', :locals => { :context => context }
end
def get_applescript2
context = current_user.contexts.find params[:context_id]
render :partial => 'applescript2', :locals => { :context => context }
end
end

View file

@ -0,0 +1,160 @@
class LoginController < ApplicationController
layout 'login'
filter_parameter_logging :user_password
skip_before_filter :set_session_expiration
skip_before_filter :login_required
before_filter :login_optional
before_filter :get_current_user
open_id_consumer if Tracks::Config.openid_enabled?
def login
@page_title = "TRACKS::Login"
@openid_url = cookies[:openid_url] if Tracks::Config.openid_enabled?
case request.method
when :post
if @user = User.authenticate(params['user_login'], params['user_password'])
session['user_id'] = @user.id
# If checkbox on login page checked, we don't expire the session after 1 hour
# of inactivity and we remember this user for future browser sessions
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 }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at }
end
redirect_back_or_home
return
else
@login = params['user_login']
notify :warning, "Login unsuccessful"
end
when :get
if User.no_users_yet?
redirect_to :controller => 'users', :action => 'new'
return
end
end
respond_to do |format|
format.html
format.m { render :action => 'login_mobile.html.erb', :layout => 'mobile' }
end
end
def begin
# If the URL was unusable (either because of network conditions,
# a server error, or that the response returned was not an OpenID
# identity page), the library will return HTTP_FAILURE or PARSE_ERROR.
# Let the user know that the URL is unusable.
case open_id_response.status
when OpenID::SUCCESS
session['openid_url'] = params[:openid_url]
session['user_noexpiry'] = params[:user_noexpiry]
# The URL was a valid identity URL. Now we just need to send a redirect
# to the server using the redirect_url the library created for us.
# redirect to the server
respond_to do |format|
format.html { redirect_to open_id_response.redirect_url((request.protocol + request.host_with_port + "/"), open_id_complete_url) }
format.m { redirect_to open_id_response.redirect_url((request.protocol + request.host_with_port + "/"), formatted_open_id_complete_url(:format => 'm')) }
end
else
notify :warning, "Unable to find openid server for <q>#{openid_url}</q>"
redirect_to_login
end
end
def complete
openid_url = session['openid_url']
if openid_url.blank?
notify :error, "expected an openid_url"
end
case open_id_response.status
when OpenID::FAILURE
# In the case of failure, if info is non-nil, it is the
# URL that we were verifying. We include it in the error
# message to help the user figure out what happened.
if open_id_response.identity_url
msg = "Verification of #{openid_url}(#{open_id_response.identity_url}) failed. "
else
msg = "Verification failed. "
end
notify :error, open_id_response.msg.to_s + msg
when OpenID::SUCCESS
# Success means that the transaction completed without
# error. If info is nil, it means that the user cancelled
# the verification.
@user = User.find_by_open_id_url(openid_url)
unless (@user.nil?)
session['user_id'] = @user.id
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 }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at }
end
cookies[:openid_url] = { :value => openid_url, :expires => Time.now + 1.year }
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."
end
when OpenID::CANCEL
notify :warning, "Verification cancelled."
else
notify :warning, "Unknown response status: #{open_id_response.status}"
end
redirect_to_login unless performed?
end
def logout
@user.forget_me if logged_in?
cookies.delete :auth_token
session['user_id'] = nil
reset_session
notify :notice, "You have been logged out of Tracks."
redirect_to_login
end
def check_expiry
# Gets called by periodically_call_remote to check whether
# the session has timed out yet
unless session == nil
if session
return unless should_expire_sessions?
# Get expiry time (allow ten seconds window for the case where we have none)
expiry_time = session['expiry_time'] || Time.now + 10
@time_left = expiry_time - Time.now
if @time_left < (10*60) # Session will time out before the next check
@msg = "Session has timed out. Please "
else
@msg = ""
end
end
end
respond_to do |format|
format.js
end
end
private
def redirect_to_login
respond_to do |format|
format.html { redirect_to login_path }
format.m { redirect_to formatted_login_path(:format => 'm') }
end
end
def should_expire_sessions?
session['noexpiry'] != "on"
end
end

View file

@ -0,0 +1,71 @@
class NotesController < ApplicationController
def index
@all_notes = current_user.notes
@count = @all_notes.size
@page_title = "TRACKS::All notes"
respond_to do |format|
format.html
format.xml { render :xml => @all_notes.to_xml( :except => :user_id ) }
end
end
def show
@note = current_user.notes.find(params['id'])
@page_title = "TRACKS::Note " + @note.id.to_s
respond_to do |format|
format.html
format.m &render_note_mobile
end
end
def render_note_mobile
lambda do
render :action => 'note_mobile'
end
end
def create
note = current_user.notes.build
note.attributes = params["new_note"]
if note.save
render :partial => 'notes_summary', :object => note
else
render :text => ''
end
end
def destroy
@note = current_user.notes.find(params['id'])
@note.destroy
respond_to do |format|
format.html
format.js do
@count = current_user.notes.size
render
end
end
# if note.destroy
# render :text => ''
# else
# notify :warning, "Couldn't delete note \"#{note.id}\""
# render :text => ''
# end
end
def update
note = current_user.notes.find(params['id'])
note.attributes = params["note"]
if note.save
render :partial => 'notes', :object => note
else
notify :warning, "Couldn't update note \"#{note.id}\""
render :text => ''
end
end
end

View file

@ -0,0 +1,23 @@
class PreferencesController < ApplicationController
def index
@page_title = "TRACKS::Preferences"
@prefs = prefs
end
def edit
@page_title = "TRACKS::Edit Preferences"
render :object => prefs
end
def update
user_updated = current_user.update_attributes(params['user'])
prefs_updated = current_user.preference.update_attributes(params['prefs'])
if user_updated && prefs_updated
redirect_to :action => 'index'
else
render :action => 'edit'
end
end
end

View file

@ -0,0 +1,282 @@
class ProjectsController < ApplicationController
helper :application, :todos, :notes
before_filter :set_source_view
before_filter :set_project_from_params, :only => [:update, :destroy, :show, :edit]
before_filter :default_context_filter, :only => [:create, :update]
skip_before_filter :login_required, :only => [:index]
prepend_before_filter :login_or_feed_token_required, :only => [:index]
session :off, :only => :index, :if => Proc.new { |req| ['rss','atom','txt'].include?(req.parameters[:format]) }
def index
@projects = current_user.projects(true)
if params[:projects_and_actions]
projects_and_actions
else
@contexts = current_user.contexts(true)
init_not_done_counts(['project'])
if params[:only_active_with_no_next_actions]
@projects = @projects.select { |p| p.active? && count_undone_todos(p) == 0 }
end
init_project_hidden_todo_counts(['project'])
respond_to do |format|
format.html &render_projects_html
format.m &render_projects_mobile
format.xml { render :xml => @projects.to_xml( :except => :user_id ) }
format.rss &render_rss_feed
format.atom &render_atom_feed
format.text &render_text_feed
end
end
end
def projects_and_actions
@projects = @projects.select { |p| p.active? }
respond_to do |format|
format.text {
render :action => 'index_text_projects_and_actions', :layout => false, :content_type => Mime::TEXT
}
end
end
def show
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
@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
end
@max_completed = current_user.prefs.show_number_completed
@count = @not_done.size
@down_count = @count + @deferred.size
@next_project = current_user.projects.next_from(@project)
@previous_project = current_user.projects.previous_from(@project)
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
respond_to do |format|
format.html
format.m &render_project_mobile
format.xml { render :xml => @project.to_xml( :except => :user_id ) }
end
end
# Example XML usage: curl -H 'Accept: application/xml' -H 'Content-Type:
# application/xml'
# -u username:password
# -d '<request><project><name>new project_name</name></project></request>'
# http://our.tracks.host/projects
#
def create
if params[:format] == 'application/xml' && params['exception']
render_failure "Expected post format is valid xml like so: <request><project><name>project name</name></project></request>."
return
end
@project = current_user.projects.build
params_are_invalid = true
if (params['project'] || (params['request'] && params['request']['project']))
@project.attributes = params['project'] || params['request']['project']
params_are_invalid = false
end
@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'")
@contexts = current_user.contexts
respond_to do |format|
format.js { @down_count = current_user.projects.size }
format.xml do
if @project.new_record? && params_are_invalid
render_failure "Expected post format is valid xml like so: <request><project><name>project name</name></project></request>."
elsif @project.new_record?
render_failure @project.errors.full_messages.join(', ')
else
head :created, :location => project_url(@project)
end
end
end
end
# Edit the details of the project
#
def update
params['project'] ||= {}
if params['project']['state']
@state_changed = @project.state != params['project']['state']
logger.info "@state_changed: #{@project.state} == #{params['project']['state']} != #{@state_changed}"
@project.transition_to(params['project']['state'])
params['project'].delete('state')
end
success_text = if params['field'] == 'name' && params['value']
params['project']['id'] = params['id']
params['project']['name'] = params['value']
end
@project.attributes = params['project']
if @project.save
if boolean_param('wants_render')
if (@project.hidden?)
@project_project_hidden_todo_counts = Hash.new
@project_project_hidden_todo_counts[@project.id] = @project.reload().not_done_todo_count(:include_project_hidden_todos => true)
else
@project_not_done_counts = Hash.new
@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'")
render :template => 'projects/update.js.rjs'
return
elsif boolean_param('update_status')
render :template => 'projects/update_status.js.rjs'
return
elsif boolean_param('update_default_context')
@initial_context_name = @project.default_context.name
render :template => 'projects/update_default_context.js.rjs'
return
else
render :text => success_text || 'Success'
return
end
else
notify :warning, "Couldn't update project"
render :text => ''
return
end
render :template => 'projects/update.js.rjs'
end
def edit
@contexts = current_user.contexts
respond_to do |format|
format.js
end
end
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'")
respond_to do |format|
format.js { @down_count = current_user.projects.size }
format.xml { render :text => "Deleted project #{@project.name}" }
end
end
def order
project_ids = params["list-active-projects"] || params["list-hidden-projects"] || params["list-completed-projects"]
projects = current_user.projects.update_positions( project_ids )
render :nothing => true
rescue
notify :error, $!
redirect_to :action => 'index'
end
def alphabetize
@state = params['state']
@projects = current_user.projects.alphabetize(: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? }
@no_projects = @projects.empty?
@projects.cache_note_counts
@new_project = current_user.projects.build
render
end
end
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? }
@down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size
cookies[:mobile_url]=request.request_uri
render :action => 'index_mobile'
end
end
def render_project_mobile
lambda do
if @project.default_context.nil?
@project_default_context = "This project does not have a default context"
else
@project_default_context = "The default context for this project is "+
@project.default_context.name
end
cookies[:mobile_url]=request.request_uri
render :action => 'project_mobile'
end
end
def render_rss_feed
lambda do
render_rss_feed_for @projects, :feed => feed_options,
:item => { :title => :name, :description => lambda { |p| summary(p) } }
end
end
def render_atom_feed
lambda do
render_atom_feed_for @projects, :feed => feed_options,
:item => { :description => lambda { |p| summary(p) },
:title => :name,
:author => lambda { |p| nil } }
end
end
def feed_options
Project.feed_options(current_user)
end
def render_text_feed
lambda do
init_project_hidden_todo_counts(['project'])
render :action => 'index', :layout => false, :content_type => Mime::TEXT
end
end
def set_project_from_params
@project = current_user.projects.find_by_params(params)
end
def set_source_view
@source_view = params['_source_view'] || 'project'
end
def default_context_filter
p = params['project']
p = params['request']['project'] if p.nil? && params['request']
p = {} if p.nil?
default_context_name = p['default_context_name']
p.delete('default_context_name')
unless default_context_name.blank?
default_context = Context.find_or_create_by_name(default_context_name)
p['default_context_id'] = default_context.id
end
end
def summary(project)
project_description = ''
project_description += sanitize(markdown( project.description )) unless project.description.blank?
project_description += "<p>#{count_undone_todos_phrase(p)}. "
project_description += "Project is #{project.state}."
project_description += "</p>"
project_description
end
end

View file

@ -0,0 +1,780 @@
class StatsController < ApplicationController
helper :todos
append_before_filter :init, :exclude => []
def index
@page_title = 'TRACKS::Statistics'
@unique_tags = @tags.count(:all, {:group=>"tag_id"})
@hidden_contexts = @contexts.find(:all, {:conditions => ["hide = ? ", true]})
@first_action = @actions.find(:first, :order => "created_at ASC")
get_stats_actions
get_stats_contexts
get_stats_projects
get_stats_tags
render :layout => 'standard'
end
def actions_done_last12months_data
@actions = @user.todos
# get actions created and completed in the past 12+3 months. +3 for running
# average
@actions_done_last12months = @actions.find(:all, {
:select => "completed_at",
:conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_year_plus3]
})
@actions_created_last12months = @actions.find(:all, {
:select => "created_at",
:conditions => ["created_at > ?", @cut_off_year_plus3]
})
# convert to hash to be able to fill in non-existing days in
# @actions_done_last12months and count the total actions done in the past
# 12 months to be able to calculate percentage
# use 0 to initialise action count to zero
@actions_done_last12months_hash = Hash.new(0)
@actions_done_last12months.each do |r|
months = (@today.year - r.completed_at.year)*12 + (@today.month - r.completed_at.month)
@actions_done_last12months_hash[months] += 1
end
# convert to hash to be able to fill in non-existing days in
# @actions_created_last12months and count the total actions done in the
# past 12 months to be able to calculate percentage
# use 0 to initialise action count to zero
@actions_created_last12months_hash = Hash.new(0)
@actions_created_last12months.each do |r|
months = (@today.year - r.created_at.year)*12 + (@today.month - r.created_at.month)
@actions_created_last12months_hash[months] += 1
end
@sum_actions_done_last12months=0
@sum_actions_created_last12months=0
# find max for graph in both hashes
@max=0
0.upto 13 do |i|
@sum_actions_done_last12months += @actions_done_last12months_hash[i]
@max = @actions_done_last12months_hash[i] if @actions_done_last12months_hash[i] > @max
end
0.upto 13 do |i|
@sum_actions_created_last12months += @actions_created_last12months_hash[i]
@max = @actions_created_last12months_hash[i] if @actions_created_last12months_hash[i] > @max
end
# find running avg for month i by calculating avg of month i and the two
# after them. Ignore current month because you do not have full data for
# it
@actions_done_avg_last12months_hash = Hash.new("null")
1.upto(12) { |i|
@actions_done_avg_last12months_hash[i] = (@actions_done_last12months_hash[i] +
@actions_done_last12months_hash[i+1] +
@actions_done_last12months_hash[i+2])/3.0
}
# find running avg for month i by calculating avg of month i and the two
# after them. Ignore current month because you do not have full data for
# it
@actions_created_avg_last12months_hash = Hash.new("null")
1.upto(12) { |i|
@actions_created_avg_last12months_hash[i] = (@actions_created_last12months_hash[i] +
@actions_created_last12months_hash[i+1] +
@actions_created_last12months_hash[i+2])/3.0
}
# interpolate avg for this month. Assume 31 days in this month
days_passed_this_month = Time.new.day/1.0
@interpolated_actions_created_this_month = (
@actions_created_last12months_hash[0]/days_passed_this_month*31.0+
@actions_created_last12months_hash[1]+
@actions_created_last12months_hash[2]) / 3.0
@interpolated_actions_done_this_month = (
@actions_done_last12months_hash[0]/days_passed_this_month*31.0 +
@actions_done_last12months_hash[1]+
@actions_done_last12months_hash[2]) / 3.0
render :layout => false
end
def actions_done_last30days_data
# get actions created and completed in the past 30 days.
@actions_done_last30days = @actions.find(:all, {
:select => "completed_at",
:conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_month]
})
@actions_created_last30days = @actions.find(:all, {
:select => "created_at",
:conditions => ["created_at > ?", @cut_off_month]
})
# convert to hash to be able to fill in non-existing days in
# @actions_done_last30days and count the total actions done in the past 30
# days to be able to calculate percentage
@sum_actions_done_last30days=0
# use 0 to initialise action count to zero
@actions_done_last30days_hash = Hash.new(0)
@actions_done_last30days.each do |r|
# only use date part of completed_at
action_date = Time.utc(r.completed_at.year, r.completed_at.month, r.completed_at.day, 0,0)
days = ((@today - action_date) / @seconds_per_day).to_i
@actions_done_last30days_hash[days] += 1
@sum_actions_done_last30days+=1
end
# convert to hash to be able to fill in non-existing days in
# @actions_done_last30days and count the total actions done in the past 30
# days to be able to calculate percentage
@sum_actions_created_last30days=0
# use 0 to initialise action count to zero
@actions_created_last30days_hash = Hash.new(0)
@actions_created_last30days.each do |r|
# only use date part of created_at
action_date = Time.utc(r.created_at.year, r.created_at.month, r.created_at.day, 0,0)
days = ((@today - action_date) / @seconds_per_day).to_i
@actions_created_last30days_hash[days] += 1
@sum_actions_created_last30days += 1
end
# find max for graph in both hashes
@max=0
0.upto(30) { |i| @max = @actions_done_last30days_hash[i] if @actions_done_last30days_hash[i] > @max }
0.upto(30) { |i| @max = @actions_created_last30days_hash[i] if @actions_created_last30days_hash[i] > @max }
render :layout => false
end
def actions_completion_time_data
@actions_completion_time = @actions.find(:all, {
:select => "completed_at, created_at",
:conditions => "completed_at IS NOT NULL"
})
# convert to hash to be able to fill in non-existing days in
# @actions_completion_time also convert days to weeks (/7)
@max_days, @max_actions, @sum_actions=0,0,0
@actions_completion_time_hash = Hash.new(0)
@actions_completion_time.each do |r|
days = (r.completed_at - r.created_at) / @seconds_per_day
weeks = (days/7).to_i
@actions_completion_time_hash[weeks] += 1
@max_days=days if days > @max_days
@max_actions = @actions_completion_time_hash[weeks] if @actions_completion_time_hash[weeks] > @max_actions
@sum_actions += 1
end
# stop the chart after 10 weeks
@cut_off = 10
render :layout => false
end
def actions_running_time_data
@actions_running_time = @actions.find(:all, {
:select => "created_at",
:conditions => "completed_at IS NULL"
})
# convert to hash to be able to fill in non-existing days in
# @actions_running_time also convert days to weeks (/7)
@max_days, @max_actions, @sum_actions=0,0,0
@actions_running_time_hash = Hash.new(0)
@actions_running_time.each do |r|
days = (@today - r.created_at) / @seconds_per_day
weeks = (days/7).to_i
@actions_running_time_hash[weeks] += 1
@max_days=days if days > @max_days
@max_actions = @actions_running_time_hash[weeks] if @actions_running_time_hash[weeks] > @max_actions
@sum_actions += 1
end
# cut off chart at 52 weeks = one year
@cut_off=52
render :layout => false
end
def actions_visible_running_time_data
# running means
# - not completed (completed_at must be null) visible means
# - actions not part of a hidden project
# - actions not part of a hidden context
# - actions not deferred (show_from must be null)
@actions_running_time = @actions.find_by_sql([
"SELECT t.created_at "+
"FROM todos t LEFT OUTER JOIN projects p ON t.project_id = p.id LEFT OUTER JOIN contexts c ON t.context_id = c.id "+
"WHERE t.user_id=? "+
"AND t.completed_at IS NULL " +
"AND t.show_from IS NULL " +
"AND NOT (p.state='hidden' OR c.hide=?) " +
"ORDER BY t.created_at ASC", @user.id, true]
)
# convert to hash to be able to fill in non-existing days in
# @actions_running_time also convert days to weeks (/7)
@max_days, @max_actions, @sum_actions=0,0,0
@actions_running_time_hash = Hash.new(0)
@actions_running_time.each do |r|
days = (@today - r.created_at) / @seconds_per_day
weeks = (days/7).to_i
# RAILS_DEFAULT_LOGGER.error("\n" + total.to_s + " - " + days + "\n")
@actions_running_time_hash[weeks] += 1
@max_days=days if days > @max_days
@max_actions = @actions_running_time_hash[weeks] if @actions_running_time_hash[weeks] > @max_actions
@sum_actions += 1
end
# cut off chart at 52 weeks = one year
@cut_off=52
render :layout => false
end
def context_total_actions_data
# get total action count per context Went from GROUP BY c.id to c.name for
# compatibility with postgresql. Since the name is forced to be unique, this
# should work.
@all_actions_per_context = @contexts.find_by_sql(
"SELECT c.name AS name, c.id as id, count(*) AS total "+
"FROM contexts c, todos t "+
"WHERE t.context_id=c.id "+
"AND t.user_id="+@user.id.to_s+" "+
"GROUP BY c.name, c.id "+
"ORDER BY total DESC"
)
pie_cutoff=10
size = @all_actions_per_context.size()
size = pie_cutoff if size > pie_cutoff
@actions_per_context = Array.new(size)
0.upto size-1 do |i|
@actions_per_context[i] = @all_actions_per_context[i]
end
if size==pie_cutoff
@actions_per_context[size-1]['name']='(others)'
@actions_per_context[size-1]['total']=0
@actions_per_context[size-1]['id']=-1
(size-1).upto @all_actions_per_context.size()-1 do |i|
@actions_per_context[size-1]['total']+=@all_actions_per_context[i]['total'].to_i
end
end
@sum=0
0.upto @all_actions_per_context.size()-1 do |i|
@sum += @all_actions_per_context[i]['total'].to_i
end
@truncate_chars = 15
render :layout => false
end
def context_running_actions_data
# get incomplete action count per visible context
#
# Went from GROUP BY c.id to c.name for compatibility with postgresql. Since
# the name is forced to be unique, this should work.
@all_actions_per_context = @contexts.find_by_sql(
"SELECT c.name AS name, c.id as id, count(*) AS total "+
"FROM contexts c, todos t "+
"WHERE t.context_id=c.id AND t.completed_at IS NULL AND NOT c.hide "+
"AND t.user_id="+@user.id.to_s+" "+
"GROUP BY c.name, c.id "+
"ORDER BY total DESC"
)
pie_cutoff=10
size = @all_actions_per_context.size()
size = pie_cutoff if size > pie_cutoff
@actions_per_context = Array.new(size)
0.upto size-1 do |i|
@actions_per_context[i] = @all_actions_per_context[i]
end
if size==pie_cutoff
@actions_per_context[size-1]['name']='(others)'
@actions_per_context[size-1]['total']=0
@actions_per_context[size-1]['id']=-1
(size-1).upto @all_actions_per_context.size()-1 do |i|
@actions_per_context[size-1]['total']+=@all_actions_per_context[i]['total'].to_i
end
end
@sum=0
0.upto @all_actions_per_context.size()-1 do |i|
@sum += @all_actions_per_context[i]['total'].to_i
end
@truncate_chars = 15
render :layout => false
end
def actions_day_of_week_all_data
@actions = @user.todos
@actions_creation_day = @actions.find(:all, {
:select => "created_at"
})
@actions_completion_day = @actions.find(:all, {
:select => "completed_at",
:conditions => "completed_at IS NOT NULL"
})
# convert to hash to be able to fill in non-existing days
@actions_creation_day_array = Array.new(7) { |i| 0}
@actions_creation_day.each do |t|
# dayofweek: sunday=0..saterday=6
dayofweek = t.created_at.wday
@actions_creation_day_array[dayofweek] += 1
end
# find max
@max=0
0.upto(6) { |i| @max = @actions_creation_day_array[i] if @actions_creation_day_array[i] > @max}
# convert to hash to be able to fill in non-existing days
@actions_completion_day_array = Array.new(7) { |i| 0}
@actions_completion_day.each do |t|
# dayofweek: sunday=0..saterday=6
dayofweek = t.completed_at.wday
@actions_completion_day_array[dayofweek] += 1
end
0.upto(6) { |i| @max = @actions_completion_day_array[i] if @actions_completion_day_array[i] > @max}
render :layout => false
end
def actions_day_of_week_30days_data
@actions_creation_day = @actions.find(:all, {
:select => "created_at",
:conditions => ["created_at > ?", @cut_off_month]
})
@actions_completion_day = @actions.find(:all, {
:select => "completed_at",
:conditions => ["completed_at IS NOT NULL AND completed_at > ?", @cut_off_month]
})
# convert to hash to be able to fill in non-existing days
@max=0
@actions_creation_day_array = Array.new(7) { |i| 0}
@actions_creation_day.each do |r|
# dayofweek: sunday=1..saterday=8
dayofweek = r.created_at.wday
@actions_creation_day_array[dayofweek] += 1
end
0.upto(6) { |i| @max = @actions_creation_day_array[i] if @actions_creation_day_array[i] > @max}
# convert to hash to be able to fill in non-existing days
@actions_completion_day_array = Array.new(7) { |i| 0}
@actions_completion_day.each do |r|
# dayofweek: sunday=1..saterday=7
dayofweek = r.completed_at.wday
@actions_completion_day_array[dayofweek] += 1
end
0.upto(6) { |i| @max = @actions_completion_day_array[i] if @actions_completion_day_array[i] > @max}
render :layout => false
end
def actions_time_of_day_all_data
@actions_creation_hour = @actions.find(:all, {
:select => "created_at"
})
@actions_completion_hour = @actions.find(:all, {
:select => "completed_at",
:conditions => "completed_at IS NOT NULL"
})
# convert to hash to be able to fill in non-existing days
@max=0
@actions_creation_hour_array = Array.new(24) { |i| 0}
@actions_creation_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.created_at).hour
@actions_creation_hour_array[hour] += 1
end
0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max}
# convert to hash to be able to fill in non-existing days
@actions_completion_hour_array = Array.new(24) { |i| 0}
@actions_completion_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.completed_at).hour
@actions_completion_hour_array[hour] += 1
end
0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max}
render :layout => false
end
def actions_time_of_day_30days_data
@actions_creation_hour = @actions.find(:all, {
:select => "created_at",
:conditions => ["created_at > ?", @cut_off_month]
})
@actions_completion_hour = @actions.find(:all, {
:select => "completed_at",
:conditions => ["completed_at IS NOT NULL AND completed_at > ?", @cut_off_month]
})
# convert to hash to be able to fill in non-existing days
@max=0
@actions_creation_hour_array = Array.new(24) { |i| 0}
@actions_creation_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.created_at).hour
@actions_creation_hour_array[hour] += 1
end
0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max}
# convert to hash to be able to fill in non-existing days
@actions_completion_hour_array = Array.new(24) { |i| 0}
@actions_completion_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.completed_at).hour
@actions_completion_hour_array[hour] += 1
end
0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max}
render :layout => false
end
def show_selected_actions_from_chart
@page_title = "TRACKS::Action selection"
@count = 99
@source_view = 'stats'
case params['id']
when 'avrt', 'avrt_end' # actions_visible_running_time
# HACK: because open flash chart uses & to denote the end of a parameter,
# we cannot use URLs with multiple parameters (that would use &). So we
# revert to using two id's for the same selection. avtr_end means that the
# last bar of the chart is selected. avtr is used for all other bars
week_from = params['index'].to_i
week_to = week_from+1
@chart_name = "actions_visible_running_time_data"
@page_title = "Actions selected from week "
if params['id'] == 'avrt_end'
@page_title += week_from.to_s + " and further"
else
@page_title += week_from.to_s + " - " + week_to.to_s + ""
end
# get all running actions that are visible
@actions_running_time = @actions.find_by_sql([
"SELECT t.id, t.created_at "+
"FROM todos t LEFT OUTER JOIN projects p ON t.project_id = p.id LEFT OUTER JOIN contexts c ON t.context_id = c.id "+
"WHERE t.user_id=? "+
"AND t.completed_at IS NULL " +
"AND t.show_from IS NULL " +
"AND NOT (p.state='hidden' OR c.hide=?) " +
"ORDER BY t.created_at ASC", @user.id, true]
)
@selected_todo_ids, @count = get_ids_from(@actions_running_time, week_from, week_to, params['id']== 'avrt_end')
@actions = @user.todos
@selected_actions = @actions.find(:all, {
:conditions => "id in (" + @selected_todo_ids + ")"
})
render :action => "show_selection_from_chart"
when 'art', 'art_end'
week_from = params['index'].to_i
week_to = week_from+1
@chart_name = "actions_running_time_data"
@page_title = "Actions selected from week "
if params['id'] == 'art_end'
@page_title += week_from.to_s + " and further"
else
@page_title += week_from.to_s + " - " + week_to.to_s + ""
end
@actions = @user.todos
# get all running actions
@actions_running_time = @actions.find(:all, {
:select => "id, created_at",
:conditions => "completed_at IS NULL"
})
@selected_todo_ids, @count = get_ids_from(@actions_running_time, week_from, week_to, params['id']=='art_end')
@selected_actions = @actions.find(:all, {
:conditions => "id in (" + @selected_todo_ids + ")"
})
render :action => "show_selection_from_chart"
else
# render error
render_failure "404 NOT FOUND. Unknown query selected"
end
end
private
def init
@actions = @user.todos
@projects = @user.projects
@contexts = @user.contexts
@tags = @user.tags
# default chart dimensions
@chart_width=460
@chart_height=250
@pie_width=@chart_width
@pie_height=325
# get the current date wih time set to 0:0
now = Time.new
@today = Time.utc(now.year, now.month, now.day, 0,0)
# define the number of seconds in a day
@seconds_per_day = 60*60*24
# define cut_off date and discard the time for a month, 3 months and a year
cut_off_time = 13.months.ago()
@cut_off_year = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0)
cut_off_time = 16.months.ago()
@cut_off_year_plus3 = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0)
cut_off_time = 31.days.ago
@cut_off_month = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0)
cut_off_time = 91.days.ago
@cut_off_3months = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0)
end
def get_stats_actions
# time to complete
@completed_actions = @actions.find(:all, {
:select => "completed_at, created_at",
:conditions => "completed_at IS NOT NULL",
})
actions_sum, actions_max, actions_min = 0,0,-1
@completed_actions.each do |r|
actions_sum += (r.completed_at - r.created_at)
actions_max = (r.completed_at - r.created_at) if (r.completed_at - r.created_at) > actions_max
actions_min = (r.completed_at - r.created_at) if actions_min == -1
actions_min = (r.completed_at - r.created_at) if (r.completed_at - r.created_at) < actions_min
end
sum_actions = @completed_actions.size
sum_actions = 1 if sum_actions==0
@actions_avg_ttc = (actions_sum/sum_actions)/@seconds_per_day
@actions_max_ttc = actions_max/@seconds_per_day
@actions_min_ttc = actions_min/@seconds_per_day
min_ttc_sec = Time.utc(2000,1,1,0,0)+actions_min
@actions_min_ttc_sec = (min_ttc_sec).strftime("%H:%M:%S")
@actions_min_ttc_sec = (actions_min / @seconds_per_day).round.to_s + " days " + @actions_min_ttc_sec if actions_min > @seconds_per_day
# get count of actions created and actions done in the past 30 days.
@sum_actions_done_last30days = @actions.count(:all, {
:conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_month]
})
@sum_actions_created_last30days = @actions.count(:all, {
:conditions => ["created_at > ?", @cut_off_month]
})
# get count of actions done in the past 12 months.
@sum_actions_done_last12months = @actions.count(:all, {
:conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_year]
})
@sum_actions_created_last12months = @actions.count(:all, {
:conditions => ["created_at > ?", @cut_off_year]
})
end
def get_stats_contexts
# get action count per context for TOP 5
#
# Went from GROUP BY c.id to c.id, c.name for compatibility with postgresql.
# Since the name is forced to be unique, this should work.
@actions_per_context = @contexts.find_by_sql(
"SELECT c.id AS id, c.name AS name, count(*) AS total "+
"FROM contexts c, todos t "+
"WHERE t.context_id=c.id "+
"AND t.user_id="+@user.id.to_s+" "+
"GROUP BY c.id, c.name ORDER BY total DESC " +
"LIMIT 5"
)
# get incomplete action count per visible context for TOP 5
#
# Went from GROUP BY c.id to c.id, c.name for compatibility with postgresql.
# Since the name is forced to be unique, this should work.
@running_actions_per_context = @contexts.find_by_sql(
"SELECT c.id AS id, c.name AS name, count(*) AS total "+
"FROM contexts c, todos t "+
"WHERE t.context_id=c.id AND t.completed_at IS NULL AND NOT c.hide "+
"AND t.user_id="+@user.id.to_s+" "+
"GROUP BY c.id, c.name ORDER BY total DESC " +
"LIMIT 5"
)
end
def get_stats_projects
# get the first 10 projects and their action count (all actions)
#
# Went from GROUP BY p.id to p.name for compatibility with postgresql. Since
# the name is forced to be unique, this should work.
@projects_and_actions = @projects.find_by_sql(
"SELECT p.id, p.name, count(*) AS count "+
"FROM projects p, todos t "+
"WHERE p.id = t.project_id "+
"AND p.user_id="+@user.id.to_s+" "+
"GROUP BY p.id, p.name "+
"ORDER BY count DESC " +
"LIMIT 10"
)
# get the first 10 projects with their actions count of actions that have
# been created or completed the past 30 days
# using GROUP BY p.name (was: p.id) for compatibility with Postgresql. Since
# you cannot create two contexts with the same name, this will work.
@projects_and_actions_last30days = @projects.find_by_sql([
"SELECT p.id, p.name, count(*) AS count "+
"FROM todos t, projects p "+
"WHERE t.project_id = p.id AND "+
" (t.created_at > ? OR t.completed_at > ?) "+
"AND p.user_id=? "+
"GROUP BY p.id, p.name "+
"ORDER BY count DESC " +
"LIMIT 10", @cut_off_month, @cut_off_month, @user.id]
)
# get the first 10 projects and their running time (creation date versus
# now())
@projects_and_runtime_sql = @projects.find_by_sql(
"SELECT id, name, created_at "+
"FROM projects "+
"WHERE state='active' "+
"AND user_id="+@user.id.to_s+" "+
"ORDER BY created_at ASC "+
"LIMIT 10"
)
i=0
@projects_and_runtime = Array.new(10, [-1, "n/a", "n/a"])
@projects_and_runtime_sql.each do |r|
days = (@today - r.created_at) / @seconds_per_day
# add one so that a project that you just create returns 1 day
@projects_and_runtime[i]=[r.id, r.name, days.to_i+1]
i += 1
end
end
def get_stats_tags
# tag cloud code inspired by this article
# http://www.juixe.com/techknow/index.php/2006/07/15/acts-as-taggable-tag-cloud/
levels=10
# TODO: parameterize limit
# Get the tag cloud for all tags for actions
query = "SELECT tags.id, name, count(*) AS count"
query << " FROM taggings, tags"
query << " WHERE tags.id = tag_id"
query << " AND taggings.user_id="+@user.id.to_s+" "
query << " AND taggings.taggable_type='Todo' "
query << " GROUP BY tags.id, tags.name"
query << " ORDER BY count DESC, name"
query << " LIMIT 100"
@tags_for_cloud = Tag.find_by_sql(query).sort_by { |tag| tag.name.downcase }
max, @tags_min = 0, 0
@tags_for_cloud.each { |t|
max = t.count.to_i if t.count.to_i > max
@tags_min = t.count.to_i if t.count.to_i < @tags_min
}
@tags_divisor = ((max - @tags_min) / levels) + 1
# Get the tag cloud for all tags for actions
query = "SELECT tags.id, tags.name AS name, count(*) AS count"
query << " FROM taggings, tags, todos"
query << " WHERE tags.id = tag_id"
query << " AND taggings.user_id=? "
query << " AND taggings.taggable_type='Todo' "
query << " AND taggings.taggable_id=todos.id "
query << " AND (todos.created_at > ? OR "
query << " todos.completed_at > ?) "
query << " GROUP BY tags.id, tags.name"
query << " ORDER BY count DESC, name"
query << " LIMIT 100"
@tags_for_cloud_90days = Tag.find_by_sql(
[query, @user.id, @cut_off_3months, @cut_off_3months]
).sort_by { |tag| tag.name.downcase }
max_90days, @tags_min_90days = 0, 0
@tags_for_cloud_90days.each { |t|
max_90days = t.count.to_i if t.count.to_i > max_90days
@tags_min_90days = t.count.to_i if t.count.to_i < @tags_min_90days
}
@tags_divisor_90days = ((max_90days - @tags_min_90days) / levels) + 1
end
def get_ids_from (actions_running_time, week_from, week_to, at_end)
count=0
selected_todo_ids = ""
actions_running_time.each do |r|
days = (@today - r.created_at) / @seconds_per_day
weeks = (days/7).to_i
if at_end
if weeks >= week_from
selected_todo_ids += r.id.to_s+","
count+=1
end
else
if weeks.between?(week_from, week_to-1)
selected_todo_ids += r.id.to_s+","
count+=1
end
end
end
# strip trailing comma
selected_todo_ids = selected_todo_ids[0..selected_todo_ids.length-2]
return selected_todo_ids, count
end
end

View file

@ -0,0 +1,708 @@
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 ]
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
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
@contexts_to_show = @contexts.reject {|x| x.hide? }
respond_to do |format|
format.html &render_todos_html
format.m &render_todos_mobile
format.xml { render :xml => @todos.to_xml( :except => :user_id ) }
format.rss &render_rss_feed
format.atom &render_atom_feed
format.text &render_text_feed
format.ics &render_ical_feed
end
end
def new
@projects = current_user.projects.select { |p| p.active? }
@contexts = current_user.contexts.find(:all)
respond_to do |format|
format.m {
@new_mobile = true
@return_path=cookies[:mobile_url]
render :action => "new"
}
end
end
def create
@source_view = params['_source_view'] || 'todo'
p = TodoCreateParamsHelper.new(params, prefs)
p.parse_dates() unless mobile?
@todo = current_user.todos.build(p.attributes)
if p.project_specified_by_name?
project = current_user.projects.find_or_create_by_name(p.project_name)
@new_project_created = project.new_record_before_save?
@todo.project_id = project.id
end
if p.context_specified_by_name?
context = current_user.contexts.find_or_create_by_name(p.context_name)
@new_context_created = context.new_record_before_save?
@not_done_todos = [@todo] if @new_context_created
@todo.context_id = context.id
end
@saved = @todo.save
unless (@saved == false) || p.tag_list.blank?
@todo.tag_with(p.tag_list, current_user)
@todo.tags.reload
end
respond_to do |format|
format.html { redirect_to :action => "index" }
format.m do
if @saved
redirect_to mobile_abbrev_url
else
@projects = current_user.projects.find(:all)
@contexts = current_user.contexts.find(:all)
render :action => "new"
end
end
format.js do
determine_down_count if @saved
@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']
render :action => 'create'
end
format.xml do
if @saved
head :created, :location => todo_url(@todo)
else
render :xml => @todo.errors.to_xml, :status => 422
end
end
end
end
def edit
@projects = current_user.projects.find(:all)
@contexts = current_user.contexts.find(:all)
@source_view = params['_source_view'] || 'todo'
respond_to do |format|
format.js
end
end
def show
respond_to do |format|
format.m do
@projects = current_user.projects.select { |p| p.active? }
@contexts = current_user.contexts.find(:all)
@edit_mobile = true
@return_path=cookies[:mobile_url]
render :action => 'show'
end
format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) }
end
end
# Toggles the 'done' status of the action
#
def toggle_check
@saved = @todo.toggle_completion!
respond_to do |format|
format.js do
if @saved
determine_remaining_in_context_count(@todo.context_id)
determine_down_count
determine_completed_count if @todo.completed?
end
render
end
format.xml { render :xml => @todo.to_xml( :except => :user_id ) }
format.html do
if @saved
# TODO: I think this will work, but can't figure out how to test it
notify :notice, "The action <strong>'#{@todo.description}'</strong> was marked as <strong>#{@todo.completed? ? 'complete' : 'incomplete' }</strong>"
redirect_to :action => "index"
else
notify :notice, "The action <strong>'#{@todo.description}'</strong> was NOT marked as <strong>#{@todo.completed? ? 'complete' : 'incomplete' } due to an error on the server.</strong>", "index"
redirect_to :action => "index"
end
end
end
end
def toggle_star
@todo.toggle_star!
@saved = @todo.save!
respond_to do |format|
format.js
format.xml { render :xml => @todo.to_xml( :except => :user_id ) }
end
end
def update
init_data_for_sidebar unless mobile?
@todo.tag_with(params[:tag_list], current_user) if params[:tag_list]
@original_item_context_id = @todo.context_id
@original_item_project_id = @todo.project_id
@original_item_was_deferred = @todo.deferred?
if params['todo']['project_id'].blank? && !params['project_name'].nil?
if params['project_name'] == 'None'
project = Project.null_object
else
project = current_user.projects.find_by_name(params['project_name'].strip)
unless project
project = current_user.projects.build
project.name = params['project_name'].strip
project.save
@new_project_created = true
end
end
params["todo"]["project_id"] = project.id
end
if params['todo']['context_id'].blank? && !params['context_name'].blank?
context = current_user.contexts.find_by_name(params['context_name'].strip)
unless context
context = current_user.contexts.build
context.name = params['context_name'].strip
context.save
@new_context_created = true
@not_done_todos = [@todo]
end
params["todo"]["context_id"] = context.id
end
if params["todo"].has_key?("due")
params["todo"]["due"] = parse_date_per_user_prefs(params["todo"]["due"])
else
params["todo"]["due"] = ""
end
if params['todo']['show_from']
params['todo']['show_from'] = parse_date_per_user_prefs(params['todo']['show_from'])
end
if params['done'] == '1' && !@todo.completed?
@todo.complete!
end
# strange. if checkbox is not checked, there is no 'done' in params.
# Therfore I've used the negation
if !(params['done'] == '1') && @todo.completed?
@todo.activate!
end
@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
@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
respond_to do |format|
format.js
format.xml { render :xml => @todo.to_xml( :except => :user_id ) }
format.m do
if @saved
if cookies[:mobile_url]
cookies[:mobile_url] = nil
redirect_to cookies[:mobile_url]
else
redirect_to formatted_todos_path(:m)
end
else
render :action => "edit", :format => :m
end
end
end
end
def destroy
@todo = get_todo_from_params
@context_id = @todo.context_id
@project_id = @todo.project_id
@saved = @todo.destroy
respond_to do |format|
format.html do
if @saved
notify :notice, "Successfully deleted next action", 2.0
redirect_to :action => 'index'
else
notify :error, "Failed to delete the action", 2.0
redirect_to :action => 'index'
end
end
format.js do
if @saved
determine_down_count
if source_view_is_one_of(:todo, :deferred)
determine_remaining_in_context_count(@context_id)
end
end
render
end
format.xml { render :text => '200 OK. Action deleted.', :status => 200 }
end
end
def completed
@page_title = "TRACKS::Completed tasks"
@done = current_user.completed_todos
@done_today = @done.completed_within current_user.time - 1.day
@done_this_week = @done.completed_within current_user.time - 1.week
@done_this_month = @done.completed_within current_user.time - 4.week
@count = @done_today.size + @done_this_week.size + @done_this_month.size
end
def completed_archive
@page_title = "TRACKS::Archived completed tasks"
@done = current_user.completed_todos
@count = @done.size
@done_archive = @done.completed_more_than current_user.time - 28.days
end
def list_deferred
@source_view = 'deferred'
@page_title = "TRACKS::Tickler"
@projects = current_user.projects.find(:all, :include => [ :todos, :default_context ])
@contexts_to_show = @contexts = current_user.contexts.find(:all, :include => [ :todos ])
current_user.deferred_todos.find_and_activate_ready
@not_done_todos = current_user.deferred_todos
@count = @not_done_todos.size
@down_count = @count
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json unless mobile?
respond_to do |format|
format.html
format.m { render :action => 'mobile_list_deferred' }
end
end
# Check for any due tickler items, activate them Called by
# periodically_call_remote
def check_deferred
@due_tickles = current_user.deferred_todos.find_and_activate_ready
respond_to do |format|
format.html { redirect_to home_path }
format.js
end
end
def filter_to_context
context = current_user.contexts.find(params['context']['id'])
redirect_to formatted_context_todos_path(context, :m)
end
def filter_to_project
project = current_user.projects.find(params['project']['id'])
redirect_to formatted_project_todos_path(project, :m)
end
# /todos/tag/[tag_name] shows all the actions tagged with tag_name
def tag
@source_view = params['_source_view'] || 'tag'
@tag_name = params[:name]
# mobile tags are routed with :name ending on .m. So we need to chomp it
if mobile?
@tag_name = @tag_name.chomp('.m')
end
@tag = Tag.find_by_name(@tag_name)
if @tag.nil?
@tag = Tag.new(:name => @tag_name)
end
tag_collection = @tag.todos
@not_done_todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active'])
@contexts = current_user.contexts.find(:all, :include => [ :todos ])
@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
format.m {
cookies[:mobile_url]=request.request_uri
render :action => "mobile_tag"
}
end
end
private
def get_todo_from_params
@todo = current_user.todos.find(params['id'])
end
def init
@source_view = params['_source_view'] || 'todo'
init_data_for_sidebar unless mobile?
init_todos
end
def with_feed_query_scope(&block)
unless TodosController.is_feed_request(request)
Todo.send(:with_scope, :find => {:conditions => ['todos.state = ?', 'active']}) do
yield
return
end
end
condition_builder = FindConditionBuilder.new
if params.key?('done')
condition_builder.add 'todos.state = ?', 'completed'
else
condition_builder.add 'todos.state = ?', 'active'
end
@title = "Tracks - Next Actions"
@description = "Filter: "
if params.key?('due')
due_within = params['due'].to_i
due_within_when = current_user.time + due_within.days
condition_builder.add('todos.due <= ?', due_within_when)
due_within_date_s = due_within_when.strftime("%Y-%m-%d")
@title << " due today" if (due_within == 0)
@title << " due within a week" if (due_within == 6)
@description << " with a due date #{due_within_date_s} or earlier"
end
if params.key?('done')
done_in_last = params['done'].to_i
condition_builder.add('todos.completed_at >= ?', current_user.time - done_in_last.days)
@title << " actions completed"
@description << " in the last #{done_in_last.to_s} days"
end
Todo.send :with_scope, :find => {:conditions => condition_builder.to_conditions} do
yield
end
end
def with_parent_resource_scope(&block)
if (params[:context_id])
@context = current_user.contexts.find_by_params(params)
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)
Todo.send :with_scope, :find => {:conditions => ['todos.project_id = ?', @project.id]} do
yield
end
else
yield
end
end
def with_limit_scope(&block)
if params.key?('limit')
Todo.send :with_scope, :find => { :limit => params['limit'] } do
yield
end
if TodosController.is_feed_request(request) && @description
if params.key?('limit')
@description << "Lists the last #{params['limit']} incomplete next actions"
else
@description << "Lists incomplete next actions"
end
end
else
yield
end
end
def init_todos
with_feed_query_scope do
with_parent_resource_scope do # @context or @project may get defined here
with_limit_scope do
if mobile?
init_todos_for_mobile_view
else
# Note: these next two finds were previously using
# current_users.todos.find but that broke with_scope for :limit
# Exclude hidden projects from count on home page
@todos = Todo.find(:all, :conditions => ['todos.user_id = ?', current_user.id], :include => [ :project, :context, :tags ])
# Exclude hidden projects from the home page
@not_done_todos = Todo.find(:all, :conditions => ['todos.user_id = ? AND contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', current_user.id, false, 'active'], :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => [ :project, :context, :tags ])
end
end
end
end
end
def init_todos_for_mobile_view
# Note: these next two finds were previously using current_users.todos.find
# but that broke with_scope for :limit
# Exclude hidden projects from the home page
@not_done_todos = Todo.find(:all,
:conditions => ['todos.user_id = ? AND todos.state = ? AND contexts.hide = ?',
current_user.id, 'active', false],
:order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC",
:include => [ :project, :context ])
end
def determine_down_count
source_view do |from|
from.todo do
@down_count = Todo.count(
:all,
:conditions => ['todos.user_id = ? and todos.state = ? and contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', current_user.id, 'active', false, 'active'],
:include => [ :project, :context ])
# #@down_count = Todo.count_by_sql(['SELECT COUNT(*) FROM todos,
# contexts WHERE todos.context_id = contexts.id and todos.user_id = ?
# and todos.state = ? and contexts.hide = ?', current_user.id, 'active',
# false])
end
from.context do
@down_count = current_user.contexts.find(@todo.context_id).not_done_todo_count
end
from.project do
unless @todo.project_id == nil
@down_count = current_user.projects.find(@todo.project_id).not_done_todo_count(:include_project_hidden_todos => true)
@deferred_count = current_user.projects.find(@todo.project_id).deferred_todo_count
end
end
from.deferred do
@down_count = current_user.todos.count_in_state(:deferred)
end
from.tag do
@tag_name = params['_tag_name']
@tag = Tag.find_by_name(@tag_name)
if @tag.nil?
@tag = Tag.new(:name => @tag_name)
end
tag_collection = @tag.todos
@not_done_todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active'])
@not_done_todos.empty? ? @down_count = 0 : @down_count = @not_done_todos.size
end
end
end
def determine_remaining_in_context_count(context_id = @todo.context_id)
if source_view_is :deferred
@remaining_in_context = current_user.contexts.find(context_id).deferred_todo_count
else
@remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count
end
end
def determine_completed_count
source_view do |from|
from.todo do
@completed_count = Todo.count_by_sql(['SELECT COUNT(*) FROM todos, contexts WHERE todos.context_id = contexts.id and todos.user_id = ? and todos.state = ? and contexts.hide = ?', current_user.id, 'completed', false])
end
from.context do
@completed_count = current_user.contexts.find(@todo.context_id).done_todo_count
end
from.project do
unless @todo.project_id == nil
@completed_count = current_user.projects.find(@todo.project_id).done_todo_count
end
end
end
end
def render_todos_html
lambda do
@page_title = "TRACKS::List tasks"
# 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 = current_user.completed_todos.find(:all, :limit => max_completed, :include => [ :context, :project, :tags ]) unless max_completed == 0
# Set count badge to number of not-done, not hidden context items
@count = 0
@todos.each do |x|
if x.active?
if x.project.nil?
@count += 1 if !x.context.hide?
else
@count += 1 if x.project.active? && !x.context.hide?
end
end
end
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
render
end
end
def render_todos_mobile
lambda do
@page_title = "All actions"
@home = true
cookies[:mobile_url]=request.request_uri
determine_down_count
render :action => 'index'
end
end
def render_rss_feed
lambda do
render_rss_feed_for @todos, :feed => todo_feed_options,
:item => {
:title => :description,
:link => lambda { |t| context_url(t.context) },
:guid => lambda { |t| todo_url(t) },
:description => todo_feed_content
}
end
end
def todo_feed_options
Todo.feed_options(current_user)
end
def todo_feed_content
lambda do |i|
item_notes = sanitize(markdown( i.notes )) if i.notes?
due = "<div>Due: #{format_date(i.due)}</div>\n" if i.due?
done = "<div>Completed: #{format_date(i.completed_at)}</div>\n" if i.completed?
context_link = "<a href=\"#{ context_url(i.context) }\">#{ i.context.name }</a>"
if i.project_id?
project_link = "<a href=\"#{ project_url(i.project) }\">#{ i.project.name }</a>"
else
project_link = "<em>none</em>"
end
"#{done||''}#{due||''}#{item_notes||''}\n<div>Project: #{project_link}</div>\n<div>Context: #{context_link}</div>"
end
end
def render_atom_feed
lambda do
render_atom_feed_for @todos, :feed => todo_feed_options,
:item => {
:title => :description,
:link => lambda { |t| context_url(t.context) },
:description => todo_feed_content,
:author => lambda { |p| nil }
}
end
end
def render_text_feed
lambda do
render :action => 'index', :layout => false, :content_type => Mime::TEXT
end
end
def render_ical_feed
lambda do
render :action => 'index', :layout => false, :content_type => Mime::ICS
end
end
def self.is_feed_request(req)
['rss','atom','txt','ics'].include?(req.parameters[:format])
end
class FindConditionBuilder
def initialize
@queries = Array.new
@params = Array.new
end
def add(query, param)
@queries << query
@params << param
end
def to_conditions
[@queries.join(' AND ')] + @params
end
end
class TodoCreateParamsHelper
def initialize(params, prefs)
@params = params['request'] || params
@prefs = prefs
@attributes = params['request'] && params['request']['todo'] || params['todo']
end
def attributes
@attributes
end
def show_from
@attributes['show_from']
end
def due
@attributes['due']
end
def project_name
@params['project_name'].strip unless @params['project_name'].nil?
end
def context_name
@params['context_name'].strip unless @params['context_name'].nil?
end
def tag_list
@params['tag_list']
end
def parse_dates()
@attributes['show_from'] = @prefs.parse_date(show_from)
@attributes['due'] = @prefs.parse_date(due)
@attributes['due'] ||= ''
end
def project_specified_by_name?
return false unless @attributes['project_id'].blank?
return false if project_name.blank?
return false if project_name == 'None'
true
end
def context_specified_by_name?
return false unless @attributes['context_id'].blank?
return false if context_name.blank?
true
end
end
end

View file

@ -0,0 +1,251 @@
class UsersController < ApplicationController
if Tracks::Config.openid_enabled?
open_id_consumer
before_filter :begin_open_id_auth, :only => :update_auth_type
end
before_filter :admin_login_required, :only => [ :index, :show, :destroy ]
skip_before_filter :login_required, :only => [ :new, :create ]
prepend_before_filter :login_optional, :only => [ :new, :create ]
# GET /users
# GET /users.xml
def index
@users = User.find(:all, :order => 'login')
respond_to do |format|
format.html do
@page_title = "TRACKS::Manage Users"
@user_pages, @users = paginate :users, :order => 'login ASC', :per_page => 10
@total_users = User.count
# When we call users/signup from the admin page
# we store the URL so that we get returned here when signup is successful
store_location
end
format.xml { render :xml => @users.to_xml(:except => [ :password ]) }
end
end
# GET /users/somelogin
# GET /users/somelogin.xml
def show
@user = User.find_by_login(params[:id])
render :xml => @user.to_xml(:except => [ :password ])
end
# GET /users/new
def new
if User.no_users_yet?
@page_title = "TRACKS::Sign up as the admin user"
@heading = "Welcome to TRACKS. To get started, please create an admin account:"
@user = get_new_user
elsif @user && @user.is_admin?
@page_title = "TRACKS::Sign up a new user"
@heading = "Sign up a new user:"
@user = get_new_user
else # all other situations (i.e. a non-admin is logged in, or no one is logged in, but we have some users)
@page_title = "TRACKS::No signups"
@admin_email = User.find_admin.preference.admin_email
render :action => "nosignup", :layout => "login"
return
end
render :layout => "login"
end
# Example usage: curl -H 'Accept: application/xml' -H 'Content-Type: application/xml'
# -u admin:up2n0g00d
# -d '<request><login>username</login><password>abc123</password></request>'
# http://our.tracks.host/users
#
# POST /users
# POST /users.xml
def create
if params['exception']
render_failure "Expected post format is valid xml like so: <request><login>username</login><password>abc123</password></request>."
return
end
respond_to do |format|
format.html do
unless User.no_users_yet? || (@user && @user.is_admin?)
@page_title = "No signups"
@admin_email = User.find_admin.preference.admin_email
render :action => "nosignup", :layout => "login"
return
end
user = User.new(params['user'])
unless user.valid?
session['new_user'] = user
redirect_to :action => 'new'
return
end
first_user_signing_up = User.no_users_yet?
user.is_admin = true if first_user_signing_up
if user.save
@user = User.authenticate(user.login, params['user']['password'])
@user.create_preference
@user.save
session['user_id'] = @user.id if first_user_signing_up
notify :notice, "Signup successful for user #{@user.login}."
redirect_back_or_home
end
return
end
format.xml do
unless User.find_by_id_and_is_admin(session['user_id'], true)
render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401
return
end
unless check_create_user_params
render_failure "Expected post format is valid xml like so: <request><login>username</login><password>abc123</password></request>."
return
end
user = User.new(params[:request])
user.password_confirmation = params[:request][:password]
if user.save
render :text => "User created.", :status => 200
else
render_failure user.errors.to_xml
end
return
end
end
end
# DELETE /users/somelogin
# DELETE /users/somelogin.xml
def destroy
@deleted_user = User.find_by_id(params[:id])
@saved = @deleted_user.destroy
@total_users = User.find(:all).size
respond_to do |format|
format.html do
if @saved
notify :notice, "Successfully deleted user #{@deleted_user.login}", 2.0
else
notify :error, "Failed to delete user #{@deleted_user.login}", 2.0
end
redirect_to users_url
end
format.js
format.xml { head :ok }
end
end
def change_password
@page_title = "TRACKS::Change password"
end
def update_password
@user.change_password(params[:updateuser][:password], params[:updateuser][:password_confirmation])
notify :notice, "Password updated."
redirect_to preferences_path
rescue Exception => error
notify :error, error.message
redirect_to :action => 'change_password'
end
def change_auth_type
@page_title = "TRACKS::Change authentication type"
end
def update_auth_type
if (params[:user][:auth_type] == 'open_id') && Tracks::Config.openid_enabled?
case open_id_response.status
when OpenID::SUCCESS
# The URL was a valid identity URL. Now we just need to send a redirect
# to the server using the redirect_url the library created for us.
session['openid_url'] = params[:openid_url]
# redirect to the server
redirect_to open_id_response.redirect_url((request.protocol + request.host_with_port + "/"), url_for(:action => 'complete'))
else
notify :warning, "Unable to find openid server for <q>#{openid_url}</q>"
redirect_to :action => 'change_auth_type'
end
return
end
@user.auth_type = params[:user][:auth_type]
if @user.save
notify :notice, "Authentication type updated."
redirect_to preferences_path
else
notify :warning, "There was a problem updating your authentication type: #{ @user.errors.full_messages.join(', ')}"
redirect_to :action => 'change_auth_type'
end
end
def complete
return unless Tracks::Config.openid_enabled?
openid_url = session['openid_url']
if openid_url.blank?
notify :error, "expected an openid_url"
end
case open_id_response.status
when OpenID::FAILURE
# In the case of failure, if info is non-nil, it is the
# URL that we were verifying. We include it in the error
# message to help the user figure out what happened.
if open_id_response.identity_url
msg = "Verification of #{openid_url}(#{open_id_response.identity_url}) failed. "
else
msg = "Verification failed. "
end
notify :error, open_id_response.msg.to_s + msg
when OpenID::SUCCESS
# Success means that the transaction completed without
# error. If info is nil, it means that the user cancelled
# the verification.
@user.auth_type = 'open_id'
@user.open_id_url = openid_url
if @user.save
notify :notice, "You have successfully verified #{openid_url} as your identity and set your authentication type to Open ID."
else
notify :warning, "You have successfully verified #{openid_url} as your identity but there was a problem saving your authentication preferences."
end
redirect_to preferences_path
when OpenID::CANCEL
notify :warning, "Verification cancelled."
else
notify :warning, "Unknown response status: #{open_id_response.status}"
end
redirect_to :action => 'change_auth_type' unless performed?
end
def refresh_token
@user.generate_token
@user.save!
notify :notice, "New token successfully generated"
redirect_to preferences_path
end
private
def get_new_user
if session['new_user']
user = session['new_user']
session['new_user'] = nil
else
user = User.new
end
user
end
def check_create_user_params
return false unless params.has_key?(:request)
return false unless params[:request].has_key?(:login)
return false if params[:request][:login].empty?
return false unless params[:request].has_key?(:password)
return false if params[:request][:password].empty?
return true
end
end

View file

@ -0,0 +1,147 @@
# The methods added to this helper will be available to all templates in the
# application.
module ApplicationHelper
def user_time
current_user.time
end
# Replicates the link_to method but also checks request.request_uri to find
# current page. If that matches the url, the link is marked id = "current"
#
def navigation_link(name, options = {}, html_options = nil, *parameters_for_method_reference)
if html_options
html_options = html_options.stringify_keys
convert_options_to_javascript!(html_options)
tag_options = tag_options(html_options)
else
tag_options = nil
end
url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference)
id_tag = (request.request_uri == url) ? " id=\"current\"" : ""
"<a href=\"#{url}\"#{tag_options}#{id_tag}>#{name || url}</a>"
end
def days_from_today(date)
date.to_date - user_time.to_date
end
# Check due date in comparison to today's date Flag up date appropriately with
# a 'traffic light' colour code
#
def due_date(due)
if due == nil
return ""
end
days = days_from_today(due)
case days
when 0
"<a title='#{format_date(due)}'><span class=\"amber\">Due Today</span></a> "
when 1
"<a title='#{format_date(due)}'><span class=\"amber\">Due Tomorrow</span></a> "
# due 2-7 days away
when 2..7
if prefs.due_style == Preference.due_styles[:due_on]
"<a title='#{format_date(due)}'><span class=\"orange\">Due on #{due.strftime("%A")}</span></a> "
else
"<a title='#{format_date(due)}'><span class=\"orange\">Due in #{pluralize(days, 'day')}</span></a> "
end
else
# overdue or due very soon! sound the alarm!
if days < 0
"<a title='#{format_date(due)}'><span class=\"red\">Overdue by #{pluralize(days * -1, 'day')}</span></a> "
else
# more than a week away - relax
"<a title='#{format_date(due)}'><span class=\"green\">Due in #{pluralize(days, 'day')}</span></a> "
end
end
end
# Check due date in comparison to today's date Flag up date appropriately with
# a 'traffic light' colour code Modified method for mobile screen
#
def due_date_mobile(due)
if due == nil
return ""
end
days = days_from_today(due)
case days
when 0
"<span class=\"amber\">"+ format_date(due) + "</span>"
when 1
"<span class=\"amber\">" + format_date(due) + "</span>"
# due 2-7 days away
when 2..7
"<span class=\"orange\">" + format_date(due) + "</span>"
else
# overdue or due very soon! sound the alarm!
if days < 0
"<span class=\"red\">" + format_date(due) +"</span>"
else
# more than a week away - relax
"<span class=\"green\">" + format_date(due) + "</span>"
end
end
end
# Returns a count of next actions in the given context or project. The result
# is count and a string descriptor, correctly pluralised if there are no
# actions or multiple actions
#
def count_undone_todos_phrase(todos_parent, string="actions")
@controller.count_undone_todos_phrase(todos_parent, string)
end
def count_undone_todos_phrase_text(todos_parent, string="actions")
count_undone_todos_phrase(todos_parent, string).gsub("&nbsp;"," ")
end
def count_undone_todos_and_notes_phrase(project, string="actions")
s = count_undone_todos_phrase(project, string)
s += ", #{pluralize(project.note_count, 'note')}" unless project.note_count == 0
s
end
def link_to_context(context, descriptor = sanitize(context.name))
link_to( descriptor, context_path(context), :title => "View context: #{context.name}" )
end
def link_to_project(project, descriptor = sanitize(project.name))
link_to( descriptor, project_path(project), :title => "View project: #{project.name}" )
end
def link_to_project_mobile(project, accesskey, descriptor = sanitize(project.name))
link_to( descriptor, formatted_project_path(project, :m), {:title => "View project: #{project.name}", :accesskey => accesskey} )
end
def item_link_to_context(item)
descriptor = "[C]"
descriptor = "[#{item.context.name}]" if prefs.verbose_action_descriptors
link_to_context( item.context, descriptor )
end
def item_link_to_project(item)
descriptor = "[P]"
descriptor = "[#{item.project.name}]" if prefs.verbose_action_descriptors
link_to_project( item.project, descriptor )
end
def render_flash
render :partial => 'shared/flash', :locals => { :flash => flash }
end
# Display a flash message in RJS templates Usage: page.notify :warning, "This
# is the message", 5.0 Puts the message into a flash of type 'warning', fades
# over 5 secs
def notify(type, message, fade_duration)
type = type.to_s # symbol to string
page.replace 'flash', "<h4 id='flash' class='alert #{type}'>#{message}</h4>"
page.visual_effect :fade, 'flash', :duration => fade_duration
end
end

View file

@ -0,0 +1,2 @@
module BackendHelper
end

View file

@ -0,0 +1,12 @@
module ContextsHelper
def get_listing_sortable_options
{
:tag => 'div',
:handle => 'handle',
:complete => visual_effect(:highlight, 'list-contexts'),
:url => order_contexts_path
}
end
end

View file

@ -0,0 +1,2 @@
module DataHelper
end

View file

@ -0,0 +1,29 @@
module FeedlistHelper
def rss_formatted_link(options = {})
image_tag = image_tag("feed-icon.png", :size => "16X16", :border => 0, :class => "rss-icon")
linkoptions = merge_hashes( {:format => 'rss'}, user_token_hash, options)
link_to(image_tag, linkoptions, :title => "RSS feed")
end
def text_formatted_link(options = {})
linkoptions = merge_hashes( {:format => 'txt'}, user_token_hash, options)
link_to('<span class="feed">TXT</span>', linkoptions, :title => "Plain text feed" )
end
def ical_formatted_link(options = {})
linkoptions = merge_hashes( {:format => 'ics'}, user_token_hash, options)
link_to('<span class="feed">iCal</span>', linkoptions, :title => "iCal feed" )
end
protected
def merge_hashes(*hashes)
hashes.inject(Hash.new){ |result, h| result.merge(h) }
end
def user_token_hash
{ :token => current_user.token }
end
end

View file

@ -0,0 +1,2 @@
module IntegrationsHelper
end

View file

@ -0,0 +1,3 @@
module LoginHelper
end

View file

@ -0,0 +1,2 @@
module MobileHelper
end

View file

@ -0,0 +1,5 @@
module NotesHelper
def truncated_note(note, characters = 50)
sanitize(textilize_without_paragraph(truncate(note.body, characters, "...")))
end
end

View file

@ -0,0 +1,2 @@
module PreferencesHelper
end

View file

@ -0,0 +1,48 @@
module ProjectsHelper
def get_listing_sortable_options(list_container_id)
{
:tag => 'div',
:handle => 'handle',
:complete => visual_effect(:highlight, list_container_id),
:url => order_projects_path
}
end
def set_element_visible(id,test)
if (test)
page.show id
else
page.hide id
end
end
def project_next_prev
html = ''
unless @previous_project.nil?
project_name = truncate(@previous_project.name, 40, "...")
html << link_to_project(@previous_project, "&laquo; #{project_name}")
end
html << ' | ' if @previous_project && @next_project
unless @next_project.nil?
project_name = truncate(@next_project.name, 40, "...")
html << link_to_project(@next_project, "#{project_name} &raquo;")
end
html
end
def project_next_prev_mobile
html = ''
unless @previous_project.nil?
project_name = truncate(@previous_project.name, 40, "...")
html << link_to_project_mobile(@previous_project, "5", "&laquo; 5-#{project_name}")
end
html << ' | ' if @previous_project && @next_project
unless @next_project.nil?
project_name = truncate(@next_project.name, 40, "...")
html << link_to_project_mobile(@next_project, "6", "6-#{project_name} &raquo;")
end
html
end
end

2
app/helpers/stats_helper.rb Executable file
View file

@ -0,0 +1,2 @@
module StatsHelper
end

268
app/helpers/todos_helper.rb Normal file
View file

@ -0,0 +1,268 @@
module TodosHelper
# #require 'users_controller' Counts the number of incomplete items in the
# specified context
#
def count_items(context)
count = Todo.find_all("done=0 AND context_id=#{context.id}").length
end
def form_remote_tag_edit_todo( &block )
form_tag(
todo_path(@todo), {
:method => :put,
:id => dom_id(@todo, 'form'),
:class => dom_id(@todo, 'form') + " inline-form edit_todo_form" },
&block )
apply_behavior 'form.edit_todo_form', make_remote_form(
:method => :put,
:before => "this.down('button.positive').startWaiting()",
:loaded => "this.down('button.positive').stopWaiting()",
:condition => "!(this.down('button.positive').isWaiting())"),
:prevent_default => true
end
def remote_delete_icon
parameters = "_source_view=#{@source_view}"
parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
str = link_to( image_tag_for_delete,
todo_path(@todo), :id => "delete_icon_"+@todo.id.to_s,
:class => "icon delete_icon", :title => "delete the action '#{@todo.description}'")
apply_behavior '.item-container a.delete_icon:click', :prevent_default => true do |page|
page.confirming "'Are you sure that you want to ' + this.title + '?'" do
page << "itemContainer = this.up('.item-container'); itemContainer.startWaiting();"
page << remote_to_href(:method => 'delete', :with => "'#{parameters}'", :complete => "itemContainer.stopWaiting();")
end
end
str
end
def remote_star_icon
str = link_to( image_tag_for_star(@todo),
toggle_star_todo_path(@todo),
:class => "icon star_item", :title => "star the action '#{@todo.description}'")
apply_behavior '.item-container a.star_item:click',
remote_to_href(:method => 'put', :with => "{ _source_view : '#{@source_view}' }"),
:prevent_default => true
str
end
def remote_edit_icon
parameters = "_source_view=#{@source_view}"
parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
if !@todo.completed?
str = link_to( image_tag_for_edit,
edit_todo_path(@todo),
:class => "icon edit_icon")
apply_behavior '.item-container a.edit_icon:click', :prevent_default => true do |page|
page << "Effect.Pulsate(this);"
page << remote_to_href(:method => 'get', :with => "'#{parameters}'")
end
else
str = '<a class="icon">' + image_tag("blank.png") + "</a> "
end
str
end
def remote_toggle_checkbox
str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox')
parameters = "_source_view=#{@source_view}"
parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
apply_behavior '.item-container input.item-checkbox:click',
remote_function(:url => javascript_variable('this.value'), :method => 'put',
:with => "'#{parameters}'")
str
end
def date_span
if @todo.completed?
"<span class=\"grey\">#{format_date( @todo.completed_at )}</span>"
elsif @todo.deferred?
show_date( @todo.show_from )
else
due_date( @todo.due )
end
end
def tag_list_text
@todo.tags.collect{|t| t.name}.join(', ')
end
def tag_list
tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
tag_list = tags_except_starred.collect{|t| "<span class=\"tag #{t.name.gsub(' ','-')}\">" + link_to(t.name, :controller => "todos", :action => "tag", :id => t.name) + "</span>"}.join('')
"<span class='tags'>#{tag_list}</span>"
end
def tag_list_mobile
tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
# removed the link. TODO: add link to mobile view of tagged actions
tag_list = tags_except_starred.collect{|t|
"<span class=\"tag #{t.name.gsub(' ','-')}\">" +
link_to(t.name, {:action => "tag", :controller => "todos", :id => t.name+".m"}) +
# link_to(t.name, formatted_tag_path(t, :m)) +
"</span>"}.join('')
"<span class='tags'>#{tag_list}</span>"
end
def deferred_due_date
if @todo.deferred? && @todo.due
"(action due on #{format_date(@todo.due)})"
end
end
def project_and_context_links(parent_container_type, opts = {})
str = ''
if @todo.completed?
str += @todo.context.name unless opts[:suppress_context]
should_suppress_project = opts[:suppress_project] || @todo.project.nil?
str += ", " unless str.blank? || should_suppress_project
str += @todo.project.name unless should_suppress_project
str = "(#{str})" unless str.blank?
else
if (['project', 'tag', 'stats'].include?(parent_container_type))
str << item_link_to_context( @todo )
end
if (['context', 'tickler', 'tag', 'stats'].include?(parent_container_type)) && @todo.project_id
str << item_link_to_project( @todo )
end
end
return str
end
# Uses the 'staleness_starts' value from settings.yml (in days) to colour the
# background of the action appropriately according to the age of the creation
# date:
# * l1: created more than 1 x staleness_starts, but < 2 x staleness_starts
# * l2: created more than 2 x staleness_starts, but < 3 x staleness_starts
# * l3: created more than 3 x staleness_starts
#
def staleness_class(item)
if item.due || item.completed?
return ""
elsif item.created_at < user_time - (prefs.staleness_starts * 3).days
return " stale_l3"
elsif item.created_at < user_time - (prefs.staleness_starts * 2).days
return " stale_l2"
elsif item.created_at < user_time - (prefs.staleness_starts).days
return " stale_l1"
else
return ""
end
end
# Check show_from date in comparison to today's date Flag up date
# appropriately with a 'traffic light' colour code
#
def show_date(d)
if d == nil
return ""
end
days = days_from_today(d)
case days
# overdue or due very soon! sound the alarm!
when -1000..-1
"<a title=\"" + format_date(d) + "\"><span class=\"red\">Scheduled to show " + (days * -1).to_s + " days ago</span></a> "
when 0
"<a title=\"" + format_date(d) + "\"><span class=\"amber\">Show Today</span></a> "
when 1
"<a title=\"" + format_date(d) + "\"><span class=\"amber\">Show Tomorrow</span></a> "
# due 2-7 days away
when 2..7
if prefs.due_style == Preference.due_styles[:due_on]
"<a title=\"" + format_date(d) + "\"><span class=\"orange\">Show on " + d.strftime("%A") + "</span></a> "
else
"<a title=\"" + format_date(d) + "\"><span class=\"orange\">Show in " + days.to_s + " days</span></a> "
end
# more than a week away - relax
else
"<a title=\"" + format_date(d) + "\"><span class=\"green\">Show in " + days.to_s + " days</span></a> "
end
end
def calendar_setup( input_field )
str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\""
str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]"
str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n"
javascript_tag str
end
def item_container_id
if source_view_is :project
return "p#{@todo.project_id}" if @todo.active?
return "tickler" if @todo.deferred?
end
return "c#{@todo.context_id}"
end
def should_show_new_item
if @todo.project.nil? == false
# do not show new actions that were added to hidden or completed projects
# on home page and context page
return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?)
return false if source_view_is(:context) && (@todo.project.hidden? || @todo.project.completed?)
end
return true if source_view_is(:deferred) && @todo.deferred?
return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden?
return true if source_view_is(:project) && @todo.deferred?
return true if !source_view_is(:deferred) && @todo.active?
return false
end
def parent_container_type
return 'tickler' if source_view_is :deferred
return 'project' if source_view_is :project
return 'stats' if source_view_is :stats
return 'context'
end
def empty_container_msg_div_id
return "tickler-empty-nd" if source_view_is(:project) && @todo.deferred?
return "p#{@todo.project_id}empty-nd" if source_view_is :project
return "c#{@todo.context_id}empty-nd"
end
def project_names_for_autocomplete
array_or_string_for_javascript( ['None'] + @projects.select{ |p| p.active? }.collect{|p| escape_javascript(p.name) } )
end
def context_names_for_autocomplete
# #return array_or_string_for_javascript(['Create a new context']) if
# @contexts.empty?
array_or_string_for_javascript( @contexts.collect{|c| escape_javascript(c.name) } )
end
def format_ical_notes(notes)
split_notes = notes.split(/\n/)
joined_notes = split_notes.join("\\n")
end
def formatted_pagination(total)
s = will_paginate(@todos)
(s.gsub /(<\/[^<]+>)/, '\1 ').chomp(' ')
end
def date_field_tag(name, id, value = nil, options = {})
text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "onfocus" => "Calendar.setup", "autocomplete" => "off"}.update(options.stringify_keys)
end
private
def image_tag_for_delete
image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
end
def image_tag_for_edit
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(@todo, 'edit_icon'))
end
def image_tag_for_star(todo)
class_str = todo.starred? ? "starred_todo" : "unstarred_todo"
image_tag("blank.png", :title =>"Star action", :class => class_str)
end
end

View file

@ -0,0 +1,2 @@
module UsersHelper
end

63
app/models/context.rb Normal file
View file

@ -0,0 +1,63 @@
class Context < ActiveRecord::Base
has_many :todos, :dependent => :delete_all, :include => :project, :order => "todos.completed_at DESC"
belongs_to :user
acts_as_list :scope => :user
extend NamePartFinder
include Tracks::TodoList
attr_protected :user
validates_presence_of :name, :message => "context must have a name"
validates_length_of :name, :maximum => 255, :message => "context name must be less than 256 characters"
validates_uniqueness_of :name, :message => "already exists", :scope => "user_id"
validates_does_not_contain :name, :string => ',', :message => "cannot contain the comma (',') character"
def self.feed_options(user)
{
:title => 'Tracks Contexts',
:description => "Lists all the contexts for #{user.display_name}"
}
end
def self.null_object
NullContext.new
end
def hidden?
self.hide == true || self.hide == 1
end
def title
name
end
def summary(undone_todo_count)
s = "<p>#{undone_todo_count}. "
s += "Context is #{hidden? ? 'Hidden' : 'Active'}."
s += "</p>"
s
end
def new_record_before_save?
@new_record_before_save
end
end
class NullContext
def nil?
true
end
def id
nil
end
def name
''
end
end

7
app/models/note.rb Normal file
View file

@ -0,0 +1,7 @@
class Note < ActiveRecord::Base
belongs_to :user
belongs_to :project
attr_protected :user
end

30
app/models/preference.rb Normal file
View file

@ -0,0 +1,30 @@
class Preference < ActiveRecord::Base
belongs_to :user
composed_of :tz,
:class_name => 'TimeZone',
:mapping => %w(time_zone name)
def self.due_styles
{ :due_in_n_days => 0, :due_on => 1}
end
def self.day_number_to_name_map
{ 0 => "Sunday",
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6 => "Saturday"}
end
def hide_completed_actions?
return show_number_completed == 0
end
def parse_date(s)
return nil if s.blank?
Date.strptime(s, date_format)
end
end

114
app/models/project.rb Normal file
View file

@ -0,0 +1,114 @@
class Project < ActiveRecord::Base
has_many :todos, :dependent => :delete_all, :include => :context
has_many :notes, :dependent => :delete_all, :order => "created_at DESC"
belongs_to :default_context, :dependent => :nullify, :class_name => "Context", :foreign_key => "default_context_id"
belongs_to :user
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"
validates_uniqueness_of :name, :message => "already exists", :scope =>"user_id"
validates_does_not_contain :name, :string => ',', :message => "cannot contain the comma (',') character"
acts_as_list :scope => 'user_id = #{user_id} AND state = \'#{state}\''
acts_as_state_machine :initial => :active, :column => 'state'
extend NamePartFinder
include Tracks::TodoList
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 }
event :activate do
transitions :to => :active, :from => [:hidden, :completed]
end
event :hide do
transitions :to => :hidden, :from => [:active, :completed]
end
event :complete do
transitions :to => :completed, :from => [:active, :hidden]
end
attr_protected :user
attr_accessor :cached_note_count
def self.null_object
NullProject.new
end
def self.feed_options(user)
{
:title => 'Tracks Projects',
:description => "Lists all the projects for #{user.display_name}"
}
end
def hide_todos
todos.each do |t|
unless t.completed? || t.deferred?
t.hide!
t.save
end
end
end
def unhide_todos
todos.each do |t|
if t.project_hidden?
t.unhide!
t.save
end
end
end
def note_count
cached_note_count || notes.count
end
alias_method :original_default_context, :default_context
def default_context
original_default_context.nil? ? Context.null_object : original_default_context
end
# would prefer to call this method state=(), but that causes an endless loop
# as a result of acts_as_state_machine calling state=() to update the attribute
def transition_to(candidate_state)
case candidate_state.to_sym
when current_state
return
when :hidden
hide!
when :active
activate!
when :completed
complete!
end
end
def name=(value)
self[:name] = value.gsub(/\s{2,}/, " ").strip
end
def new_record_before_save?
@new_record_before_save
end
end
class NullProject
def hidden?
false
end
def nil?
true
end
def id
nil
end
end

11
app/models/tag.rb Normal file
View file

@ -0,0 +1,11 @@
class Tag < ActiveRecord::Base
has_many_polymorphs :taggables,
:from => [:todos],
:through => :taggings,
:dependent => :destroy
def on(taggable, user)
tagging = taggings.create :taggable => taggable, :user => user
end
end

11
app/models/tagging.rb Normal file
View file

@ -0,0 +1,11 @@
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
belongs_to :user
# def before_destroy
# # disallow orphaned tags
# # TODO: this doesn't seem to be working
# tag.destroy if tag.taggings.count < 2
# end
end

122
app/models/todo.rb Normal file
View file

@ -0,0 +1,122 @@
class Todo < ActiveRecord::Base
belongs_to :context, :order => 'name'
belongs_to :project
belongs_to :user
STARRED_TAG_NAME = "starred"
acts_as_state_machine :initial => :active, :column => 'state'
# when entering active state, also remove completed_at date.
# Looks like :exit 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 :deferred
event :defer do
transitions :to => :deferred, :from => [:active]
end
event :complete do
transitions :to => :completed, :from => [:active, :project_hidden, :deferred]
end
event :activate do
transitions :to => :active, :from => [:project_hidden, :completed, :deferred]
end
event :hide do
transitions :to => :project_hidden, :from => [:active, :deferred]
end
event :unhide do
transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? }
transitions :to => :active, :from => [:project_hidden]
end
attr_protected :user
# Description field can't be empty, and must be < 100 bytes
# Notes must be < 60,000 bytes (65,000 actually, but I'm being cautious)
validates_presence_of :description
validates_length_of :description, :maximum => 100
validates_length_of :notes, :maximum => 60000, :allow_nil => true
validates_presence_of :show_from, :if => :deferred?
validates_presence_of :context
def validate
if !show_from.blank? && show_from < user.date
errors.add("show_from", "must be a date in the future")
end
end
def toggle_completion!
saved = false
if completed?
saved = activate!
else
saved = complete!
end
return saved
end
def show_from
self[:show_from]
end
def show_from=(date)
activate! if deferred? && date.blank?
defer! if active? && !date.blank? && date > user.date
self[:show_from] = date
end
alias_method :original_project, :project
def project
original_project.nil? ? Project.null_object : original_project
end
alias_method :original_set_initial_state, :set_initial_state
def set_initial_state
if show_from && (show_from > user.date)
write_attribute self.class.state_column, 'deferred'
else
original_set_initial_state
end
end
alias_method :original_run_initial_state_actions, :run_initial_state_actions
def run_initial_state_actions
#only run the initial state actions if the standard initial state hasn't been changed
if self.class.initial_state.to_sym == current_state
original_run_initial_state_actions
end
end
def self.feed_options(user)
{
:title => 'Tracks Actions',
:description => "Actions for #{user.display_name}"
}
end
def starred?
tags.any? {|tag| tag.name == STARRED_TAG_NAME}
end
def toggle_star!
if starred?
delete_tags STARRED_TAG_NAME
tags.reload
else
add_tag STARRED_TAG_NAME
tags.reload
end
starred?
end
end

223
app/models/user.rb Normal file
View file

@ -0,0 +1,223 @@
require 'digest/sha1'
class User < ActiveRecord::Base
# Virtual attribute for the unencrypted password
attr_accessor :password
has_many :contexts,
:order => 'position ASC',
:dependent => :delete_all do
def find_by_params(params)
find(params['id'] || params['context_id']) || nil
end
end
has_many :projects,
:order => 'projects.position ASC',
:dependent => :delete_all do
def find_by_params(params)
find(params['id'] || params['project_id'])
end
def update_positions(project_ids)
project_ids.each_with_index do |id, position|
project = self.detect { |p| p.id == id.to_i }
raise "Project id #{id} not associated with user id #{@user.id}." if project.nil?
project.update_attribute(:position, position + 1)
end
end
def projects_in_state_by_position(state)
self.sort{ |a,b| a.position <=> b.position }.select{ |p| p.state == state }
end
def next_from(project)
self.offset_from(project, 1)
end
def previous_from(project)
self.offset_from(project, -1)
end
def offset_from(project, offset)
projects = self.projects_in_state_by_position(project.state)
position = projects.index(project)
return nil if position == 0 && offset < 0
projects.at( position + offset)
end
def cache_note_counts
project_note_counts = Note.count(:group => 'project_id')
self.each do |project|
project.cached_note_count = project_note_counts[project.id] || 0
end
end
def alphabetize(scope_conditions = {})
projects = find(:all, :conditions => scope_conditions)
projects.sort!{ |x,y| x.name.downcase <=> y.name.downcase }
self.update_positions(projects.map{ |p| p.id })
return projects
end
end
has_many :active_projects,
:class_name => 'Project',
:order => 'projects.position ASC',
:conditions => [ 'state = ?', 'active' ]
has_many :active_contexts,
:class_name => 'Context',
:order => 'position ASC',
:conditions => [ 'hide = ?', 'true' ]
has_many :todos,
:order => 'todos.completed_at DESC, todos.created_at DESC',
:dependent => :delete_all
has_many :deferred_todos,
:class_name => 'Todo',
: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! }
end
end
has_many :completed_todos,
:class_name => 'Todo',
:conditions => ['todos.state = ? and todos.completed_at is not null', 'completed'],
:order => 'todos.completed_at DESC',
:include => [ :project, :context ] do
def completed_within( date )
reject { |x| x.completed_at < date }
end
def completed_more_than( date )
reject { |x| x.completed_at > date }
end
end
has_many :notes, :order => "created_at DESC", :dependent => :delete_all
has_one :preference, :dependent => :destroy
has_many :taggings
has_many :tags, :through => :taggings, :select => "DISTINCT tags.*"
attr_protected :is_admin
validates_presence_of :login
validates_presence_of :password, :if => :password_required?
validates_length_of :password, :within => 5..40, :if => :password_required?
validates_presence_of :password_confirmation, :if => :password_required?
validates_confirmation_of :password
validates_length_of :login, :within => 3..80
validates_uniqueness_of :login, :on => :create
validates_presence_of :open_id_url, :if => :using_openid?
before_create :crypt_password, :generate_token
before_update :crypt_password
before_save :normalize_open_id_url
def validate
unless Tracks::Config.auth_schemes.include?(auth_type)
errors.add("auth_type", "not a valid authentication type (#{auth_type})")
end
end
alias_method :prefs, :preference
def self.authenticate(login, pass)
return nil if login.blank?
candidate = find(:first, :conditions => ["login = ?", login])
return nil if candidate.nil?
return candidate if candidate.auth_type == 'database' && candidate.crypted_password == sha1(pass)
if Tracks::Config.auth_schemes.include?('ldap')
return candidate if candidate.auth_type == 'ldap' && SimpleLdapAuthenticator.valid?(login, pass)
end
return nil
end
def self.find_by_open_id_url(raw_open_id_url)
normalized_open_id_url = normalize_open_id_url(raw_open_id_url)
find(:first, :conditions => ['open_id_url = ?', normalized_open_id_url])
end
def self.no_users_yet?
count == 0
end
def self.find_admin
find(:first, :conditions => [ "is_admin = ?", true ])
end
def to_param
login
end
def display_name
if first_name.blank? && last_name.blank?
return login
elsif first_name.blank?
return last_name
elsif last_name.blank?
return first_name
end
"#{first_name} #{last_name}"
end
def change_password(pass,pass_confirm)
self.password = pass
self.password_confirmation = pass_confirm
save!
end
def time
prefs.tz.adjust(Time.now.utc)
end
def date
time.to_date
end
def generate_token
self.token = Digest::SHA1.hexdigest "#{Time.now.to_i}#{rand}"
end
def remember_token?
remember_token_expires_at && Time.now.utc < remember_token_expires_at
end
# These create and unset the fields required for remembering users between browser closes
def remember_me
self.remember_token_expires_at = 2.weeks.from_now.utc
self.remember_token = self.class.sha1("#{login}--#{remember_token_expires_at}")
save(false)
end
def forget_me
self.remember_token_expires_at = nil
self.remember_token = nil
save(false)
end
protected
def self.sha1(s)
Digest::SHA1.hexdigest("#{Tracks::Config.salt}--#{s}--")
end
def crypt_password
return if password.blank?
write_attribute("crypted_password", self.class.sha1(password)) if password == password_confirmation
end
def password_required?
auth_type == 'database' && crypted_password.blank? || !password.blank?
end
def using_openid?
auth_type == 'open_id'
end
def password_matches?(pass)
crypted_password == sha1(pass)
end
def normalize_open_id_url
return if open_id_url.nil?
self.open_id_url = self.class.normalize_open_id_url(open_id_url)
end
def self.normalize_open_id_url(raw_open_id_url)
normalized = raw_open_id_url
normalized = "http://#{raw_open_id_url}" unless raw_open_id_url =~ /\:\/\//
normalized.downcase.chomp('/')
end
end

View file

@ -0,0 +1,36 @@
<% @not_done = @not_done_todos.select {|t| t.context_id == context.id } %>
<div id="c<%= context.id %>" class="container context" <%= "style=\"display:none\"" if collapsible && @not_done.empty? %>>
<h2>
<% if collapsible -%>
<a href="#" class="container_toggle" id="toggle_c<%= context.id %>"><%= image_tag("collapse.png") %></a>
<% apply_behavior '.container_toggle:click', :prevent_default => true do |page|
page << "containerElem = this.up('.container')
toggleTarget = containerElem.down('.toggle_target')
if (Element.visible(toggleTarget))
{
todoItems.collapseNextActionListing(this, toggleTarget);
todoItems.contextCollapseCookieManager.setCookie(todoItems.buildCookieName(containerElem), true)
}
else
{
todoItems.expandNextActionListing(this, toggleTarget);
todoItems.contextCollapseCookieManager.clearCookie(todoItems.buildCookieName(containerElem))
}
"
end
%>
<% 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'}" } %>
<% else %>
<%= link_to_context( context ) %>
<% end %>
</h2>
<div id="c<%= context.id %>items" class="items toggle_target">
<div id="c<%= context.id %>empty-nd" style="display:<%= @not_done.empty? ? 'block' : 'none'%>;">
<div class="message"><p>Currently there are no incomplete actions in this context</p></div>
</div>
<%= render :partial => "todos/todo", :collection => @not_done, :locals => { :parent_container_type => "context" } %>
</div><!-- [end:items] -->
</div><!-- [end:c<%= context.id %>] -->

View file

@ -0,0 +1,35 @@
<% context = context_form
@context = context-%>
<div id="<%= dom_id(context, 'edit') %>" class="edit-form" style="display:none;">
<% form_tag(context_path(context), {:id => dom_id(context, 'edit_form'), :class => "inline-form "+dom_id(context, 'edit_form')+"-edit-context-form edit-context-form", :method => :put}) do -%>
<%= error_messages_for 'context' %>
<label for="context_name">Context name</label><br/>
<%= text_field('context', 'name', :class => 'context-name') %><br/>
<label for="context_hide">Hide from front page?</label>
<%= check_box('context', 'hide', :class => 'context-hide') %>
<input type="hidden" name="wants_render" value="true" />
<div class="submit_box">
<div class="widgets">
<button type="submit" class="positive" id="<%= dom_id(context, 'submit') %>" tabindex="15">
<%=image_tag("accept.png", :alt => "") %>
Update
</button>
<a href="javascript:void(0);" onclick="Element.toggle('<%= dom_id(context) %>');Element.toggle('<%= dom_id(context, 'edit') %>');" class="negative">
<%=image_tag("cancel.png", :alt => "") %>
Cancel
</a>
</div>
</div>
<br/><br/>
<% end %>
<%= apply_behavior ".edit-context-form", make_remote_form(
:before => "this.up('div.edit-form').down('button.positive').startWaiting()",
:condition => "!(this.up('div.edit-form').down('button.positive')).isWaiting()"),
:external => true
@context = nil %>
</div>

View file

@ -0,0 +1,36 @@
<% context = context_listing %>
<div id="<%= dom_id(context, "container") %>" class="list">
<div id="<%= dom_id(context) %>" class="context sortable_row" style="display:'';">
<div class="position">
<span class="handle">DRAG</span>
</div>
<div class="data">
<%= link_to_context( context ) %> <%= " (" + count_undone_todos_phrase(context,"actions") + ")" %>
</div>
<div class="buttons">
<% if context.hide? %>
<span class="grey">HIDDEN</span>
<% else %>
<span class="grey">VISIBLE</span>
<% end %>
<a class="delete_context_button" href="<%= formatted_context_path(context, :js) %>" title="delete the context '<%= context.name %>'"><%= image_tag( "blank.png", :title => "Delete context", :class=>"delete_item") %></a>
<%= apply_behavior "a.delete_context_button:click", { :prevent_default => true, :external => true} do |page, element|
page.confirming "'Are you sure that you want to ' + this.title + '?'" do
element.up('.context').start_waiting
page << remote_to_href(:method => 'delete')
end
end -%>
<a class="edit_context_button" href="#"><%= image_tag( "blank.png", :title => "Edit context", :class=>"edit_item") %></a>
<%= apply_behavior 'a.edit_context_button:click', :prevent_default => true do |page, element|
element.up('.context').toggle
editform = element.up('.list').down('.edit-form')
editform.toggle
editform.visual_effect(:appear)
editform.down('input').focus
end
-%>
</div>
</div>
<%= render :partial => 'context_form', :object => context %>
</div>

View file

@ -0,0 +1,14 @@
<%
# select actions from this context
@not_done = @not_done_todos.select {|t| t.context_id == mobile_context.id }
if not @not_done.empty?
# only show a context when there are actions in it
%>
<h2><%=mobile_context.name%></h2>
<table cellpadding="0" cellspacing="0" border="0">
<%= render :partial => "todos/mobile_todo",
:collection => @not_done,
:locals => { :parent_container_type => "context" }%>
</table>
<% end -%>

View file

@ -0,0 +1,2 @@
<% context = mobile_context_listing %>
<div id="ctx"><%= link_to context.name, formatted_context_path(context, :m) %><%= " (" + count_undone_todos_phrase(context,"actions") + ")" %></div>

View file

@ -0,0 +1,8 @@
<%
context = text_context
todos_in_context = todos.select { |t| t.context_id == context.id }
if todos_in_context.length > 0
-%>
<%= context.name.upcase %>:
<%= render :partial => "todos/text_todo", :collection => todos_in_context -%>
<% end -%>

View file

@ -0,0 +1,12 @@
if @saved
page.hide 'contexts-empty-nd'
page.insert_html :bottom, "list-contexts", :partial => 'context_listing', :locals => { :context_listing => @context }
page.sortable "list-contexts", get_listing_sortable_options
page.hide 'status'
page['badge_count'].replace_html @down_count
page.call "Form.reset", "context-form"
page.call "Form.focusFirstElement", "context-form"
else
page.show 'status'
page.replace_html 'status', "#{error_messages_for('context')}"
end

View file

@ -0,0 +1,6 @@
page.visual_effect :fade, dom_id(@context, "container"), :duration => 0.5
page.delay(0.5) do
page[dom_id(@context, "container")].remove
end
page['badge_count'].replace_html @down_count
page.notify :notice, "Deleted context '#{@context.name}'", 5.0

View file

@ -0,0 +1 @@
page.notify :error, @error_message || "An error occurred on the server.", 8.0

View file

@ -0,0 +1,54 @@
<div id="display_box">
<div id="list-contexts">
<div id="contexts-empty-nd" style="<%= @no_contexts ? 'display:block' : 'display:none'%>">
<div class="message"><p>Currently there are no contexts</p></div>
</div>
<%= render :partial => 'context_listing', :collection => @contexts %>
</div>
</div>
<div id="input_box">
<div id="context_new_container">
<div id="toggle_context_new" class="hide_form">
<a title="Hide new context form" accesskey="n">&laquo; Hide form</a>
<% apply_behavior '#toggle_context_new a:click', :prevent_default => true do |page|
page << "TracksForm.toggle('toggle_context_new', 'context_new', 'context-form',
'&laquo; Hide form', 'Hide new context form',
'Create a new context &#187;', 'Add a context');"
end
%>
</div>
<div id="context_new" class="context_new" style="display:block">
<% form_remote_tag(
:url => contexts_path,
:method => :post,
:html=> { :id => 'context-form', :name => 'context', :class => 'inline-form'},
:before => "$('context_new_submit').startWaiting()",
:complete => "$('context_new_submit').stopWaiting()",
:condition => "!$('context_new_submit').isWaiting()") do -%>
<div id="status"><%= error_messages_for('context') %></div>
<label for="context_name">Context name</label><br />
<%= text_field( "context", "name" ) %><br />
<label for="context_hide">Hide from front page?</label>
<%= check_box( "context", "hide" ) %><br />
<div class="submit_box">
<div class="widgets">
<button type="submit" class="positive" id="context_new_submit">
<%= image_tag("accept.png", :alt => "") + 'Add Context' %>
</button>
</div>
</div>
<br/><br/>
<% end -%>
</div>
</div>
</div>
<%
sortable_element 'list-contexts', get_listing_sortable_options
-%>

View file

@ -0,0 +1,5 @@
<% @contexts.each do |c| -%>
<%= c.name.upcase %>
<%= count_undone_todos_phrase_text(c)%>. Context is <%= c.hidden? ? "Hidden" : "Active" %>.
<% end -%>

View file

@ -0,0 +1,6 @@
<h2>Visible Contexts</h2>
<%= render :partial => 'mobile_context_listing', :collection => @active_contexts%>
<h2>Hidden Contexts</h2>
<%= render :partial => 'mobile_context_listing', :collection => @hidden_contexts %>

View file

@ -0,0 +1,14 @@
<%
# select actions from this context
@not_done = @not_done_todos.select {|t| t.context_id == @context.id }
if not @not_done.empty?
# only show a context when there are actions in it
%>
<h2><%=@context.name%></h2>
<table cellpadding="0" cellspacing="0" border="0">
<%= render :partial => "todos/mobile_todo",
:collection => @not_done,
:locals => { :parent_container_type => "context" }%>
</table>
<% end -%>

View file

@ -0,0 +1,12 @@
<div id="display_box">
<%= render :partial => "contexts/context", :locals => { :context => @context, :collapsible => false } %>
<% unless @max_completed==0 -%>
<%= render :partial => "todos/completed", :locals => { :done => @done, :suppress_context => true, :collapsible => false, :append_descriptor => "in this context (last #{prefs.show_number_completed})" } %>
<% end -%>
</div><!-- [end:display_box] -->
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -0,0 +1,6 @@
status_message = 'Context saved'
page.notify :notice, status_message, 5.0
page.replace_html dom_id(@context, 'container'), :partial => 'context_listing', :object => @context
page.sortable "list-contexts", get_listing_sortable_options
page.hide "busy"

View file

@ -0,0 +1,37 @@
<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 porting data between Tracks installations</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><!-- End of feeds -->

View file

View file

@ -0,0 +1,19 @@
<div id="display_box">
<div id="feeds">
<div id="feedlegend">
<p>Paste the contents of the YAML file you exported into the text box below:</p>
</div>
<p>
<% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %>
<%= f.text_area :yaml %><br />
<input type="submit" value="Import data">
<% end %>
</p>
</div><!-- End of feeds -->
</div><!-- End of display_box -->
<div id="input_box">
</div><!-- End of input box -->

View file

@ -0,0 +1 @@
<p>Import was successful</p>

View file

@ -0,0 +1,87 @@
<div id="display_box">
<div id="feeds">
<div id="feedlegend">
<h3>Legend:</h3>
<dl>
<dt><%= image_tag("feed-icon.png", :size => "16X16", :border => 0)%></dt><dd>RSS Feed</dd>
<dt><span class="feed">TXT</span></dt><dd>Plain Text Feed</dd>
<dt><span class="feed">iCal</span></dt><dd>iCal feed</dd>
</dl>
<p>Note: All feeds show only actions that have not been marked as done.</p>
</div>
<ul>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :limit => 15 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :limit => 15 }) %>
<%= ical_formatted_link({ :controller => 'todos', :action => 'index', :limit => 15 }) %>
Last 15 actions
</li>
<li>
<%= rss_formatted_link( { :controller => 'todos', :action => 'index' } ) %>
<%= text_formatted_link( { :controller => 'todos', :action => 'index' } ) %>
<%= ical_formatted_link( { :controller => 'todos', :action => 'index' } ) %>
All actions
</li>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :due => 0 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :due => 0 }) %>
<%= ical_formatted_link({ :controller => 'todos', :action => 'index', :due => 0 }) %>
Actions due today or earlier
</li>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :due => 6 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :due => 6 }) %>
<%= ical_formatted_link({ :controller => 'todos', :action => 'index', :due => 6 }) %>
Actions due in 7 days or earlier
</li>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :done => 7 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :done => 7 }) %>
Actions completed in the last 7 days
</li>
<li>
<%= rss_formatted_link({:controller => 'contexts', :action => 'index'}) %>
<%= text_formatted_link({:controller => 'contexts', :action => 'index'}) %>
All Contexts
</li>
<li>
<%= rss_formatted_link({:controller => 'projects', :action => 'index'}) %>
<%= text_formatted_link({:controller => 'projects', :action => 'index'}) %>
All Projects
</li>
<li>
<%= rss_formatted_link({:controller => 'projects', :action => 'index', :only_active_with_no_next_actions => true}) %>
<%= text_formatted_link({:controller => 'projects', :action => 'index', :only_active_with_no_next_actions => true}) %>
Active projects with no next actions
</li>
<li><h4>Feeds for incomplete actions in a specific context:</h4>
<ul>
<% for context in @contexts %>
<li>
<%= rss_formatted_link({ :controller=> 'todos', :action => 'index', :context_id => context.to_param }) %>
<%= text_formatted_link({ :controller=> 'todos', :action => 'index', :context_id => context.to_param }) %>
<%= ical_formatted_link({ :controller=> 'todos', :action => 'index', :context_id => context.to_param }) %>
Next actions in <strong><%=h context.name %></strong>
</li>
<% end %>
</ul>
</li>
<li><h4>Feeds for incomplete actions in a specific project:</h4>
<ul>
<% for project in @projects %>
<li>
<%= rss_formatted_link({ :controller=> 'todos', :action => 'index', :project_id => project.to_param }) %>
<%= text_formatted_link({ :controller=> 'todos', :action => 'index', :project_id => project.to_param }) %>
<%= ical_formatted_link({ :controller=> 'todos', :action => 'index', :project_id => project.to_param }) %>
Next actions for <strong><%=h project.name %></strong>
</li>
<% end %>
</ul>
</li>
</ul>
</div>
</div><!-- End of display_box -->
<div id="input_box">
<%= render "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -0,0 +1,50 @@
<ul>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :limit => 15 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :limit => 15 }) %>
<%= ical_formatted_link({ :controller => 'todos', :action => 'index', :limit => 15 }) %>
Last 15 actions
</li>
<li>
<%= rss_formatted_link( { :controller => 'todos', :action => 'index' } ) %>
<%= text_formatted_link( { :controller => 'todos', :action => 'index' } ) %>
<%= ical_formatted_link( { :controller => 'todos', :action => 'index' } ) %>
All actions
</li>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :due => 0 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :due => 0 }) %>
<%= ical_formatted_link({ :controller => 'todos', :action => 'index', :due => 0 }) %>
Actions due today or earlier
</li>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :due => 6 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :due => 6 }) %>
<%= ical_formatted_link({ :controller => 'todos', :action => 'index', :due => 6 }) %>
Actions due in 7 days or earlier
</li>
<li>
<%= rss_formatted_link({ :controller => 'todos', :action => 'index', :done => 7 }) %>
<%= text_formatted_link({ :controller => 'todos', :action => 'index', :done => 7 }) %>
Actions completed in the last 7 days
</li>
<li>
<%= rss_formatted_link({:controller => 'contexts', :action => 'index'}) %>
<%= text_formatted_link({:controller => 'contexts', :action => 'index'}) %>
All Contexts
</li>
<li>
<%= rss_formatted_link({:controller => 'projects', :action => 'index'}) %>
<%= text_formatted_link({:controller => 'projects', :action => 'index'}) %>
All Projects
</li>
<li>
<%= rss_formatted_link({:controller => 'projects', :action => 'index', :only_active_with_no_next_actions => true}) %>
<%= text_formatted_link({:controller => 'projects', :action => 'index', :only_active_with_no_next_actions => true}) %>
Active projects with no next actions
</li>
<li>
<%= text_formatted_link({:controller => 'projects', :action => 'index', :projects_and_actions => true}) %>
Active projects with their actions
</li>
</ul>

View file

@ -0,0 +1,20 @@
(* Pops up a dialog box in which you enter a description for a next action.
It then creates that next action in Tracks in the context specified below.
*)
set myUsername to "<%= current_user.login %>"
set myToken to "<%= current_user.token %>"
set myContextID to <%= context.id %> (* <%= context.name %> *)
-- Display dialog to enter your description
display dialog "Description of next action:" default answer ""
set myDesc to text returned of the result
-- Now send all that info to Tracks
-- Edit the URL of your Tracks installation if necessary"
tell application "<%= home_url %>backend/api"
set returnValue to call xmlrpc {method name:"NewTodo", parameters:{myUsername, myToken, myContextID, myDesc}}
end tell
-- Show the ID of the newly created next action
display dialog "New next action with id " & returnValue & " created"

View file

@ -0,0 +1,79 @@
(*
Script to grab the sender and subject of the selected
Mail message(s), and create new next action(s) with description
"Email [sender] about [subject]"
If you have Growl, it pops a notification up with the id of
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 %> *)
-- this string is used when the message subject is empty
property emptySubject : "No Subject Specified"
-- Get the selected email in Mail
tell application "Mail"
set theSelection to the selection
if the length of theSelection is less than 1 then
display dialog "One or more messages must be selected." buttons {"OK"} default button 1 with icon caution
else
repeat with theMessage in theSelection
my importMessage(theMessage)
end repeat
end if
end tell
on importMessage(theMessage)
-- Get the info from the email message
tell application "Mail"
try
set theSender to the sender of theMessage
set theSubject to subject of theMessage
if theSubject is equal to "" then set theSubject to emptySubject
-- Construct the description string from the email info
set myDesc to "Email " & theSender & " about " & theSubject
-- Trim the string to 100 characters otherwise it won't validate
if length of myDesc > 100 then
set myDesc to characters 1 thru 100 of myDesc
end if
end try
end tell
-- Now send all that info to Tracks
-- Edit the URL of your Tracks installation if necessary"
tell application "<%= home_url %>backend/api"
set returnValue to call xmlrpc {method name:"NewTodo", parameters:{myUsername, myToken, myContextID, myDesc}}
end tell
(* Growl support - comment out or delete this section if
you don't have Growl *)
tell application "GrowlHelperApp"
set the allNotificationsList to ¬
{"Tracks Notification"}
-- Make a list of the notifications
-- that will be enabled by default.
-- Those not enabled by default can be enabled later
-- in the 'Applications' tab of the growl prefpane.
set the enabledNotificationsList to ¬
{"Tracks Notification"}
-- Register our script with growl.
-- You can optionally (as here) set a default icon
-- for this script's notifications.
register as application ¬
"Tracks Applescript" all notifications allNotificationsList ¬
default notifications enabledNotificationsList ¬
icon of application "Script Editor"
set growlDescription to "Action with ID " & returnValue & " was created."
notify with name "Tracks Notification" title "New action sent to Tracks" description growlDescription application name "Tracks Applescript" icon of application "Script Editor.app"
end tell
(* End of Growl section *)
end importMessage

View file

@ -0,0 +1,15 @@
using terms from application "Quicksilver"
on process text ThisClipping
set myUsername to "<%= current_user.login %>"
set myToken to "<%= current_user.token %>"
set myContextID to <%= context.id %> (* <%= context.name %> *)
tell application "<%= home_url %>backend/api"
set returnValue to call xmlrpc {method name:"NewTodo", parameters:{myUsername, myToken, myContextID, ThisClipping}}
end tell
tell application "Quicksilver"
show notification "Tracks: action added."
end tell
end process text
end using terms from

View file

@ -0,0 +1,105 @@
<% has_contexts = !current_user.contexts.empty? -%>
<h1>Integrations</h1>
<p>Tracks can be integrated with a number of other tools... whatever it takes to help you get things done! This page has information on setting up some of these. Not all of these are applicable to all platforms, and some require more technical knowledge than others. See also <%= link_to "developer documentation for Tracks' REST API", url_for(:action => 'rest_api') %>.</p>
<p>Contents:
<ol>
<li><a href="#applescript1-section">Add an Action with Applescript</a></li>
<li><a href="#applescript2-section">Add an Action with Applescript based on the currently selected Email in Mail.app</a></li>
<li><a href="#quicksilver-applescript-section">Add Actions with Quicksilver and Applescript</a></li>
<li><a href="#email-cron-section">Automatically Email Yourself Upcoming Actions</a></li>
</ol><br />
</p>
<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="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>
<% if has_contexts -%>
<ol>
<li>Choose the context you want to add actions to: <select name="applescript1-contexts" id="applescript1-contexts"><%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %></select>
<%= observe_field "applescript1-contexts", :update => "applescript1",
:with => 'context_id',
:url => { :controller => "integrations", :action => "get_applescript1" },
:before => "$('applescript1').startWaiting()",
:complete => "$('applescript1').stopWaiting()"
%>
</li>
<li>Copy the Applescript below to the clipboard.<br />
<textarea id="applescript1" name="applescript1" rows="15"><%= render :partial => 'applescript1', :locals => { :context => current_user.contexts.first } %></textarea>
</li>
<li>Open Script Editor and paste the script into a new document.</li>
<li>Compile and save the script. Run it as necessary.</li>
</ol>
<% else %>
<br/><p id="no_context_msg"><i>You do not have any context yet. The script will be available after you add your first context</i></p>
<% end %>
<a name="applescript2-section"> </a>
<h2>Add an Action with Applescript based on the currently selected Email in Mail.app</h2>
<p>This script takes the sender and subject of the selected email(s) in Mail and creates a new action for each one, with the description, "Email [sender] about [subject]". The description gets truncated to 100 characters (the validation limit for the field) if it is longer than that. It also has Growl notifications if you have Growl installed.</p>
<% if has_contexts -%>
<ol>
<li>Choose the context you want to add actions to: <select name="applescript2-contexts" id="applescript2-contexts"><%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %></select>
<%= observe_field "applescript2-contexts", :update => "applescript2",
:with => 'context_id',
:url => { :controller => "integrations", :action => "get_applescript2" },
:before => "$('applescript2').startWaiting()",
:complete => "$('applescript2').stopWaiting()"
%>
</li>
<li>Copy the Applescript below to the clipboard.<br />
<textarea id="applescript2" name="applescript2" rows="15"><%= render :partial => 'applescript2', :locals => { :context => current_user.contexts.first } %></textarea>
</li>
<li>Open Script Editor and paste the script into a new document.</li>
<li>Compile and save the script to the ~/Library/Scriipts/Mail Scripts directory.</li>
<li>For more information on using AppleScript with Mail.app, see <a href="http://www.apple.com/applescript/mail/" title="Scriptable Applications: Mail">this overview</a>.
</ol>
<% else %>
<br/><p><i>You do not have any context yet. The script will be available after you add your first context</i></p>
<% end %>
<a name="quicksilver-applescript-section"></a>
<h2>Add Actions with Quicksilver and Applescript</h2>
<p>This integration will allow you to add actions to Tracks via <a href="http://quicksilver.blacktree.com/">Quicksilver</a>.</p>
<% if has_contexts -%>
<ol>
<li>Choose the context you want to add actions to: <select name="quicksilver-contexts" id="quicksilver-contexts"><%= options_from_collection_for_select(current_user.contexts, "id", "name", current_user.contexts.first.id) %></select>
<%= observe_field "quicksilver-contexts", :update => "quicksilver",
:with => 'context_id',
:url => { :controller => "integrations", :action => "get_quicksilver_applescript" },
:before => "$('quicksilver').startWaiting()",
:complete => "$('quicksilver').stopWaiting()"
%>
</li>
<li>Copy the Applescript below to the clipboard.<br />
<textarea id="quicksilver" name="quicksilver" rows="15"><%= render :partial => 'quicksilver_applescript', :locals => { :context => current_user.contexts.first } %></textarea>
</li>
<li>Open Script Editor and paste the script into a new document.</li>
<li>Compile and save the script as "Add to Tracks.scpt" in ~/Library/Application Support/Quicksilver/Actions/ (you may need to create the Actions directory)</li>
<li>Restart Quicksilver</li>
<li>Activate Quicksilver (Ctrl+Space by default)</li>
<li>Press "." to put quicksilver into text mode</li>
<li>Type the description of the next action you want to add</li>
<li>Press tab to switch to the action pane.</li>
<li>By typing or scrolling, choose the "Add to Tracks" action.</li>
</ol>
<% else %>
<br/><p><i>You do not have any context yet. The script will be available after you add your first context</i></p>
<% end %>
<a name="email-cron-section"> </a>
<h2>Automatically Email Yourself Upcoming Actions</h2>
<p>If you enter the following entry to your crontab, you will receive email every day around 5 AM with a list of the upcoming actions which are due within the next 7 days.</p>
<textarea id="cron" name="cron">0 5 * * * /usr/bin/curl -0 "<%= home_url %>todos.txt?due=6&token=<%= current_user.token %>" | /usr/bin/mail -e -s 'Tracks actions due in the next 7 days' youremail@yourdomain.com</textarea>
<p>You can of course use other text <%= link_to 'feeds provided by Tracks', feeds_path %> -- why not email a list of next actions in a particular project to a group of colleagues who are working on the project?</p>

View file

@ -0,0 +1,218 @@
<h1>REST API Documentation for Developers</h1>
<h2>Introduction</h2>
<p>Tracks is designed to be integrated with scripts, web services, and third-party applications. This page serves as the documentation of our REST API.</p>
<h2>Tracks REST API</h2>
<p>The Tracks REST API allows developers to integrate Tracks into their applications. It allows applications to access and modify Tracks data, and is implemented as Vanilla XML over HTTP.</p>
<p>The API is a <a href="http://en.wikipedia.org/wiki/REST">RESTful</a> service. All data is available through the API as a resource to which can be referred using a unique identifier. It responds to a number of the HTTP methods, specifically GET, PUT, POST and UPDATE, and all responses from the API are in a simple XML format encoded as UTF-8.</p>
<h2>Authentication</h2>
<p>Authentication is handled using <a href="http://en.wikipedia.org/wiki/Basic_authentication">Basic HTTP authentication</a>. Your Tracks username and password is used as the authentication credentials for the API. Note that in Basic HTTP authentication, your password is sent in clear text. If you need a more secure authentication solution, you should configure your web server to run Tracks under HTTPS.</p>
<h2>Retrieving data from the API</h2>
<p>To retrieve data you only need to do an HTTP GET on a resource identifier. For example, if you want to get all the contexts with <a href="http://en.wikipedia.org/wiki/CURL">cURL</a>:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd \
<%= home_url %>contexts.xml
&gt;&gt; &lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;contexts&gt;...&lt;/contexts&gt;
</code>
</pre>
<p>Getting a single context:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd \
<%= home_url %>contexts/51.xml
&gt;&gt; &lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;context&gt;...&lt;/context&gt;
</code>
</pre>
<p>Getting the todos within a context:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd \
<%= home_url %>contexts/51/todos.xml
&gt;&gt; &lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;todos type="array"&gt;...&lt;/todos&gt;
</code>
</pre>
<p>You also can apply the pattern shown above with projects instead of contexts.</p>
<p>All data is available according to the following resource paths:</p>
<ul>
<li>/todos.xml</li>
<li>/todos/<code>ID</code>.xml</li>
<li>/contexts.xml</li>
<li>/contexts/<code>ID</code>.xml</li>
<li>/contexts/<code>ID</code>/todos.xml</li>
<li>/projects.xml</li>
<li>/projects/<code>ID</code>.xml</li>
<li>/projects/<code>ID</code>/todos.xml</li>
</ul>
<h2>Writing to the API</h2>
<p>The API provides mechanisms for adding, updating and deleting resources using the HTTP methods <code>PUT</code>, <code>POST</code> and <code>DELETE</code> in combination with the content.</p>
<p>Creating a new project, using curl:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd \
-d "project[name]=Build a treehouse for the kids" \
<%= home_url %>projects.xml -i
&gt;&gt; HTTP/1.1 201 Created
Location: <%= home_url %>projects/65.xml
...
</code>
</pre>
<p>The response is an <code>HTTP/1.1 201 Created</code> with <code>Location</code> header indicating where the new project resource can be found. Now we can add a todo to this project, using curl:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd \
-d "todo[description]=Model treehouse in SketchUp&#38;todo[context_id]=2&#38;todo[project_id]=65" \
<%= home_url %>todos.xml -i
&gt;&gt; HTTP/1.1 201 Created
Location: <%= home_url %>todos/452.xml
...
</code>
</pre>
<p>The response is a again an <code>HTTP/1.1 201 Created</code> with <code>Location</code> header indicating where the new todo resource can be found. Changing the todo notes, again using curl:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd -X PUT \
-d "todo[notes]=use maple texture" \
<%= home_url %>todos/452.xml -i
&gt;&gt; HTTP/1.1 200 OK
...
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;todo&gt;
...
&lt;description&gt;Model treehouse in SketchUp&lt;/description&gt;
&lt;notes&gt;use maple texture&lt;/notes&gt;
...
&lt;/todo&gt;
</code>
</pre>
<p>The response is an <code>HTTP/1.1 200 OK</code> with in the body the XML representation of the updated todo. We provide a shorcut method to toggle a todo done or undone without having to perform the update with the right field values:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd -X PUT \
<%= home_url %>todos/452/toggle_check.xml -i
&gt;&gt; HTTP/1.1 200 OK
...
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;todo&gt;
...
&lt;completed-at type=\"datetime\"&gt;2007-12-05T06:43:25Z&lt;/completed-at&gt;
&lt;state&gt;completed&lt;/state&gt;
...
&lt;/todo&gt;
</code>
</pre>
<p>If we want to delete that todo we can call its unique resource identifier (the URL) with the HTTP method <code>DELETE</code>, again with curl:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd -X DELETE \
<%= home_url %>todos/452.xml -i
&gt;&gt; HTTP/1.1 200 OK
...
</code>
</pre>
<p>The API returns an <code>HTTP/1.1 200 OK</code> and the todo is now deleted.</p>
<h2>Dealing with the response and response status</h2>
<p>All successful operations respond with a status code of <code>200 OK</code> or <code>201 Created</code> depending on the operation. Sometimes a list, say <code>GET /contexts/2/todos.xml</code> will not have any items, it will return an empty list.</p>
<p>The XML for empty list responses look like this, again with curl:</p>
<pre>
<code>
$ curl -u username:p4ssw0rd \
<%= home_url %>contexts/2/todos.xml
&gt;&gt; &lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;nil-classes type="array"/&gt;
</code>
</pre>
<h2>Consuming the API with ActiveResource</h2>
<p><a href="http://weblog.rubyonrails.org/2007/9/30/rails-2-0-0-preview-release">ActiveResource</a> is a thin but powerful wrapper around RESTful services exposed by <a href="http://www.rubyonrails.org">Ruby on Rails</a>. It will be part of Rails 2.0 but until then you can get it with <code>gem install activeresource --source http://gems.rubyonrails.org --include-dependencies</code>.</p>
<pre>
<code>
$ script/console
Loading development environment (Rails 1.2.4)
&gt;&gt; class Context &lt; ActiveResource::Base; end
=&gt; nil
&gt;&gt; Context.site = "<%= home_url %>"
=&gt; "<%= home_url %>"
&gt;&gt; Context.site.user = "username"
=&gt; "username"
&gt;&gt; Context.site.password = CGI.escape "p4ssw0rd"
=&gt; "p4ssw0rd"
&gt;&gt; Context.find :first
=&gt; #&lt;Context:0x262396c @prefix_options={}, @attributes={...}&gt;
&gt;&gt; &gt;&gt; Context.find :all
=&gt; [#&lt;Context:0x274cfc8 @prefix_options={}, @attributes={...}, ...]
</code>
</pre>
<p>Inspired by <a href="http://www.37signals.com">37 Signals</a> &#8217;s Highrise wrapper, we&#8217;ve put together a small ruby wrapper (find it in the doc/ directory) for the API which sets up the ActiveResource models for you to play with in an IRB session:</p>
<pre>
<code>
$ SITE="http://username:p4ssw0rd@<%= request.host_with_port %>" irb \
-r tracks_api_wrapper.rb
irb(main):001:0&gt; inbox = Tracks::Context.find :first
irb(main):002:0&gt; inbox.name
=&gt; "@inbox"
irb(main):003:0&gt;
</code>
</pre>
<h3>Notes about the documentation</h3>
<p>A few conventions have been applied in the documentation, these are:</p>
<ul>
<li><code>ID</code>&#8217;s in a resource <span class="caps">URL</span> indicate that the resource&#8217;s unique ID needs to be inserted there</li>
<li><code>...</code> indicates that unimportant bits of response data have been removed to eliminate noise from the documentation</li>
</ul>
<p>All examples make use of <a href="http://en.wikipedia.org/wiki/CURL">cURL</a> .</p></div>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<%= stylesheet_link_tag "scaffold" %>
<%= javascript_include_tag :defaults %>
<title><%= @page_title -%></title>
</head>
<body>
<%= yield %>
<%= render :partial => "shared/footer" %>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="initial-scale = 1.0" />
<%= stylesheet_link_tag "mobile" %>
<title><%= @page_title %></title>
</head>
<body>
<% if !(@new_mobile || @edit_mobile)
if !@prefs.nil? %>
<h1><span class="count"><%= @down_count %></span> <%=
user_time.strftime(@prefs.title_date_format) %></h1>
<%= (link_to("Add new action", formatted_new_todo_path(:m))+" | ") unless @new_mobile -%>
<%= (link_to("Home", formatted_todos_path(:m))+" | ") unless @home -%>
<%= (link_to("Contexts", formatted_contexts_path(:m))+" | ") %>
<%= (link_to("Projects", formatted_projects_path(:m))+" | ") %>
<%= (link_to("Starred", {:action => "tag", :controller => "todos", :id => "starred.m"})) -%>
<% end
end %>
<%= render_flash -%>
<hr/>
<%= yield %>
<hr/>
<% if !@prefs.nil? %>
<%= link_to "Logout", formatted_logout_path(:format => 'm') %> |
<%= (link_to("Add new action", formatted_new_todo_path(:m))+" | ") unless @new_mobile -%>
<%= (link_to("Home", formatted_todos_path(:m))+" | ") unless @home -%>
<%= (link_to("Contexts", formatted_contexts_path(:m))+" | ") %>
<%= (link_to("Projects", formatted_projects_path(:m))+" | ") %>
<%= (link_to("Starred", {:action => "tag", :controller => "todos", :id => "starred.m"})+" | ") -%>
<%= (link_to("Tickler", {:action => "index", :controller => "tickler.m"})+" | ") -%>
<%= (link_to("Feeds", {:action => "index", :controller => "feeds.m"})) %>
<% end %>
<%= render :partial => "shared/mobile_footer" %>
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Login</title>
<%= stylesheet_link_tag "scaffold" %>
</head>
<body>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1,82 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<% if @prefs.refresh != 0 -%>
<meta http-equiv="Refresh" content="<%= @prefs["refresh"].to_i*60 %>;url=<%= request.request_uri %>">
<% end -%>
<%= javascript_include_merged :tracks %>
<%= javascript_include_tag :unobtrusive %>
<%= stylesheet_link_merged :tracks %>
<%= stylesheet_link_tag "print", :media => "print" %>
<link rel="shortcut icon" href="<%= url_for(:controller => 'favicon.ico') %>" />
<%= auto_discovery_link_tag(:rss, {:controller => "todos", :action => "index", :format => 'rss', :token => "#{current_user.token}"}, {:title => "RSS feed of next actions"}) %>
<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");
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
}
</script>
<title><%= @page_title %></title>
</head>
<body class="<%= @controller.controller_name %>">
<div id="topbar">
<div id="date">
<h1>
<% if @count %>
<span id="badge_count" class="badge"><%= @count %></span>
<% end %>
<%= user_time.strftime(@prefs.title_date_format) %>
</h1>
</div>
<div id="minilinks">
<%= link_to_function("Toggle notes", nil, {:accesskey => "S", :title => "Toggle all notes", :id => "toggle-notes-nav"}) do |page|
page.select('body .todo_notes').each { |e| e.toggle }
end
-%>&nbsp;|&nbsp;
<%= link_to "Logout (#{current_user.display_name}) »", logout_path %>
</div>
<div id="navcontainer">
<ul id="navlist">
<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( "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>
<li><%= navigation_link( "Export", {:controller => "data", :action => "index"}, {:accesskey => "i", :title => "Import and export data"} ) %></li>
<% 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("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>
<li><%= navigation_link(image_tag("stats.gif", :size => "16X16", :border => 0), {:controller => "stats", :action => "index"}, :title => "See your statistics" ) %></li>
</ul>
</div>
<%= render_flash %>
</div>
<div id="content">
<% unless @controller_name == 'feed' or session['noexpiry'] == "on" -%>
<%= periodically_call_remote( :url => {:controller => "login", :action => "check_expiry"},
:frequency => (5*60)) %>
<% end -%>
<%= periodically_call_remote( :url => formatted_check_deferred_todos_path(:js),
:method => :post,
:frequency => (10*60)) %>
<%= yield %>
</div>
<%= render :partial => "shared/footer" %>
</body>
</html>

View file

@ -0,0 +1 @@
page.redirect_to :controller => 'login', :action => 'login'

View file

@ -0,0 +1,3 @@
unless @msg == ""
page.replace_html "info", content_tag("div", @msg + link_to("log in again.", :controller => "login", :action => "login"), "class" => "warning")
end

View file

@ -0,0 +1,78 @@
<% auth_schemes = Tracks::Config.auth_schemes
show_database_form = auth_schemes.include?('database')
show_openid_form = auth_schemes.include?('open_id')
-%>
<div title="Account login" id="loginform" class="form">
<%= render_flash %>
<h3>Please log in to use Tracks:</h3>
<% if show_database_form %>
<div id="database_auth_form" style="display:block">
<% form_tag :action=> 'login' do %>
<table>
<tr>
<td width="100px"><label for="user_login">Login:</label></td>
<td width="100px"><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_password">Password:</label></td>
<td width="100px"><input type="password" name="user_password" id="user_password" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>
<% end %>
</div>
<% end %>
<% if show_openid_form %>
<div id="openid_auth_form" style="display:none">
<% form_tag :action=> 'login', :action => 'begin' do %>
<table>
<tr>
<td width="100px"><label for="openid_url">Identity URL:</label></td>
<td width="100px"><input type="text" name="openid_url" id="openid_url" value="<%= @openid_url %>" class="login_text open_id" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>
<% end %>
</div>
<% end %>
</div>
<% if show_openid_form %><p id="alternate_auth_openid" class="alternate_auth">or, <a href="#" onclick="Login.showOpenid();return false;">login with an OpenId</a></p><% end %>
<% if show_database_form %><p id="alternate_auth_database" class="alternate_auth">or, <a href="#" onclick="Login.showDatabase();return false;">go back to the standard login</a></p><% end %>
<script type="text/javascript">
function showPreferredAuth() {
var preferredAuth = new CookieManager().getCookie('preferred_auth');
var databaseEnabled = <%= show_database_form ? 'true' : 'false' %>;
var openidEnabled = <%= show_openid_form ? 'true' : 'false' %>;
if (preferredAuth && preferredAuth == 'openid' && openidEnabled) {
Login.showOpenid();
}
else if (databaseEnabled) {
Login.showDatabase();
}
else if (openidEnabled) {
Login.showOpenid();
}
}
Event.observe(window, 'load', showPreferredAuth);
</script>

View file

@ -0,0 +1,61 @@
<% auth_schemes = Tracks::Config.auth_schemes
show_database_form = auth_schemes.include?('database')
show_openid_form = auth_schemes.include?('open_id')
-%>
<div title="Account login" id="loginform" class="form">
<%= render_flash %>
<h3>Please log in to use Tracks:</h3>
<% if show_database_form %>
<div id="database_auth_form">
<% form_tag formatted_login_path(:format => 'm') do %>
<table>
<tr>
<td width="100px"><label for="user_login">Login:</label></td>
<td width="100px"><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_password">Password:</label></td>
<td width="100px"><input type="password" name="user_password" id="user_password" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>
<% end %>
</div>
<% end %>
<% if show_openid_form %>
<h4>...or login with an Open ID:</h4>
<div id="openid_auth_form">
<% form_tag formatted_open_id_begin_path(:format => 'm') do %>
<table>
<tr>
<td width="100px"><label for="openid_url">Identity URL:</label></td>
<td width="100px"><input type="text" name="openid_url" id="openid_url" value="<%= @openid_url %>" class="login_text open_id" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>
<% end %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,13 @@
<% note = mobile_notes -%>
<div class="mobile_note">
<%= sanitize(textilize_without_paragraph(note.body)) %>
</div>
<div class="mobile_note_info">
<br/>
<%= link_to("In: " + note.project.name, formatted_project_path(note.project, :m)) %>
Created: <%= format_date(note.created_at) %>
<% if note.updated_at? -%>
&nbsp;|&nbsp;Modified: <%= format_date(note.updated_at) %>
<% end -%>
</div>
<% note = nil -%>

View file

@ -0,0 +1,3 @@
<% note = mobile_notes_summary -%>
<div class="note"><%= link_to( truncated_note(note), formatted_note_path(note, :m)) %></div>
<% note = nil -%>

View file

@ -0,0 +1,6 @@
<% @note = note_edit_form %>
<%= hidden_field( "note", "project_id" ) %>
<%= text_area( "note", "body", "cols" => 70, "rows" => 15, "tabindex" => 1 ) %>
<br /><br />
<input type="submit" value="Update" tabindex="2" />
<% @note = nil %>

View file

@ -0,0 +1,39 @@
<% note = notes -%>
<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)) %>
<div class="note_footer">
<%= link_to_remote(
image_tag("blank.png",
:title =>"Delete this note",
:class=>"delete_item",
:id => "delete_note_"+note.id.to_s),
:update => dom_id(note),
:loading => visual_effect(:fade, dom_id(note, 'container')),
:complete => "Element.remove('#{dom_id(note, 'container')}');",
:url => note_path(note),
:method => :delete,
:confirm => "Are you sure that you want to delete the note \'#{note.id.to_s}\'?" ) + "&nbsp;" -%>
<%= link_to_function(image_tag( "blank.png", :title => "Edit item", :class=>"edit_item"),
"Element.toggle('#{dom_id(note)}'); Element.toggle('#{dom_id(note, 'edit')}'); Effect.Appear('#{dom_id(note, 'edit')}'); Form.focusFirstElement('#{dom_id(note, 'edit_form')}');" ) + " | " %>
<%= link_to("In: " + note.project.name, project_path(note.project), :class=>"footer_link" ) %>&nbsp;|&nbsp;
Created: <%= format_date(note.created_at) %>
<% if note.updated_at? -%>
&nbsp;|&nbsp;Modified: <%= format_date(note.updated_at) %>
<% end -%>
</div>
</div>
<div id="<%= dom_id(note, 'edit') %>" class="edit-form" style="display:none;">
<% form_remote_tag :url => note_path(note),
:method => :put,
:html => { :id => dom_id(note, 'edit_form'), :class => "inline-form" },
:update => dom_id(note, 'container'),
:complete => visual_effect(:appear, dom_id(note, 'container')) do -%>
<%= render :partial => "note_edit_form", :object => note %>
<% end -%>
</div>
</div>
<% note = nil -%>

View file

@ -0,0 +1,6 @@
<% note = notes_summary -%>
<div class="note_wrapper" id="<%= dom_id(note) %>">
<%= link_to( image_tag("blank.png", :border => 0), note_path(note), :title => "Show note", :class => "link_to_notes icon") %>&nbsp;
<%= truncated_note(note) %>
</div>
<% note = nil -%>

View file

@ -0,0 +1,3 @@
page.notify :notice, "Deleted note '#{@note.id}'", 5.0
page['badge_count'].replace_html @count
page.hide "busy"

View file

@ -0,0 +1,11 @@
<div id="display_box_projects">
<% if @all_notes.empty? -%>
<div class="message"><p>Currently there are no notes: add notes to projects from individual project pages.</p></div>
<% else -%>
<% for notes in @all_notes -%>
<div class="container" id="note-<%= notes.id %>-wrapper">
<%= render :partial => 'notes', :object => notes %>
</div>
<% end -%>
<% end -%>
</div>

View file

@ -0,0 +1 @@
<%= render :partial => 'mobile_notes', :object => @note %>

View file

@ -0,0 +1,5 @@
<div id="display_box_projects">
<div class="container" id="note-<%= @note.id %>-wrapper">
<%= render :partial => 'notes', :object => @note %>
</div>
</div>

View file

@ -0,0 +1,76 @@
<div id="display_box" class="container context">
<h2>Help on preferences</h2>
<p>The preference settings should mostly be self-explanatory, but some hints are included below: </p>
<ul>
<li><strong>first name and last name:</strong> Used for display purposes if set</li>
<li><strong>date format:</strong> the format in which you'd like dates to be shown. For example, for the date 31st January 2006, %d/%m/%Y will show 31/01/2006, %b-%e-%y will show Jan-31-06. See the <a href="http://uk2.php.net/strftime" title="PHP strftime manual">strftime manual</a> for more formatting options for the date.</li>
<li><strong>title date format:</strong> same as above, but for the big date at the top of each page.</li>
<li><strong>time zone:</strong> your local time zone</li>
<li><strong>week starts:</strong> day of the week shown as the start of the week on the popup calendar.</li>
<li><strong>due style:</strong> style in which due dates are shown, e.g. "Due in 3 days", "Due on Wednesday"</li>
<li><strong>show completed projects in sidebar:</strong> whether or not projects marked as complete are shown in the sidebar on the home page and elsewhere</li>
<li><strong>show hidden contexts in sidebar:</strong> whether or not contexts marked as hidden are shown in the sidebar on the home page and elsewhere</li>
<li><strong>show project on todo done:</strong> whether or not to redirect to the project page when an action associated with a project is marked complete</li>
<% if current_user.is_admin? %>
<li><strong>admin email:</strong> email address for the admin user of Tracks (displayed on the signup page for users to contact to obtain an account)</li>
<% end %>
<li><strong>staleness starts:</strong> the number of days before items with no due date get marked as stale (with a yellow highlight)</li>
<li><strong>show number completed:</strong> number of completed actions to show on the page. If you set this to zero, the completed actions box will not be shown on the home page or on the individual context or project pages. You can still see all your completed items by clicking the 'Done' link in the navigation bar at the top of each page.</li>
<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>
</ul>
</div>
<div id="input_box" class="container context">
<% form_tag :action => 'update' do %>
<table>
<tr>
<td><label>first name:</label></td>
<td><%= text_field 'user', 'first_name' %></td>
</tr>
<tr>
<td><label>last name:</label></td>
<td><%= text_field 'user', 'last_name' %></td>
</tr>
<%
def table_row(pref_name, nowrap_label = false, &block)
nowrap_attribute = nowrap_label ? ' nowrap="nowrap"' : ''
s = %Q|<tr>\n<td#{nowrap_attribute}><label>#{pref_name.gsub(/_/,' ')}:</label></td>\n<td>\n|
s << yield
s << "\n</td></tr>"
s
end
def row_with_select_field(pref_name, collection = [true,false], nowrap_label = false)
table_row(pref_name, nowrap_label) { select('prefs', pref_name, collection) }
end
def row_with_text_field(pref_name, nowrap_label = false)
table_row(pref_name, nowrap_label) { text_field('prefs', pref_name) }
end
%>
<%= row_with_text_field('date_format') %>
<%= row_with_text_field('title_date_format') %>
<%= table_row('time_zone', false) { time_zone_select('prefs','time_zone') } %>
<%= row_with_select_field("week_starts", Preference.day_number_to_name_map.invert.sort{|a,b| a[1]<=>b[1]})%>
<%= row_with_select_field("due_style", [['Due in ___ days',Preference.due_styles[:due_in_n_days]],['Due on _______',Preference.due_styles[:due_on]]]) %>
<%= row_with_select_field("show_completed_projects_in_sidebar") %>
<%= row_with_select_field("show_hidden_projects_in_sidebar") %>
<%= row_with_select_field("show_hidden_contexts_in_sidebar") %>
<%= row_with_select_field("show_project_on_todo_done") %>
<% if current_user.is_admin? %> <%= row_with_text_field('admin_email') %> <% end %>
<%= row_with_text_field('staleness_starts', true) %>
<%= row_with_text_field('show_number_completed') %>
<%= row_with_text_field('refresh') %>
<%= row_with_select_field("verbose_action_descriptors") %>
<%= row_with_text_field("mobile_todos_per_page") %>
<tr><td><%= submit_tag "Update" %></td>
<td><%= link_to "Cancel", :action => 'index' %></td>
</tr>
</table>
<% end %>
</div>

View file

@ -0,0 +1,65 @@
<div id="single_box" class="container context prefscontainer">
<h2>Your preferences</h2>
<ul id="prefs">
<li>First name: <span class="highlight"><%= current_user.first_name %></span></li>
<li>Last name: <span class="highlight"><%= current_user.last_name %></span></li>
<li>Date format: <span class="highlight"><%= prefs.date_format %></span> Your current date: <%= format_date(user_time) %></li>
<li>Title date format: <span class="highlight"><%= prefs.title_date_format %></span> Your current title date: <%= user_time.strftime(prefs.title_date_format) %></li>
<li>Time zone: <span class="highlight"><%= prefs.tz %></span> Your current time: <%= user_time.strftime('%I:%M %p') %></li>
<li>Week starts on: <span class="highlight"><%= Preference.day_number_to_name_map[prefs.week_starts] %></span></li>
<li>Show the last <span class="highlight"><%= prefs.show_number_completed %></span> completed items</li>
<li>Show completed projects in sidebar: <span class="highlight"><%= prefs.show_completed_projects_in_sidebar %></span></li>
<li>Show hidden projects in sidebar: <span class="highlight"><%= prefs.show_hidden_projects_in_sidebar %></span></li>
<li>Show hidden contexts in sidebar: <span class="highlight"><%= prefs.show_hidden_contexts_in_sidebar %></span></li>
<li>Go to project page on todo complete: <span class="highlight"><%= prefs.show_project_on_todo_done %></span></li>
<li>Staleness starts after <span class="highlight"><%= prefs.staleness_starts %></span> days</li>
<li>Due style: <span class="highlight">
<% if prefs.due_style == Preference.due_styles[:due_in_n_days] %>
Due in ___ days
<% else %>
Due on ________
<% end %>
</span></li>
<% if current_user.is_admin? %>
<li>Admin email: <span class="highlight"><%= prefs.admin_email %></span></li>
<% end %>
<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>
</ul>
<div class="actions">
<%= link_to "Edit preferences &raquo;", { :controller => 'preferences', :action => 'edit'}, :class => 'edit_link' %>
</div>
<h2>Your token</h2>
<div id="token_area">
<div class="description">Token (for feeds and API use):</div>
<div id="token><span class="highlight"><%= current_user.token %></span></div>
<div class="token_regenerate">
<%= button_to "Generate a new token", refresh_token_user_path(current_user),
:confirm => "Are you sure? Generating a new token will replace the existing one and break any external usages of this token." %>
</div>
</div>
<h2>Your authentication</h2>
<div id="authentication_area">
<% if Tracks::Config.auth_schemes.length > 1 %>
<p>Your authentication type is <span class="highlight"><%= current_user.auth_type %></span>.
<div class="actions">
<%= link_to "Change your authentication type &raquo;", change_auth_type_user_path(current_user), :class => 'edit_link' %>
</div>
<% end %>
<% if current_user.auth_type == 'database' %>
<div class="actions">
<%= link_to 'Change your password &raquo;', change_password_user_path(current_user) %>
</div>
<% end %>
<% if current_user.auth_type == 'open_id' %>
<p>Your Open ID URL is <span class="highlight"><%= current_user.open_id_url %></span>.
<div class="actions">
<%= link_to 'Change Your Identity URL &raquo;', change_auth_type_user_path(current_user) %></p>
</div>
<% end %>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="page_name_auto_complete" id="default_context_list" style="display:none;z-index:9999"></div>
<script type="text/javascript">
defaultContextAutoCompleter = new Autocompleter.Local('project[default_context_name]', 'default_context_list', <%= context_names_for_autocomplete %>, {choices:100,autoSelect:false});
Event.observe($('project[default_context_name]'), "focus", defaultContextAutoCompleter.activate.bind(defaultContextAutoCompleter));
Event.observe($('project[default_context_name]'), "click", defaultContextAutoCompleter.activate.bind(defaultContextAutoCompleter));
</script>

View file

@ -0,0 +1,5 @@
<%= hidden_field( "project", "id" ) %>
<label for="project_name">Project name</label><br />
<%= text_field( "project", "name" ) %>
<br />
<br />

View file

@ -0,0 +1,2 @@
<% project = mobile_project_listing %>
<div id="pjr"><%= link_to project.name, formatted_project_path(project) %><%= " (" + count_undone_todos_and_notes_phrase(project,"actions") + ")" %></div>

View file

@ -0,0 +1,26 @@
<% @not_done = project.not_done_todos -%>
<div id="p<%= project.id %>" class="container project">
<h2>
<% if collapsible %>
<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'}" } %>
</h2>
<% unless @project.description.blank? -%>
<div class="project_description"><%= sanitize(@project.description) %></div>
<% end -%>
<% if @project.completed? -%>
<p class="project_completed">Project has been marked as completed</p>
<% elsif @project.completed? -%>
<p class="project_completed">Project has been marked as hidden</p>
<% end -%>
<div id="p<%= project.id %>items" class="items toggle_target">
<div id="p<%= project.id %>empty-nd" style="display:<%= @not_done.empty? ? 'block' : 'none'%>;">
<div class="message"><p>Currently there are no incomplete actions in this project</p></div>
</div>
<%= render :partial => "todos/todo", :collection => @not_done, :locals => { :parent_container_type => "project" } %>
</div><!-- [end:items] -->
</div><!-- [end:p<%= project.id %>] -->

View file

@ -0,0 +1,44 @@
<%
project = project_form
%>
<% form_tag project_path(project), { :id => dom_id(project, 'edit_form'), :class => "inline-form "+dom_id(project, 'edit_form')+"-edit-project-form", :method => :put } do -%>
<label for="project_name">Name:</label><br/>
<%= text_field :project, 'name', :class => 'project-name' %><br/>
<label for="project_description">Description (optional):</label><br/>
<%= text_area :project, 'description', "cols" => 30, "rows" => 4, :class => 'project-description' %><br/>
<label for="project_done">Project status:</label><br/>
<% ['active', 'hidden', 'completed'].each do | state | %>
<%= radio_button(:project, 'state', state) %> <%= state.titlecase %>
<% end %><br/>
<label for="project[default_context_name]">Default Context</label><br/>
<%= text_field_tag("project[default_context_name]", project.default_context.name, {:tabindex=>1,:size=> 25}) %>
<%= render :partial => 'default_context_autocomplete' %>
<br/>
<input type="hidden" name="wants_render" value="true" />
<div class="submit_box">
<div class="widgets" id="<%= dom_id(project, 'widgets') %>">
<button type="submit" class="positive" id="<%= dom_id(project, 'submit') %>" tabindex="15">
<%=image_tag("accept.png", :alt => "") %>
Update
</button>
<a href="javascript:void(0);" id="<%= dom_id(project, 'cancel') %>" onclick="Element.toggle('<%= dom_id(project) %>');Element.toggle('<%= dom_id(project, 'edit') %>');" class="negative">
<%=image_tag("cancel.png", :alt => "") %>
Cancel
</a>
</div>
</div>
<br/><br/>
<% end -%>
<%= apply_behavior "."+dom_id(project, 'edit_form')+"-edit-project-form", make_remote_form(
:before => "$('"+dom_id(project, 'submit')+"').startWaiting();",
:condition => "!$('"+dom_id(project, 'submit')+"').isWaiting()",
:external => false) %>

View file

@ -0,0 +1,41 @@
<% project = project_listing
@project_listing_zindex = @project_listing_zindex.nil? ? 200 : @project_listing_zindex - 1
-%>
<div id="<%= dom_id(project, "container") %>" class="list" style="z-index:<%= @project_listing_zindex %>">
<div id="<%= dom_id(project) %>" class="project sortable_row" style="display:''">
<div class="position">
<span class="handle">DRAG</span>
</div>
<div class="data">
<%= link_to_project( project ) %><%= " (" + count_undone_todos_and_notes_phrase(project,"actions") + ")" %>
</div>
<div class="buttons">
<span class="grey"><%= project.current_state.to_s.upcase %></span>
<a class="delete_project_button"
href="<%= formatted_project_path(project, :js) %>"
title="delete the project '<%= project.name %>'"><%= image_tag( "blank.png",
:title => "Delete project",
:class=>"delete_item") %></a>
<%= apply_behavior "a.delete_project_button:click", { :prevent_default => true, :external => true } do |page, element|
page.confirming "'Are you sure that you want to ' + this.title + '?'" do
element.up('.project').start_waiting
page << remote_to_href(:method => 'delete')
end
end -%>
<a class="edit_project_button" id="<%= dom_id(project, 'editbutton') %>" href="<%= formatted_edit_project_path(project, :js) %>" title="delete the project '<%= project.name %>'"><%= image_tag( "blank.png", :title => "Edit project", :class=>"edit_item") %></a>
<%= apply_behavior 'a.edit_project_button:click', { :prevent_default => true, :external => true } do |page, element|
element.up('div.project').start_waiting
page << remote_to_href(:method => 'get')
end
-%>
</div>
</div>
<div id="<%= dom_id(project, 'edit') %>" class="edit-form" style="display:none;">
</div>
</div>
<% if controller.action_name == 'create' %>
<script>
new Effect.Appear('<%= dom_id(project) %>');
</script>
<% end %>

View file

@ -0,0 +1,19 @@
<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="alpha_sort">
<%= link_to("Sort 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
page << "alphaSort = this.up('.alpha_sort');
alphaSort.startWaiting();"
page << remote_to_href(:complete => "alphaSort.stopWaiting()")
end
end
%></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>

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

@ -0,0 +1,17 @@
if @saved and @go_to_project
page.redirect_to project_path(@project)
elsif @saved
page.hide 'status'
page['badge_count'].replace_html @down_count
page.hide 'projects-empty-nd'
page.show 'list-active-projects-container'
page.replace_html "active-projects-count", @active_projects_count
page.insert_html :bottom, "list-active-projects", :partial => 'project_listing', :locals => { :project_listing => @project }
page.sortable "list-active-projects", get_listing_sortable_options('list-active-projects')
page.call "Form.reset", "project-form"
page.call "Form.focusFirstElement", "project-form"
else
page.show 'status'
page.replace_html 'status', "#{error_messages_for('project')}"
end
page.hide "busy"

View file

@ -0,0 +1,13 @@
page.visual_effect :fade, dom_id(@project, "container"), :duration => 0.5
page.delay(0.5) do
page[dom_id(@project, "container")].remove
page.replace_html "active-projects-count", @active_projects_count
page.replace_html "hidden-projects-count", @hidden_projects_count
page.replace_html "completed-projects-count", @completed_projects_count
page.set_element_visible("list-hidden-projects-container", @hidden_projects_count > 0)
page.set_element_visible("list-active-projects-container", @active_projects_count > 0)
page.set_element_visible("list-completed-projects-container", @completed_projects_count > 0)
end
page.notify :notice, "Deleted project '#{@project.name}'", 5.0
page['badge_count'].replace_html @down_count
page.hide "busy"

View file

@ -0,0 +1,5 @@
page[dom_id(@project, 'edit')].replace_html :partial => 'project_form', :locals => { :project_form => @project }
page[@project].hide
page[@project].stop_waiting
page[dom_id(@project, 'edit')].show
page[dom_id(@project, 'edit_form')].down('input.project-name').focus

View file

@ -0,0 +1 @@
page.notify :error, @error_message || "An error occurred on the server.", 8.0

View file

@ -0,0 +1,64 @@
<div id="display_box">
<div id="projects-empty-nd" style="<%= @no_projects ? 'display:block' : 'display:none'%>">
<div class="message"><p>Currently there are no projects</p></div>
</div>
<%= render :partial => 'project_state_group', :object => @active_projects, :locals => { :state => 'active'} %>
<%= render :partial => 'project_state_group', :object => @hidden_projects, :locals => { :state => 'hidden'} %>
<%= render :partial => 'project_state_group', :object => @completed_projects, :locals => { :state => 'completed'} %>
</div>
<% @project = @new_project -%>
<div id="input_box">
<div id="project_new_project_filler">
<div id="project_new_project_container">
<div id="toggle_project_new" class="hide_form">
<a title="Hide new project form" accesskey="n">&laquo; Hide form</a>
<% apply_behavior '#toggle_project_new a:click', :prevent_default => true do |page|
page << "TracksForm.toggle('toggle_project_new', 'project_new', 'project-form',
'&laquo; Hide form', 'Hide new project form',
'Create a new project &#187;', 'Add a project');"
end
%>
</div>
<div id="project_new" class="project_new" style="display:block">
<% form_remote_tag(:url => projects_path, :method => :post,
:html=> { :id=>'project-form', :name=>'project', :class => 'inline-form'},
:before => "$('project_new_project_submit').startWaiting()",
:complete => "$('project_new_project_submit').stopWaiting()",
:condition => "!$('project_new_project_submit').isWaiting()") do -%>
<div id="status"><%= error_messages_for('project') %></div>
<label for="project_name">Name:</label><br />
<%= text_field 'project', 'name', "tabindex" => 1 %><br />
<label for="project_description">Description (optional):</label><br />
<%= text_area 'project', 'description', "cols" => 30, "rows" => 4, "tabindex" => 2 %><br />
<% unless @contexts.empty? -%>
<label for="default_context_name">Default Context (optional):</label><br />
<%= text_field_tag("project[default_context_name]", @project.default_context.name, :tabindex => 3) %>
<%= render :partial => 'default_context_autocomplete' %>
<br />
<% end -%>
<br/>
<div class="submit_box">
<div class="widgets">
<button type="submit" class="positive" id="project_new_project_submit">
<%= image_tag("accept.png", :alt => "") + 'Add Project' %>
</button>
</div>
</div>
<br/><br/>
<input id="go_to_project" type="checkbox" tabindex="5" name="go_to_project"/><label for="go_to_project"> Take me to the new project page</label><br />
<% end -%>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<% @projects.each do |p| -%>
<%= p.name.upcase %>
<%= p.description + "\n" unless p.description.blank? -%>
<%= count_undone_todos_phrase_text(p)%>. Project is <%= p.state %>.
<% end -%>

View file

@ -0,0 +1,8 @@
<h2>Active projects</h2>
<%= render :partial => 'mobile_project_listing', :collection => @active_projects%>
<h2>Hidden projects</h2>
<%= render :partial => 'mobile_project_listing', :collection => @hidden_projects %>
<h2>Completed projects</h2>
<%= render :partial => 'mobile_project_listing', :collection => @completed_projects %>

View file

@ -0,0 +1,6 @@
<% @projects.each do |p| -%>
<%= p.name.upcase -%>
<% actions = p.todos.select { |t| t.active? } -%>
<%= render :partial => "todos/text_todo", :collection => actions -%>
<% end -%>

View file

@ -0,0 +1,27 @@
<% project = @project %>
<%= project_next_prev_mobile %>
<h2><%=project.name%></h2>
<% unless @project.description.blank? -%>
<div class="project_description"><%= sanitize(@project.description) %></div>
<% end -%>
<%= render :partial => "todos/mobile_todo", :collection => @not_done, :locals => { :parent_container_type => "project" }%>
<h2>Deferred actions for this project</h2>
<% if @deferred.empty? -%>
There are no deferred actions for this project
<% else -%>
<%= render :partial => "todos/mobile_todo", :collection => @deferred, :locals => { :parent_container_type => "project" }%>
<% end
-%>
<h2>Completed actions for this project</h2>
<% if @done.empty? -%>
There are no completed actions for this project
<% else -%>
<%= render :partial => "todos/mobile_todo", :collection => @done, :locals => { :parent_container_type => "project" }%>
<% end %>
<h2>Notes</h2>
<% if @project.notes.empty? -%>
There are no notes for this project
<% else -%><%= render :partial => "notes/mobile_notes_summary", :collection => @project.notes %>
<% end -%>
<h2>Settings</h2>
This project is <%= project.current_state.to_s %>. <%= @project_default_context %>

View file

@ -0,0 +1,78 @@
<div id="display_box">
<div id="project-next-prev">
<%= project_next_prev %>
</div>
<%= render :partial => "projects/project", :locals => { :project => @project, :collapsible => false } %>
<%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => false, :append_descriptor => "in this project" } %>
<% unless @max_completed==0 -%>
<%= render :partial => "todos/completed", :locals => { :done => @done, :collapsible => false, :suppress_project => true, :append_descriptor => "in this project" } %>
<% end -%>
<div class="container">
<div id="notes">
<div class="add_note_link"><%= link_to_function( "Add a note", "Element.toggle('new-note'); Form.focusFirstElement('form-new-note');", :id=>"add_note_href") %></div>
<h2>Notes</h2>
<div id="empty-n" style="display:<%= @project.notes.empty? ? 'block' : 'none'%>;">
<%= render :partial => "shared/empty",
:locals => { :message => "Currently there are no notes attached to this project"} %>
</div>
<%= render :partial => "notes/notes_summary", :collection => @project.notes %>
</div>
</div>
<div id="new-note" style="display:none;">
<% form_remote_tag :url => notes_path,
:method => :post,
:update => "notes",
:position => "bottom",
:complete => "new Effect.Highlight('notes');$('empty-n').hide();Form.reset('form-new-note');",
:html => {:id=>'form-new-note', :class => 'inline-form'} do %>
<%= hidden_field( "new_note", "project_id", "value" => "#{@project.id}" ) %>
<%= text_area( "new_note", "body", "cols" => 50, "rows" => 3, "tabindex" => 1 ) %>
<br /><br />
<input type="submit" value="Add note" name="add-new-note" tabindex="2" />
<% end -%>
</div>
<div class="container">
<div id="project_status">
<h2>Status</h2>
<div>
<% ['active', 'hidden', 'completed'].each do | state | %>
<% span_class = @project.current_state.to_s == state ? 'active_state' : 'inactive_state' %>
<span class="<%= state %>"><%= radio_button(:project, 'state', state) %> <span class="<%= span_class %>"><%= state.titlecase %></span></span>
<% end %>
<% apply_behavior "#project_status input:click",
remote_function(:url => project_path(@project), :method => :put,
:with => "'wants_render=false&update_status=true&project[state]='+this.value" )
%>
</div>
</div>
</div>
<div class="container">
<div id="default_context">
<h2>Default Context</h2>
<div>
<% form_remote_tag( :url => project_path(@project), :method => :put,
:html=> { :id => 'set-default-context-action',
:name => 'default_context',
:class => 'inline-form' }) do -%>
<%= hidden_field_tag("update_default_context", true) %>
<%= text_field_tag("project[default_context_name]",
@project.default_context.name,
{ :tabindex => 9,:size => 25 }) %>
<%= submit_tag "Set Default Context for this Project", { :tabindex => 10 } %>
<%= render :partial => 'default_context_autocomplete' %>
<% end -%>
</div>
</div>
</div>
</div><!-- [end:display_box] -->
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -0,0 +1,16 @@
status_message = 'Project saved'
page.notify :notice, status_message, 5.0
if @state_changed
page[dom_id(@project, 'container')].remove
page.insert_html :bottom, "list-#{@project.state}-projects", :partial => 'project_listing', :object => @project
else
page.replace_html dom_id(@project, 'container'), :partial => 'project_listing', :object => @project
end
page.sortable "list-#{@project.state}-projects", get_listing_sortable_options("list-#{@project.state}-projects")
page.replace_html "active-projects-count", @active_projects_count
page.replace_html "hidden-projects-count", @hidden_projects_count
page.replace_html "completed-projects-count", @completed_projects_count
page.set_element_visible("list-hidden-projects-container", @hidden_projects_count > 0)
page.set_element_visible("list-active-projects-container", @active_projects_count > 0)
page.set_element_visible("list-completed-projects-container", @completed_projects_count > 0)

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