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

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