mirror of
https://github.com/TracksApp/tracks.git
synced 2026-01-21 00:16:09 +01:00
Removed superfluous 'tracks' directory at the root of the repository.
Testing commits to github.
This commit is contained in:
parent
6a42901514
commit
4cbf5a34d3
2269 changed files with 0 additions and 0 deletions
216
app/controllers/application.rb
Normal file
216
app/controllers/application.rb
Normal 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 + " " + 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
|
||||
108
app/controllers/backend_controller.rb
Normal file
108
app/controllers/backend_controller.rb
Normal 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
|
||||
204
app/controllers/contexts_controller.rb
Normal file
204
app/controllers/contexts_controller.rb
Normal 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
|
||||
95
app/controllers/data_controller.rb
Normal file
95
app/controllers/data_controller.rb
Normal 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
|
||||
18
app/controllers/feedlist_controller.rb
Normal file
18
app/controllers/feedlist_controller.rb
Normal 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
|
||||
25
app/controllers/integrations_controller.rb
Normal file
25
app/controllers/integrations_controller.rb
Normal 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
|
||||
160
app/controllers/login_controller.rb
Normal file
160
app/controllers/login_controller.rb
Normal 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
|
||||
71
app/controllers/notes_controller.rb
Normal file
71
app/controllers/notes_controller.rb
Normal 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
|
||||
23
app/controllers/preferences_controller.rb
Normal file
23
app/controllers/preferences_controller.rb
Normal 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
|
||||
282
app/controllers/projects_controller.rb
Normal file
282
app/controllers/projects_controller.rb
Normal 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
|
||||
780
app/controllers/stats_controller.rb
Executable file
780
app/controllers/stats_controller.rb
Executable 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
|
||||
708
app/controllers/todos_controller.rb
Normal file
708
app/controllers/todos_controller.rb
Normal 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
|
||||
251
app/controllers/users_controller.rb
Normal file
251
app/controllers/users_controller.rb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue