Merge branch 'master' into clinton/master

* master: (40 commits)
  Updated the manual in HTML, PDF and .tex format.
  Minor updates to the example db.
  set version to 1.7RC
  update version in template
  update changelog
  OpenSearch support.
  update the sample sqlite databases and update sample content sql. Solves #802
  forgot ot change one version to 1.7
  update documentation for 1.7rc release
  No point in changing the name of the OpenID identity column in users table. Use the existing one.
  show recurrence pattern in :title of a recurring todo. Needed slight refactoring to make it happen
  Re-write OpenID code to use new authentication plugin. Tested to work!
  Upgraded to open_id_authentication plugin at 00d8bc7f97 and unpacked ruby-openid gem version 2.1.2.
  remove old compressed js and css from the asset_packager plugin that we don't use anymore
  applied the patches from Jakub to improve the mobile html
  adds test for changing context on a todo in tag view. Resolves #762
  Add testcase where changing the name of a project should be reflected in the default project name. resolves #756
  add testcase for deleting a user. Resolves #734
  implement suggestions for search in #787
  fix failing tests
  ...
This commit is contained in:
bsag 2008-12-14 15:08:28 +00:00
commit 4fb4a98bc8
1905 changed files with 119478 additions and 58883 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ nbproject
vendor/plugins/query_trace/
db/schema.rb
.dotest
public/javascripts/cache
public/stylesheets/cache

5
README
View file

@ -8,12 +8,10 @@
* Mailing list: http://lists.rousette.org.uk/mailman/listinfo/tracks-discuss
* Original developer: bsag (http://www.rousette.org.uk/)
* Contributors: http://dev.rousette.org.uk/wiki/Tracks/Contributing/Contributors
* Version: 1.6
* Version: 1.7RC
* Copyright: (cc) 2004-2008 rousette.org.uk.
* License: GNU GPL
**An important note for version 1.6: OpenID support is broken in this release. The fix isn't trivial because of changes to the `ruby-openid` gem, so we wanted to get this version out now and fix OpenID for the next release. If you depend on OpenID integration, we recommend waiting until the next release.**
All the documentation for Tracks can be found within the /doc directory. It contains a manual in HTML (manual.html) or PDF format (manual.pdf), and this includes full instructions for both new installations and upgrades from older installations of Tracks. The instructions might appear long and intimidatingly complex, but that is mostly because of the number of different platforms supported, and the different configurations which can be used (e.g. running Tracks on your local computer or on a remote server). If you choose the appropriate section for your situation (installation vs. upgrade), and use the easiest (recommended) method, you should find the instructions easy to follow. If you encounter problems, try searching the wiki, forum or mailing list (URLs above), and ask a question if you cannot find a solution to your problem.
For those upgrading, change notes are available in /doc/CHANGELOG. If you are thinking about contributing towards the development of Tracks, please read /doc/README_DEVELOPERS for general information, or /doc/tracks_api_wrapper.rb for information on Tracks' API.
@ -21,3 +19,4 @@ For those upgrading, change notes are available in /doc/CHANGELOG. If you are th
While fully usable for everyday use, Tracks is still a work in progress. Make sure that you take sensible precautions and back up all your data frequently, taking particular care when you are upgrading.
Enjoy being productive!

View file

@ -32,13 +32,13 @@ class ApplicationController < ActionController::Base
before_filter :set_time_zone
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
extend ActionView::Helpers::SanitizeHelper::ClassMethods
helper_method :format_date, :markdown
# By default, sets the charset to UTF-8 if it isn't already set
@ -148,21 +148,15 @@ class ApplicationController < ActionController::Base
# 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
return params[:format] == 'm'
end
def enable_mobile_content_negotiation
if mobile?
request.accepts.unshift(Mime::Type::lookup(MOBILE_CONTENT_TYPE))
request.format = :m
end
end
def restore_content_type_for_mobile
if mobile?
response.content_type = 'text/html'
end
end
def create_todo_from_recurring_todo(rt, date=nil)
# create todo and initialize with data from recurring_todo rt
todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id})
@ -234,8 +228,13 @@ class ApplicationController < ActionController::Base
end
def init_data_for_sidebar
@projects = @projects || current_user.projects.find(:all, :include => [:default_context ])
@contexts = @contexts || current_user.contexts
@completed_projects = current_user.projects.completed
@hidden_projects = current_user.projects.hidden
@active_projects = current_user.projects.active
@active_contexts = current_user.contexts.active
@hidden_contexts = current_user.contexts.hidden
init_not_done_counts
if prefs.show_hidden_projects_in_sidebar
init_project_hidden_todo_counts(['project'])
@ -244,13 +243,13 @@ class ApplicationController < ActionController::Base
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)")
eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || current_user.todos.active.count(: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)")
eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || current_user.todos.count(:conditions => ['state = ? or state = ?', 'project_hidden', 'active'], :group => :#{parent}_id)")
end
end

View file

@ -134,8 +134,8 @@ class ContextsController < ApplicationController
def render_contexts_mobile
lambda do
@page_title = "TRACKS::List Contexts"
@active_contexts = @contexts.find(:all, { :conditions => ["hide = ?", false]})
@hidden_contexts = @contexts.find(:all, { :conditions => ["hide = ?", true]})
@active_contexts = @contexts.active
@hidden_contexts = @contexts.hidden
@down_count = @active_contexts.size + @hidden_contexts.size
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
render :action => 'index_mobile'
@ -201,6 +201,9 @@ class ContextsController < ApplicationController
: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])
@projects = current_user.projects
@count = @not_done_todos.size
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end

View file

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

View file

@ -1,5 +1,7 @@
class IntegrationsController < ApplicationController
skip_before_filter :login_required, :only => :search_plugin
def index
@page_title = 'TRACKS::Integrations'
end
@ -22,4 +24,12 @@ class IntegrationsController < ApplicationController
context = current_user.contexts.find params[:context_id]
render :partial => 'applescript2', :locals => { :context => context }
end
def search_plugin
@icon_data = [File.open(RAILS_ROOT + '/public/images/done.png').read].
pack('m').gsub(/\n/, '')
render :layout => false
end
end

View file

@ -6,114 +6,45 @@ class LoginController < ApplicationController
skip_before_filter :login_required
before_filter :login_optional
before_filter :get_current_user
open_id_consumer if openid_enabled?
def login
@page_title = "TRACKS::Login"
@openid_url = cookies[:openid_url] if 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, :secure => TRACKS_COOKIES_SECURE }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
if openid_enabled? && using_open_id?
login_openid
else
@page_title = "TRACKS::Login"
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, :secure => TRACKS_COOKIES_SECURE }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
end
redirect_back_or_home
return
else
@login = params['user_login']
notify :warning, "Login unsuccessful"
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' }
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
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, :secure => TRACKS_COOKIES_SECURE }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
end
cookies[:openid_url] = { :value => openid_url, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE }
redirect_back_or_home
else
notify :warning, "You have successfully verified #{openid_url} as your identity, but you do not have a Tracks account. Please ask your administrator to sign you up."
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
@ -156,5 +87,31 @@ class LoginController < ApplicationController
def should_expire_sessions?
session['noexpiry'] != "on"
end
protected
def login_openid
# 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']
authenticate_with_open_id do |result, identity_url|
if result.successful?
if @user = User.find_by_open_id_url(identity_url)
session['user_id'] = @user.id
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, :secure => TRACKS_COOKIES_SECURE }
unless should_expire_sessions?
@user.remember_me
cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE }
end
redirect_back_or_home
else
notify :warning, "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
notify :warning, result.message
end
end
end
end

View file

@ -31,7 +31,7 @@ class ProjectsController < ApplicationController
end
def projects_and_actions
@projects = @projects.select { |p| p.active? }
@projects = @projects.active
respond_to do |format|
format.text {
render :action => 'index_text_projects_and_actions', :layout => false, :content_type => Mime::TEXT
@ -83,7 +83,7 @@ class ProjectsController < ApplicationController
@go_to_project = params['go_to_project']
@saved = @project.save
@project_not_done_counts = { @project.id => 0 }
@active_projects_count = current_user.projects.count(:conditions => "state = 'active'")
@active_projects_count = current_user.projects.active.count
@contexts = current_user.contexts
respond_to do |format|
format.js { @down_count = current_user.projects.size }
@ -124,9 +124,9 @@ class ProjectsController < ApplicationController
@project_not_done_counts[@project.id] = @project.reload().not_done_todo_count(:include_project_hidden_todos => true)
end
@contexts = current_user.contexts
@active_projects_count = current_user.projects.count(:conditions => "state = 'active'")
@hidden_projects_count = current_user.projects.count(:conditions => "state = 'hidden'")
@completed_projects_count = current_user.projects.count(:conditions => "state = 'completed'")
@active_projects_count = current_user.projects.active.count
@hidden_projects_count = current_user.projects.hidden.count
@completed_projects_count = current_user.projects.completed.count
render :template => 'projects/update.js.rjs'
return
elsif boolean_param('update_status')
@ -161,9 +161,9 @@ class ProjectsController < ApplicationController
def destroy
@project.destroy
@active_projects_count = current_user.projects.count(:conditions => "state = 'active'")
@hidden_projects_count = current_user.projects.count(:conditions => "state = 'hidden'")
@completed_projects_count = current_user.projects.count(:conditions => "state = 'completed'")
@active_projects_count = current_user.projects.active.count
@hidden_projects_count = current_user.projects.hidden.count
@completed_projects_count = current_user.projects.completed.count
respond_to do |format|
format.js { @down_count = current_user.projects.size }
format.xml { render :text => "Deleted project #{@project.name}" }
@ -199,9 +199,9 @@ class ProjectsController < ApplicationController
lambda do
@page_title = "TRACKS::List Projects"
@count = current_user.projects.size
@active_projects = @projects.select{ |p| p.active? }
@hidden_projects = @projects.select{ |p| p.hidden? }
@completed_projects = @projects.select{ |p| p.completed? }
@active_projects = @projects.active
@hidden_projects = @projects.hidden
@completed_projects = @projects.completed
@no_projects = @projects.empty?
@projects.cache_note_counts
@new_project = current_user.projects.build
@ -211,9 +211,9 @@ class ProjectsController < ApplicationController
def render_projects_mobile
lambda do
@active_projects = @projects.select{ |p| p.active? }
@hidden_projects = @projects.select{ |p| p.hidden? }
@completed_projects = @projects.select{ |p| p.completed? }
@active_projects = @projects.active
@hidden_projects = @projects.hidden
@completed_projects = @projects.completed
@down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size
cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE}
render :action => 'index_mobile'

View file

@ -118,7 +118,7 @@ class RecurringTodosController < ApplicationController
else
@message += " / did not create todo"
end
@count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"])
@count = current_user.recurring_todos.active.count
else
@message = "Error saving recurring todo"
end
@ -140,7 +140,7 @@ class RecurringTodosController < ApplicationController
# delete the recurring todo
@saved = @recurring_todo.destroy
@remaining = current_user.recurring_todos.count(:all)
@remaining = current_user.recurring_todos.count
respond_to do |format|

View file

@ -1,27 +1,30 @@
class SearchController < ApplicationController
helper :todos, :application, :notes, :projects
def results
@source_view = params['_source_view'] || 'search'
@page_title = "TRACKS::Search Results for #{params[:search]}"
terms = '%' + params[:search] + '%'
@found_todos = current_user.todos.find(:all, :conditions => ["todos.description LIKE ? OR todos.notes LIKE ?", terms, terms], :include => [:tags, :project, :context])
@found_projects = current_user.projects.find(:all, :conditions => ["name LIKE ? or description LIKE ?", terms, terms])
@found_notes = current_user.notes.find(:all, :conditions => ["body LIKE ?", terms])
@count = @found_todos.size + @found_projects.size + @found_notes.size
init_not_done_counts(['project'])
init_project_hidden_todo_counts(['project'])
end
def index
@page_title = "TRACKS::Search"
end
def init
@source_view = params['_source_view'] || 'search'
end
end
class SearchController < ApplicationController
helper :todos, :application, :notes, :projects
def results
@source_view = params['_source_view'] || 'search'
@page_title = "TRACKS::Search Results for #{params[:search]}"
terms = '%' + params[:search] + '%'
@found_todos = current_user.todos.find(:all, :conditions => ["todos.description LIKE ? OR todos.notes LIKE ?", terms, terms], :include => [:tags, :project, :context])
@found_projects = current_user.projects.find(:all, :conditions => ["name LIKE ? OR description LIKE ?", terms, terms])
@found_notes = current_user.notes.find(:all, :conditions => ["body LIKE ?", terms])
@found_contexts = current_user.contexts.find(:all, :conditions => ["name LIKE ?", terms])
# TODO: limit search to tags on todos
@found_tags = current_user.tags.find(:all, :conditions => ["name LIKE ?", terms])
@count = @found_todos.size + @found_projects.size + @found_notes.size + @found_contexts.size + @found_tags.size
init_not_done_counts
init_project_hidden_todo_counts
end
def index
@page_title = "TRACKS::Search"
end
def init
@source_view = params['_source_view'] || 'search'
end
end

View file

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

View file

@ -14,7 +14,7 @@ class TodosController < ApplicationController
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
@contexts_to_show = @contexts.reject {|x| x.hide? }
@contexts_to_show = current_user.contexts.active
respond_to do |format|
format.html &render_todos_html
@ -28,7 +28,7 @@ class TodosController < ApplicationController
end
def new
@projects = current_user.projects.select { |p| p.active? }
@projects = current_user.projects.active
@contexts = current_user.contexts.find(:all)
respond_to do |format|
format.m {
@ -76,7 +76,7 @@ class TodosController < ApplicationController
format.m do
@return_path=cookies[:mobile_url]
# todo: use function for this fixed path
@return_path='/mobile' if @return_path.nil?
@return_path='/m' if @return_path.nil?
if @saved
redirect_to @return_path
else
@ -116,7 +116,7 @@ class TodosController < ApplicationController
def show
respond_to do |format|
format.m do
@projects = current_user.projects.select { |p| p.active? }
@projects = current_user.projects.active
@contexts = current_user.contexts.find(:all)
@edit_mobile = true
@return_path=cookies[:mobile_url]
@ -399,8 +399,9 @@ class TodosController < ApplicationController
:limit => max_completed,
:conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'completed'],
:order => 'todos.completed_at DESC')
@contexts = current_user.contexts.find(:all)
@projects = current_user.projects
@contexts = current_user.contexts
@contexts_to_show = @contexts.reject {|x| x.hide? }
# Set count badge to number of items with this tag
@ -442,33 +443,33 @@ class TodosController < ApplicationController
due_next_week_date = due_this_week_date + 7.days
due_this_month_date = Time.zone.now.end_of_month
@due_today = current_user.todos.find(:all,
@due_today = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date],
:conditions => ['todos.due <= ?', due_today_date],
:order => "due")
@due_this_week = current_user.todos.find(:all,
@due_this_week = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date],
:conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date],
:order => "due")
@due_next_week = current_user.todos.find(:all,
@due_next_week = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date],
:conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date],
:order => "due")
@due_this_month = current_user.todos.find(:all,
@due_this_month = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date],
:conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date],
:order => "due")
@due_after_this_month = current_user.todos.find(:all,
@due_after_this_month = current_user.todos.not_completed.find(:all,
:include => [:taggings, :tags],
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date],
:conditions => ['todos.due > ?', due_this_month_date],
:order => "due")
@count = current_user.todos.not_completed.are_due.count
respond_to do |format|
format.html
format.ics {
@due_all = current_user.todos.find(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND NOT todos.due IS NULL', 'active', 'deferred'],
:order => "due")
@due_all = current_user.todos.not_completed.are_due.find(:all, :order => "due")
render :action => 'calendar', :layout => false, :content_type => Mime::ICS
}
end
@ -536,13 +537,17 @@ class TodosController < ApplicationController
end
def with_parent_resource_scope(&block)
@feed_title = "Actions "
if (params[:context_id])
@context = current_user.contexts.find_by_params(params)
@feed_title = @feed_title + "in context '#{@context.name}'"
Todo.send :with_scope, :find => {:conditions => ['todos.context_id = ?', @context.id]} do
yield
end
elsif (params[:project_id])
@project = current_user.projects.find_by_params(params)
@feed_title = @feed_title + "in project '#{@project.name}'"
@project_feed = true
Todo.send :with_scope, :find => {:conditions => ['todos.project_id = ?', @project.id]} do
yield
end
@ -718,7 +723,7 @@ class TodosController < ApplicationController
render_rss_feed_for @todos, :feed => todo_feed_options,
:item => {
:title => :description,
:link => lambda { |t| context_url(t.context) },
:link => lambda { |t| @project_feed.nil? ? context_url(t.context) : project_url(t.project) },
:guid => lambda { |t| todo_url(t) },
:description => todo_feed_content
}
@ -726,7 +731,9 @@ class TodosController < ApplicationController
end
def todo_feed_options
Todo.feed_options(current_user)
options = Todo.feed_options(current_user)
options[:title] = @feed_title
return options
end
def todo_feed_content
@ -779,8 +786,8 @@ class TodosController < ApplicationController
if todo.from_recurring_todo?
recurring_todo = todo.recurring_todo
# check if there are active todos belonging to this recurring todo.
# only add new one if all active todos are completed
# check if there are active todos belonging to this recurring todo. only
# add new one if all active todos are completed
if recurring_todo.todos.active.count == 0
# check for next todo either from the due date or the show_from date
@ -788,7 +795,7 @@ class TodosController < ApplicationController
# if both due and show_from are nil, check for a next todo from now
date_to_check = Time.zone.now if date_to_check.nil?
if recurring_todo.active? && recurring_todo.has_next_todo(date_to_check)
# shift the reference date to yesterday if date_to_check is furher in
@ -799,7 +806,7 @@ class TodosController < ApplicationController
# that new todos due for today will be created instead of new todos
# for tomorrow.
date = date_to_check.at_midnight >= Time.zone.now.at_midnight ? date_to_check : Time.zone.now-1.day
new_recurring_todo = create_todo_from_recurring_todo(recurring_todo, date)
end
end
@ -834,20 +841,20 @@ class TodosController < ApplicationController
due_this_month_date = Time.zone.now.end_of_month
case id
when "due_today"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date])
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due <= ?', due_today_date])
when "due_this_week"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date])
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date])
when "due_next_week"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date])
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date])
when "due_this_month"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date])
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date])
when "due_after_this_month"
return 0 == current_user.todos.count(:all,
:conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date])
return 0 == current_user.todos.not_completed.count(:all,
:conditions => ['todos.due > ?', due_this_month_date])
else
raise Exception.new, "unknown due id for calendar: '#{id}'"
end

View file

@ -1,10 +1,4 @@
class UsersController < ApplicationController
if 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 ]
@ -153,18 +147,25 @@ class UsersController < ApplicationController
end
def update_auth_type
if (params[:user][:auth_type] == 'open_id') && 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'))
if (params[:open_id_complete] || (params[:user][:auth_type] == 'open_id')) && openid_enabled?
authenticate_with_open_id do |result, identity_url|
if result.successful?
# 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 = identity_url
if @user.save
notify :notice, "You have successfully verified #{identity_url} as your identity and set your authentication type to Open ID."
else
debugger
notify :warning, "You have successfully verified #{identity_url} as your identity but there was a problem saving your authentication preferences."
end
redirect_to preferences_path
else
notify :warning, "Unable to find openid server for <q>#{openid_url}</q>"
notify :warning, result.message
redirect_to :action => 'change_auth_type'
end
end
return
end
@ -178,47 +179,6 @@ class UsersController < ApplicationController
end
end
def complete
return unless 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!

View file

@ -8,7 +8,7 @@ module ApplicationHelper
# Replicates the link_to method but also checks request.request_uri to find
# current page. If that matches the url, the link is marked id = "current"
#
#
def navigation_link(name, options = {}, html_options = nil, *parameters_for_method_reference)
if html_options
html_options = html_options.stringify_keys
@ -29,7 +29,7 @@ module ApplicationHelper
# Check due date in comparison to today's date Flag up date appropriately with
# a 'traffic light' colour code
#
#
def due_date(due)
if due == nil
return ""
@ -62,7 +62,7 @@ module ApplicationHelper
# Check due date in comparison to today's date Flag up date appropriately with
# a 'traffic light' colour code Modified method for mobile screen
#
#
def due_date_mobile(due)
if due == nil
return ""
@ -92,7 +92,7 @@ module ApplicationHelper
# Returns a count of next actions in the given context or project. The result
# is count and a string descriptor, correctly pluralised if there are no
# actions or multiple actions
#
#
def count_undone_todos_phrase(todos_parent, string="actions")
@controller.count_undone_todos_phrase(todos_parent, string)
end
@ -143,5 +143,31 @@ module ApplicationHelper
page.replace 'flash', "<h4 id='flash' class='alert #{type}'>#{message}</h4>"
page.visual_effect :fade, 'flash', :duration => fade_duration
end
def recurrence_time_span(rt)
case rt.ends_on
when "no_end_date"
return rt.start_from.nil? ? "" : "from " + format_date(rt.start_from)
when "ends_on_number_of_times"
return "for "+rt.number_of_occurences.to_s + " times"
when "ends_on_end_date"
starts = rt.start_from.nil? ? "" : "from " + format_date(rt.start_from)
ends = rt.end_date.nil? ? "" : " until " + format_date(rt.end_date)
return starts+ends
else
raise Exception.new, "unknown recurrence time span selection (#{rt.ends_on})"
end
end
def recurrence_pattern_as_text(recurring_todo)
rt = recurring_todo.recurring_target_as_text
rp = recurring_todo.recurrence_pattern
# only add space if recurrence_pattern has content
rp = " " + rp if !rp.nil?
rts = recurrence_time_span(recurring_todo)
# only add space if recurrence_time_span has content
rts = " " + rts if !(rts == "")
return rt+rp+rts
end
end

View file

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

View file

@ -20,12 +20,12 @@ module ProjectsHelper
def project_next_prev
html = ''
unless @previous_project.nil?
project_name = truncate(@previous_project.name, 40, "...")
project_name = truncate(@previous_project.name, :length => 40, :omission => "...")
html << link_to_project(@previous_project, "&laquo; #{project_name}")
end
html << ' | ' if @previous_project && @next_project
unless @next_project.nil?
project_name = truncate(@next_project.name, 40, "...")
project_name = truncate(@next_project.name, :length => 40, :omission => "...")
html << link_to_project(@next_project, "#{project_name} &raquo;")
end
html
@ -34,12 +34,12 @@ module ProjectsHelper
def project_next_prev_mobile
html = ''
unless @previous_project.nil?
project_name = truncate(@previous_project.name, 40, "...")
project_name = truncate(@previous_project.name, :length => 40, :omission => "...")
html << link_to_project_mobile(@previous_project, "5", "&laquo; 5-#{project_name}")
end
html << ' | ' if @previous_project && @next_project
unless @next_project.nil?
project_name = truncate(@next_project.name, 40, "...")
project_name = truncate(@next_project.name, :length => 40, :omission => "...")
html << link_to_project_mobile(@next_project, "6", "6-#{project_name} &raquo;")
end
html

View file

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

View file

@ -117,7 +117,7 @@ module TodosHelper
"<span class=\"tag\">" +
link_to(t.name, {:action => "tag", :controller => "todos", :id => t.name+".m"}) +
"</span>"}.join('')
"<span class=\"tags\">#{tag_list}</span>"
if tag_list.empty? then "" else "<span class=\"tags\">#{tag_list}</span>" end
end
def deferred_due_date
@ -242,13 +242,13 @@ module TodosHelper
end
def project_names_for_autocomplete
array_or_string_for_javascript( ['None'] + @projects.select{ |p| p.active? }.collect{|p| escape_javascript(p.name) } )
array_or_string_for_javascript( ['None'] + current_user.projects.active.collect{|p| escape_javascript(p.name) } )
end
def context_names_for_autocomplete
# #return array_or_string_for_javascript(['Create a new context']) if
# @contexts.empty?
array_or_string_for_javascript( @contexts.collect{|c| escape_javascript(c.name) } )
array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } )
end
def format_ical_notes(notes)

View file

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

View file

@ -1,5 +1,7 @@
class MessageGateway < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper
extend ActionView::Helpers::SanitizeHelper::ClassMethods
def receive(email)
user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip])
if user.nil?

View file

@ -3,6 +3,10 @@ class Project < ActiveRecord::Base
has_many :notes, :dependent => :delete_all, :order => "created_at DESC"
belongs_to :default_context, :class_name => "Context", :foreign_key => "default_context_id"
belongs_to :user
named_scope :active, :conditions => { :state => 'active' }
named_scope :hidden, :conditions => { :state => 'hidden' }
named_scope :completed, :conditions => { :state => 'completed'}
validates_presence_of :name, :message => "project must have a name"
validates_length_of :name, :maximum => 255, :message => "project name must be less than 256 characters"

View file

@ -295,6 +295,17 @@ class RecurringTodo < ActiveRecord::Base
def recurring_target=(t)
self.target = t
end
def recurring_target_as_text
case self.target
when 'due_date'
return "due"
when 'show_from_date'
return "show"
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
end
def recurring_show_days_before=(days)
self.show_from_delta=days
@ -361,6 +372,8 @@ class RecurringTodo < ActiveRecord::Base
when 'show_from'
# so leave due date empty
return nil
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
end
@ -471,12 +484,23 @@ class RecurringTodo < ActiveRecord::Base
start = determine_start(previous)
day = self.every_other1
n = self.every_other2
case self.recurrence_selector
when 0 # specific day of the month
if start.mday >= day
if start.mday >= day
# there is no next day n in this month, search in next month
start += n.months
#
# start += n.months
#
# The above seems to not work. Fiddle with timezone. Looks like we hit a
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
# local timezone.
#
# TODO: recheck if future rails versions have this problem too
start = Time.utc(start.year, start.month, start.day)+n.months
start = Time.zone.local(start.year, start.month, start.day)
# go back to day
end
return Time.zone.local(start.year, start.month, day)

View file

@ -7,6 +7,7 @@ class Todo < ActiveRecord::Base
named_scope :active, :conditions => { :state => 'active' }
named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed']
named_scope :are_due, :conditions => ['NOT todos.due IS NULL']
STARRED_TAG_NAME = "starred"

View file

@ -124,7 +124,7 @@ class User < ActiveRecord::Base
#for will_paginate plugin
cattr_accessor :per_page
@@per_page = 1
@@per_page = 5
def validate
unless Tracks::Config.auth_schemes.include?(auth_type)
@ -145,8 +145,8 @@ class User < ActiveRecord::Base
return nil
end
def self.find_by_open_id_url(raw_open_id_url)
normalized_open_id_url = normalize_open_id_url(raw_open_id_url)
def self.find_by_open_id_url(raw_identity_url)
normalized_open_id_url = OpenIdAuthentication.normalize_url(raw_identity_url)
find(:first, :conditions => ['open_id_url = ?', normalized_open_id_url])
end
@ -188,7 +188,7 @@ class User < ActiveRecord::Base
end
def at_midnight(date)
return TimeZone[prefs.time_zone].local(date.year, date.month, date.day, 0, 0, 0)
return ActiveSupport::TimeZone[prefs.time_zone].local(date.year, date.month, date.day, 0, 0, 0)
end
def generate_token
@ -237,13 +237,6 @@ protected
def normalize_open_id_url
return if open_id_url.nil?
self.open_id_url = self.class.normalize_open_id_url(open_id_url)
self.open_id_url = OpenIdAuthentication.normalize_url(open_id_url)
end
def self.normalize_open_id_url(raw_open_id_url)
normalized = raw_open_id_url
normalized = "http://#{raw_open_id_url}" unless raw_open_id_url =~ /\:\/\//
normalized.downcase.chomp('/')
end
end

View file

@ -1,9 +1,14 @@
<% context = context_listing %>
<% context = context_listing
suppress_drag_handle ||= false
suppress_edit_button ||= false
%>
<div id="<%= dom_id(context, "container") %>" class="list">
<div id="<%= dom_id(context) %>" class="context sortable_row" style="display:'';">
<div class="position">
<span class="handle">DRAG</span>
</div>
<% unless suppress_drag_handle -%>
<div class="position">
<span class="handle">DRAG</span>
</div>
<% end -%>
<div class="data">
<%= link_to_context( context ) %> <%= " (" + count_undone_todos_phrase(context,"actions") + ")" %>
</div>
@ -32,5 +37,5 @@
-%>
</div>
</div>
<%= render :partial => 'context_form', :object => context %>
<%= render :partial => 'contexts/context_form', :object => context %>
</div>

View file

@ -6,9 +6,9 @@ if not @not_done.empty?
# only show a context when there are actions in it
-%>
<h2><%=mobile_context.name%></h2>
<table cellpadding="0" cellspacing="0" border="0">
<ul class="c">
<%= render :partial => "todos/mobile_todo",
:collection => @not_done,
:locals => { :parent_container_type => "context" }-%>
</table>
<% end -%>
</ul>
<% end -%>

View file

@ -8,5 +8,5 @@
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
<%= render :template => "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -123,7 +123,7 @@
</div><!-- End of display_box -->
<div id="input_box">
<%= render "sidebar/sidebar" %>
<%= render :template => "sidebar/sidebar" %>
</div><!-- End of input box -->
<script type="text/javascript">

View file

@ -0,0 +1,14 @@
xml.instruct!
xml.OpenSearchDescription 'xmlns' => "http://a9.com/-/spec/opensearch/1.1/" do
xml.ShortName Tracks
xml.Description 'Search in Tracks'
xml.InputEncoding 'UTF-8'
xml.Image("data:image/x-icon;base64," + @icon_data,
'width' => '16', 'height' => '16')
xml.Url 'type' => 'text/html', 'method' => 'GET',
'template' => url_for(:controller => 'search', :action => 'results',
:only_path => false) + '?search={searchTerms}'
end

View file

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

View file

@ -5,13 +5,23 @@
<% if @prefs.refresh != 0 -%>
<meta http-equiv="Refresh" content="<%= @prefs["refresh"].to_i*60 %>;url=<%= request.request_uri %>">
<% end -%>
<%= javascript_include_merged :tracks %>
<% bundle do %>
<%= javascript_include_tag *%w[
prototype effects dragdrop controls application
calendar calendar-en calendar-setup
accesskey-hints todo-items niftycube
protoload flashobject lowpro
] %>
<%= stylesheet_link_tag *%w[ standard calendar-system niftyCorners] %>
<% end %>
<%= javascript_include_tag :unobtrusive %>
<%= stylesheet_link_merged :tracks %>
<%= stylesheet_link_tag "print", :media => "print" %>
<link rel="shortcut icon" href="<%= url_for(:controller => 'favicon.ico') %>" />
<%= auto_discovery_link_tag(:rss, {:controller => "todos", :action => "index", :format => 'rss', :token => "#{current_user.token}"}, {:title => "RSS feed of next actions"}) %>
<link rel="search" type="application/opensearchdescription+xml"
title="Tracks"
href="<%= search_plugin_path %>" />
<script type="text/javascript">
window.onload=function(){

View file

@ -14,19 +14,19 @@
<% form_tag :action=> 'login' do %>
<table>
<tr>
<td width="100px"><label for="user_login">Login:</label></td>
<td width="100px"><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
<td><label for="user_login">Login:</label></td>
<td><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_password">Password:</label></td>
<td width="100px"><input type="password" name="user_password" id="user_password" class="login_text" /></td>
<td><label for="user_password">Password:</label></td>
<td><input type="password" name="user_password" id="user_password" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
<td><label for="user_noexpiry">Stay logged in:</label></td>
<td><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td></td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>
@ -36,18 +36,18 @@
<% if show_openid_form %>
<div id="openid_auth_form" style="display:none">
<% form_tag :action=> 'login', :action => 'begin' do %>
<% form_tag :action=> 'login' do %>
<table>
<tr>
<td width="100px"><label for="openid_url">Identity URL:</label></td>
<td width="100px"><input type="text" name="openid_url" id="openid_url" value="<%= @openid_url %>" class="login_text open_id" /></td>
<td><label for="openid_url">Identity URL:</label></td>
<td><input type="text" name="openid_url" id="openid_url" value="<%= @openid_url %>" class="login_text open_id" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
<td><label for="user_noexpiry">Stay logged in:</label></td>
<td><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
</tr>
<tr>
<td width="100px"></td>
<td></td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>

View file

@ -14,19 +14,19 @@
<% form_tag formatted_login_path(:format => 'm') do %>
<table>
<tr>
<td width="100px"><label for="user_login">Login:</label></td>
<td width="100px"><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
<td><label for="user_login">Login:</label></td>
<td><input type="text" name="user_login" id="user_login" value="" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_password">Password:</label></td>
<td width="100px"><input type="password" name="user_password" id="user_password" class="login_text" /></td>
<td><label for="user_password">Password:</label></td>
<td><input type="password" name="user_password" id="user_password" class="login_text" /></td>
</tr>
<tr>
<td width="100px"><label for="user_noexpiry">Stay logged in:</label></td>
<td width="100px"><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked /></td>
<td><label for="user_noexpiry">Stay logged in:</label></td>
<td><input type="checkbox" name="user_noexpiry" id="user_noexpiry" checked="checked" /></td>
</tr>
<tr>
<td width="100px"></td>
<td>&nbsp;</td>
<td><input type="submit" name="login" value="Sign In &#187;" class="primary" /></td>
</tr>
</table>

View file

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

View file

@ -74,5 +74,5 @@
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
<%= render :template => "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -6,7 +6,7 @@
<div class="description">
<span class="todo.descr"><%= sanitize(recurring_todo.description) %></span> <%= recurring_todo_tag_list %>
<span class='recurrence_pattern'>
[<%=recurrence_target(recurring_todo)%> <%= recurring_todo.recurrence_pattern %> <%= recurrence_time_span(recurring_todo) %>]
[<%= recurrence_pattern_as_text(@recurring_todo) %>]
</span>
</div>
</div>

View file

@ -13,7 +13,7 @@ if @recurring_saved
page.hide 'recurring-todos-empty-nd'
page.insert_html :bottom,
'recurring_todos_container',
:partial => 'recurring_todos/recurring_todo'
:partial => 'recurring_todos/recurring_todo', :locals => { :recurring_todo => @recurring_todo}
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
# update badge count
page['badge_count'].replace_html @count

View file

@ -4,7 +4,7 @@ if @saved
if @recurring_todo.completed?
# show completed recurring todo
page.insert_html :top, "completed_recurring_todos_container", :partial => 'recurring_todos/recurring_todo'
page.insert_html :top, "completed_recurring_todos_container", :partial => 'recurring_todos/recurring_todo', :locals => { :recurring_todo => @recurring_todo }
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
# set empty messages
@ -14,7 +14,7 @@ if @saved
# recurring_todo is activated
# show completed recurring todo
page.insert_html :top, "recurring_todos_container", :partial => 'recurring_todos/recurring_todo'
page.insert_html :top, "recurring_todos_container", :partial => 'recurring_todos/recurring_todo', :locals => { :recurring_todo => @recurring_todo }
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
# inform user if a new todo has been created because of the activation

View file

@ -13,7 +13,7 @@ if @saved
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created
# replace old recurring todo with updated todo
page.replace dom_id(@recurring_todo), :partial => 'recurring_todos/recurring_todo'
page.replace dom_id(@recurring_todo), :partial => 'recurring_todos/recurring_todo', :locals => { :recurring_todo => @recurring_todo }
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
else

View file

@ -1,6 +1,9 @@
<div id="display_box_search">
<% form_tag(:action => :results) do %>
<%= text_field_tag(:search, params[:search]) %>
<%= submit_tag "Search" %>
<% end %>
</div>
<div id="display_box_search">
<% form_tag({:action => :results}, :id => 'search-form') do %>
<%= text_field_tag(:search, params[:search]) %>
<%= submit_tag "Search" %>
<% end %>
</div>
<script type="text/javascript">
Form.focusFirstElement('search-form')
</script>

View file

@ -1,32 +1,48 @@
<div id="display_box_results">
<% if @count == 0 -%>
<div class="message"><p>Your search yielded no results.</p></div>
<% else -%>
<% source_view_is = :search %>
<% parent_container_type = 'search' %>
<% if not @found_todos.empty? -%>
<div id="found-todos-container" class="container">
<h2><span id="found-todos-count" class="badge"><%= @found_todos.size %></span>Todos matching query</h2>
<%= render :partial => "todos/todo", :collection => @found_todos, :locals => { :parent_container_type => 'search', :suppress_context => false, :suppress_project => false, :suppress_edit_button => true } %>
</div>
<% end -%>
<% if not @found_projects.empty? -%>
<div id="found-projects-container" class="container">
<h2><span id="found-projects-count" class="badge"><%= @found_projects.size %></span>Projects matching query</h2>
<%= render :partial => "projects/project_listing", :collection => @found_projects, :locals => { :suppress_drag_handle => true, :suppress_edit_button => true } %>
</div>
<% end -%>
<% if not @found_notes.empty? -%>
<div id="found-notes-container" class="container">
<h2><span id="found-notes-count" class="badge"><%= @found_notes.size %></span>Notes matching query</h2>
<% for notes in @found_notes -%>
<div class="container" id="note-<%= notes.id %>-wrapper">
<%= render :partial => "notes/notes_summary", :object => notes %>
</div>
<% end -%>
</div>
<% end -%>
<% end -%>
</div>
<div id="display_box_results">
<% if @count == 0 -%>
<div class="message"><p>Your search yielded no results.</p></div>
<% else -%>
<% unless @found_todos.empty? -%>
<div id="found-todos-container" class="container">
<h2><span id="found-todos-count" class="badge"><%= @found_todos.size %></span>Todos matching query</h2>
<%= render :partial => "todos/todo", :collection => @found_todos, :locals => { :parent_container_type => 'search', :suppress_context => false, :suppress_project => false, :suppress_edit_button => true } %>
</div>
<% end -%>
<% unless @found_projects.empty? -%>
<div id="found-projects-container" class="container">
<h2><span id="found-projects-count" class="badge"><%= @found_projects.size %></span>Projects matching query</h2>
<%= render :partial => "projects/project_listing", :collection => @found_projects, :locals => { :suppress_drag_handle => true, :suppress_edit_button => true } %>
</div>
<% end -%>
<% unless @found_notes.empty? -%>
<div id="found-notes-container" class="container">
<h2><span id="found-notes-count" class="badge"><%= @found_notes.size %></span>Notes matching query</h2>
<% for notes in @found_notes -%>
<div class="container" id="note-<%= notes.id %>-wrapper">
<%= render :partial => "notes/notes_summary", :object => notes %>
</div>
<% end -%>
</div>
<% end -%>
<% unless @found_contexts.empty? -%>
<div id="found-contexts-container" class="container">
<h2><span id="found-contexts-count" class="badge"><%= @found_contexts.size %></span>Contexts matching query</h2>
<%= render :partial => "contexts/context_listing", :collection => @found_contexts, :locals => { :suppress_drag_handle => true, :suppress_edit_button => true } %>
</div>
<% end -%>
<% unless @found_tags.empty? -%>
<div id="found-tags-container" class="container">
<h2><span id="found-tags-count" class="badge"><%= @found_tags.size %></span>Tags matching query</h2>
<span class="tags"><% @found_tags.each do |tag| -%>
<span class="tag"><%= link_to tag.name, {:controller => "todos", :action => "tag", :id => tag.name} -%></span>
<% end %>
</span>
<br/>
</div>
<% end %>
<% end -%>
</div>

View file

@ -2,7 +2,7 @@
@todo = nil
@initial_context_name = @context.name unless @context.nil?
@initial_context_name ||= @project.default_context.name unless @project.nil? || @project.default_context.nil?
@initial_context_name ||= @contexts[0].name unless @contexts[0].nil?
@initial_context_name ||= current_user.contexts.first.name unless current_user.contexts.first.nil?
@initial_project_name = @project.name unless @project.nil?
%>
<div id="todo_new_action_container">

View file

@ -1,30 +1,30 @@
<div id="sidebar">
<% # show active items before hidden / completed items -%>
<% # show active items before hidden / completed items -%>
<%= render :partial => "sidebar/project_list",
:locals => { :list_name => 'Active Projects',
:projects => @projects.select{|p| p.active? } } -%>
:projects => @active_projects } -%>
<%= render :partial => "sidebar/context_list",
:locals => { :list_name => 'Active Contexts',
:contexts => @contexts.reject{|c| c.hide? } } -%>
:contexts => @active_contexts } -%>
<% if prefs.show_hidden_projects_in_sidebar -%>
<%= render :partial => "sidebar/project_list",
:locals => { :list_name => 'Hidden Projects',
:projects => @projects.select{|p| p.hidden? } } -%>
:projects => @hidden_projects } -%>
<% end -%>
<% if prefs.show_completed_projects_in_sidebar -%>
<%= render :partial => "sidebar/project_list",
:locals => { :list_name => 'Completed Projects',
:projects => @projects.select{|p| p.completed? } } -%>
:projects => @completed_projects } -%>
<% end -%>
<% if prefs.show_hidden_contexts_in_sidebar -%>
<%= render :partial => "sidebar/context_list",
:locals => { :list_name => 'Hidden Contexts',
:contexts => @contexts.select{|c| c.hide? } } -%>
:contexts => @hidden_contexts } -%>
<% end -%>
<div class="integrations-link"><ul>

View file

@ -8,9 +8,9 @@ end
-%><%=@actions_per_context[@actions_per_context.size()-1]['total'].to_i*100/@sum%>&
&pie_labels=<%
0.upto @actions_per_context.size()-2 do | i |
%><%=truncate(@actions_per_context[i]['name'],@truncate_chars, '...')%>,<%
%><%=truncate(@actions_per_context[i]['name'], :length => @truncate_chars, :omission => '...')%>,<%
end
-%><%=truncate(@actions_per_context[@actions_per_context.size()-1]['name'],@truncate_chars,'...') %>&
-%><%=truncate(@actions_per_context[@actions_per_context.size()-1]['name'], :legnth => @truncate_chars, :omission => '...') %>&
&links=<%
0.upto @actions_per_context.size()-2 do | i |
%><%=url_for :controller => "contexts", :action => "show", :id=>@actions_per_context[i]['id']%>,<%

View file

@ -8,9 +8,9 @@ end
-%><%=@actions_per_context[@actions_per_context.size()-1]['total'].to_i*100/@sum%>&
&pie_labels=<%
0.upto @actions_per_context.size()-2 do | i |
%><%=truncate(@actions_per_context[i]['name'], @truncate_chars, '...') %>,<%
%><%=truncate(@actions_per_context[i]['name'], :length => @truncate_chars, :omission => '...') %>,<%
end
-%><%=truncate(@actions_per_context[@actions_per_context.size()-1]['name'], @truncate_chars, '...') %>&
-%><%=truncate(@actions_per_context[@actions_per_context.size()-1]['name'], :length => @truncate_chars, :omission => '...') %>&
&links=<%
0.upto @actions_per_context.size()-2 do | i |
%><%=url_for :controller => "contexts", :action => "show", :id=>@actions_per_context[i]['id']%>,<%

View file

@ -10,7 +10,7 @@ if parent_container_type == 'show_mobile' -%>
<h2><label for="todo_description">Description</label></h2>
<%= text_field( "todo", "description", "tabindex" => 2) %>
<h2><label for="todo_notes">Notes</label></h2>
<%= text_area( "todo", "notes", "cols" => 30, "rows" => 5, "tabindex" => 3) %>
<%= text_area( "todo", "notes", "cols" => 30, "rows" => 2, "tabindex" => 3) %>
<h2><label for="todo_context_id">Context</label></h2>
<%= unless @mobile_from_context
collection_select( "todo", "context_id", @contexts, "id", "name", {}, {"tabindex" => 4} )
@ -22,7 +22,7 @@ end %>
<h2><label for="todo_project_id">Project</label></h2>
<%= unless @mobile_from_project
collection_select( "todo", "project_id", @projects, "id", "name",
{:include_blank => true}, {"tabindex" => 5} )
{:include_blank => '--No project--'}, {"tabindex" => 5} )
else
# manually add blank option since :include_blank does not work
# with options_from_collection_for_select
@ -34,7 +34,7 @@ end %>
<%= text_field_tag "tag_list", @tag_list_text, :size => 30, :tabindex => 6 %>
<h2><label for="todo_due">Due</label></h2>
<%= date_select("todo", "due", {:order => [:day, :month, :year],
:start_year => this_year, :include_blank => true}, :tabindex => 7) %>
:start_year => this_year, :include_blank => '--'}, :tabindex => 7) %>
<h2><label for="todo_show_from">Show from</label></h2>
<%= date_select("todo", "show_from", {:order => [:day, :month, :year],
:start_year => this_year, :include_blank => true}, :tabindex => 8) %>

View file

@ -1,11 +1,10 @@
<% @todo = mobile_todo
if mobile_todo.starred?
bullet = "<span class=star>"+image_tag("menustar_small.gif")+"</span>"
li_class = " class=\"star\""
else
bullet = "<span class=r>&raquo;&nbsp;</span>"
li_class = ""
end -%>
<div class="t" id="<%= dom_id(mobile_todo) %>">
<tr valign="top"><td><%= bullet %></td><td><%
<li id="<%= dom_id(mobile_todo) %>" <%= li_class %>><%
if mobile_todo.completed?
-%><span class="m_t_d">
<% else
@ -23,4 +22,4 @@ end -%>
")</span>" -%>
<% end -%>
<%= tag_list_mobile -%>
</span></td></tr></div>
</span></li>

View file

@ -14,7 +14,10 @@
<% unless @todo.completed? %><span class="defer-container"><%= defer_link(1) %> <%= defer_link(7) %></span><% end %>
<%= date_span -%>
<span class="todo.descr"><%= h sanitize(todo.description) %></span>
<%= link_to(image_tag("recurring16x16.png"), {:controller => "recurring_todos", :action => "index"}, :class => "recurring_icon") if @todo.from_recurring_todo? %>
<%= link_to(
image_tag("recurring16x16.png"),
{:controller => "recurring_todos", :action => "index"},
:class => "recurring_icon", :title => recurrence_pattern_as_text(@todo.recurring_todo)) if @todo.from_recurring_todo? %>
<%= tag_list %>
<%= deferred_due_date %>
<%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %>

View file

@ -20,7 +20,7 @@ CLASS:PUBLIC
CATEGORIES:Tracks
CREATED:<%= todo.created_at.strftime("%Y%m%dT%H%M%SZ") %>
DESCRIPTION:<%= format_ical_notes(todo.notes) %>
LAST-MODIFIED:<%= due_date.strftime("%Y%m%dT%H%M%SZ") %>
LAST-MODIFIED:<%= todo.updated_at.strftime("%Y%m%dT%H%M%SZ") %>
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED

View file

@ -16,7 +16,7 @@ if @saved
page.insert_html :top, 'display_box', :partial => 'contexts/context', :locals => { :context => @todo.context, :collapsible => true }
else
page.call "todoItems.ensureVisibleWithEffectAppear", "c#{@todo.context_id}" if source_view_is_one_of(:todo, :deferred)
page.insert_html :bottom, item_container_id(@todo) + 'items', :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type, :source_view => @source_view }
page.insert_html :bottom, item_container_id(@todo) + 'items', :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type, :source_view => @source_view }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
end

View file

@ -10,5 +10,7 @@
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
<%- # TODO: this used to be render :template, but somehow it was not
#rendered after the rails2.2.2 upgrade -%>
<%= render :file => "sidebar/sidebar.html.erb" %>
</div><!-- End of input box -->

View file

@ -11,5 +11,5 @@
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
<%= render :template => "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -23,5 +23,5 @@
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
<%= render :template => "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -5,7 +5,7 @@ if @saved
# completed todos move from their context to the completed container
unless @prefs.hide_completed_actions?
page.insert_html :top, "completed_containeritems", :partial => 'todos/todo', :locals => { :parent_container_type => "completed" }
page.insert_html :top, "completed_containeritems", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => "completed" }
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
page[empty_container_msg_div_id].show if @down_count == 0 && !empty_container_msg_div_id.nil?
page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0
@ -33,7 +33,7 @@ if @saved
else
# todo is activated from completed container
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo)
page.insert_html :bottom, item_container_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.insert_html :bottom, item_container_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
page.show "empty-d" if @completed_count == 0
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil? # If we've checked something as undone, incomplete items can't be empty

View file

@ -36,7 +36,7 @@ if @saved
page.call "todoItems.expandNextActionListingByContext", "c#{@todo.context_id}items", true
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
# show all todos in context
page.insert_html :bottom, "c#{@todo.context_id}items", :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.insert_html :bottom, "c#{@todo.context_id}items", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
end
# update badge count
@ -52,7 +52,7 @@ if @saved
end
end
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
elsif source_view_is :project
@ -63,17 +63,17 @@ if @saved
elsif @todo.deferred?
page[@todo].remove
page.show("p#{@original_item_project_id}empty-nd") if (@remaining_undone_in_project == 0)
page.insert_html :bottom, "tickler", :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.insert_html :bottom, "tickler", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page['tickler-empty-nd'].hide
page.replace_html "badge_count", @down_count
elsif @todo_was_activated_from_deferred_state
page[@todo].remove
page['tickler-empty-nd'].show if (@deferred_count == 0)
page.insert_html :bottom, "p#{@todo.project_id}", :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.insert_html :bottom, "p#{@todo.project_id}", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page["p#{@todo.project_id}empty-nd"].hide
page.replace_html "badge_count", @down_count
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
elsif source_view_is :deferred
@ -87,7 +87,7 @@ if @saved
page.call "todoItems.ensureVisibleWithEffectAppear", "c#{@todo.context_id}"
page.call "todoItems.expandNextActionListingByContext", "c#{@todo.context_id}items", true
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
page.insert_html :bottom, "c#{@todo.context_id}items", :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.insert_html :bottom, "c#{@todo.context_id}items", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.replace_html("badge_count", @down_count)
page.delay(0.5) do
page.call "todoItems.ensureContainerHeight", "c#{@original_item_context_id}items"
@ -95,18 +95,18 @@ if @saved
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
elsif source_view_is :stats
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
elsif source_view_is :calendar
if @due_date_changed
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
page.hide "empty_"+@new_due_id
page.insert_html :bottom, @new_due_id, :partial => 'todos/todo'
page.insert_html :bottom, @new_due_id, :partial => 'todos/todo', :locals => {:todo => @todo}
page.visual_effect :highlight, dom_id(@todo), :duration => 3
else
if @todo.due.nil?
@ -114,7 +114,7 @@ if @saved
page[@todo].remove
page.show "empty_"+@original_item_due_id if @old_due_empty
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
end

View file

@ -1,22 +0,0 @@
---
javascripts:
- tracks:
- prototype
- effects
- dragdrop
- controls
- application
- calendar
- calendar-en
- calendar-setup
- accesskey-hints
- todo-items
- niftycube
- protoload
- flashobject
- lowpro
stylesheets:
- tracks:
- standard
- calendar-system
- niftyCorners

View file

@ -67,7 +67,7 @@ module Rails
class << self
def rubygems_version
Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion
Gem::RubyGemsVersion rescue nil
end
def gem_version
@ -82,14 +82,14 @@ module Rails
def load_rubygems
require 'rubygems'
unless rubygems_version >= '0.9.4'
$stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.)
min_version = '1.3.1'
unless rubygems_version >= min_version
$stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
exit 1
end
rescue LoadError
$stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org)
$stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
exit 1
end

View file

@ -15,6 +15,10 @@ class Rails::Configuration
attr_accessor :action_web_service
end
# Leave this alone or set it to one or more of ['database', 'ldap', 'open_id'].
# If you choose ldap, see the additional configuration options further down.
AUTHENTICATION_SCHEMES = ['database']
Rails::Initializer.run do |config|
# Skip frameworks you're not going to use
# config.frameworks -= [ :action_web_service, :action_mailer ]
@ -22,6 +26,8 @@ Rails::Initializer.run do |config|
config.action_web_service = Rails::OrderedOptions.new
config.load_paths += %W( #{RAILS_ROOT}/app/apis )
config.action_controller.use_accept_header = true
# Add additional load paths for your own custom dirs
# config.load_paths += %W( #{RAILS_ROOT}/app/services )
@ -33,10 +39,10 @@ Rails::Initializer.run do |config|
# (create the session table with 'rake create_sessions_table')
config.action_controller.session_store = :active_record_store
# config.action_controller.session = {
# :session_key => '_tracks_session_id',
# :secret => SALT * (30.0 / SALT.length).ceil #must be at least 30 characters
# }
config.action_controller.session = {
:session_key => '_tracks_session_id',
:secret => SALT * (30.0 / SALT.length).ceil #must be at least 30 characters
}
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
@ -70,9 +76,6 @@ end
# Include your application configuration below
# Leave this alone or set it to one or more of ['database', 'ldap', 'open_id'].
# If you choose ldap, see the additional configuration options further down.
AUTHENTICATION_SCHEMES = ['database']
require 'name_part_finder'
require 'tracks/todo_list'
@ -96,14 +99,11 @@ end
# setting this to true will make the cookies only available over HTTPS
TRACKS_COOKIES_SECURE = false
MOBILE_CONTENT_TYPE = 'tracks/mobile'
Mime::Type.register(MOBILE_CONTENT_TYPE, :m)
tracks_version='1.7-devel'
tracks_version='1.7RC'
# comment out next two lines if you do not want (or can not) the date of the
# last git commit in the footer
info=`git log --pretty=format:"%ai" -1`
tracks_version=tracks_version + ' ('+info+')'
# info=`git log --pretty=format:"%ai" -1`
# tracks_version=tracks_version + ' ('+info+')'
TRACKS_VERSION=tracks_version

View file

@ -1,3 +1,4 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
# Mime::Type.register "application/x-mobile", :mobile
Mime::Type.register_alias "text/html", :m

View file

@ -6,10 +6,6 @@ ActionController::Routing::Routes.draw do |map|
login.formatted_login 'login.:format', :action => 'login'
login.logout 'logout', :action => 'logout'
login.formatted_logout 'logout.:format', :action => 'logout'
login.open_id_begin 'begin', :action => 'begin'
login.formatted_open_id_begin 'begin.:format', :action => 'begin'
login.open_id_complete 'complete', :action => 'complete'
login.formatted_open_id_complete 'complete.:format', :action => 'complete'
end
map.resources :users,
@ -57,6 +53,7 @@ ActionController::Routing::Routes.draw do |map|
todos.mobile_abbrev 'm', :action => "index", :format => 'm'
todos.mobile_abbrev_new 'm/new', :action => "new", :format => 'm'
end
map.root :controller => 'todos' # Make OpenID happy because it needs #root_url defined
map.resources :notes
map.feeds 'feeds', :controller => 'feedlist', :action => 'index'
@ -68,6 +65,7 @@ ActionController::Routing::Routes.draw do |map|
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
map.integrations 'integrations', :controller => 'integrations', :action => 'index'
map.search_plugin '/integrations/search_plugin.xml', :controller => 'integrations', :action => 'search_plugin', :format => 'xml'
map.resources :recurring_todos,
:member => {:toggle_check => :put, :toggle_star => :put}

View file

@ -0,0 +1,43 @@
class UpgradeOpenId < ActiveRecord::Migration
def self.up
create_table :open_id_authentication_associations, :force => true do |t|
t.integer :issued, :lifetime
t.string :handle, :assoc_type
t.binary :server_url, :secret
end
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :timestamp, :null => false
t.string :server_url, :null => true
t.string :salt, :null => false
end
drop_table :open_id_associations
drop_table :open_id_nonces
drop_table :open_id_settings
end
def self.down
drop_table :open_id_authentication_associations
drop_table :open_id_authentication_nonces
create_table "open_id_associations", :force => true do |t|
t.binary "server_url"
t.string "handle"
t.binary "secret"
t.integer "issued"
t.integer "lifetime"
t.string "assoc_type"
end
create_table "open_id_nonces", :force => true do |t|
t.string "nonce"
t.integer "created"
end
create_table "open_id_settings", :force => true do |t|
t.string "setting"
t.binary "value"
end
end
end

View file

@ -5,15 +5,15 @@
-- Dump of table contexts
-- ------------------------------------------------------------
INSERT INTO "contexts" VALUES(1,'agenda',1,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(2,'call',2,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(3,'email',3,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(4,'errand',4,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(5,'lab',5,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(6,'library',6,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(7,'freetime',7,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(8,'office',8,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(9,'waiting for',9,'f',1,'2008-02-25 20:21:09','2008-02-25 20:21:09');
INSERT INTO "contexts" VALUES(1,'agenda',1,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(2,'call',2,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(3,'email',3,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(4,'errand',4,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(5,'lab',5,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(6,'library',6,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(7,'freetime',7,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(8,'office',8,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
INSERT INTO "contexts" VALUES(9,'waiting for',9,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-- Dump of table notes
-- ------------------------------------------------------------
@ -25,33 +25,33 @@ INSERT INTO "notes" VALUES(2,1,1,'Should I go for a swirly effect or a whooshy o
-- Dump of table projects
-- ------------------------------------------------------------
INSERT INTO "projects" VALUES(1,'Build a working time machine',1,1,'','active','2008-02-25 20:21:09','2008-02-25 20:21:09',NULL,NULL);
INSERT INTO "projects" VALUES(2,'Make more money than Billy Gates',2,1,'','active','2008-02-25 20:21:09','2008-02-25 20:21:09',NULL,NULL);
INSERT INTO "projects" VALUES(3,'Evict dinosaurs from the garden',3,1,'','active','2008-02-25 20:21:09','2008-02-25 20:21:09',NULL,NULL);
INSERT INTO "projects" VALUES(1,'Build a working time machine',1,1,'','active','2008-02-25 20:21:09','2008-03-24 19:23:53',NULL,NULL);
INSERT INTO "projects" VALUES(2,'Make more money than Billy Gates',2,1,'','active','2008-02-25 20:21:09','2008-03-24 19:23:53',NULL,NULL);
INSERT INTO "projects" VALUES(3,'Evict dinosaurs from the garden',3,1,'','active','2008-02-25 20:21:09','2008-03-24 19:23:53',NULL,NULL);
-- Dump of table schema_info
-- ------------------------------------------------------------
INSERT INTO "schema_info" VALUES(37);
INSERT INTO "schema_migrations" VALUES('44');
-- Dump of table todos
-- ------------------------------------------------------------
INSERT INTO "todos" VALUES(1,1,2,'Call Bill Gates to find out how much he makes per day',NULL,'2006-06-03 14:36:02','2006-06-24',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(2,2,3,'Call dinosaur exterminator','Ask him if I need to hire a skip for the corpses.','2006-06-10 14:36:02','2006-06-24',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(3,4,NULL,'Buy milk',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'completed');
INSERT INTO "todos" VALUES(4,4,NULL,'Buy bread',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'completed');
INSERT INTO "todos" VALUES(5,5,1,'Construct time dilation device',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(6,2,1,'Phone Grandfather to ask about the paradox','Added some _notes_.','2006-06-10 14:36:02','2006-06-03',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(7,6,3,'Get a book out of the library','Dinosaurs''R','2006-06-10 14:36:02',NULL,NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(8,4,NULL,'Upgrade to Rails 0.9.1',NULL,'2006-06-10 14:36:02','2006-06-10',NULL,1,NULL,'completed');
INSERT INTO "todos" VALUES(9,1,NULL,'This should be due today',NULL,'2006-06-10 14:36:02','2006-06-10',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(10,1,NULL,'foo',NULL,'2006-06-10 14:36:02','2005-01-05',NULL,1,NULL,'completed');
INSERT INTO "todos" VALUES(11,1,2,'Buy shares',NULL,'2006-06-10 14:36:02','2005-02-01',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(12,1,3,'Buy stegosaurus bait',NULL,'2006-06-10 14:36:02','2006-06-17',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(13,1,3,'New action in context','Some notes','2006-06-10 14:36:02','2006-06-17',NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(14,2,2,'Call stock broker','tel: 12345','2006-06-03 14:36:02',NULL,NULL,1,NULL,'active');
INSERT INTO "todos" VALUES(1,1,2,'Call Bill Gates to find out how much he makes per day',NULL,'2006-06-03 14:36:02','2006-06-23 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-03 14:36:02');
INSERT INTO "todos" VALUES(2,2,3,'Call dinosaur exterminator','Ask him if I need to hire a skip for the corpses.','2006-06-10 14:36:02','2006-06-23 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(3,4,NULL,'Buy milk',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(4,4,NULL,'Buy bread',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(5,5,1,'Construct time dilation device',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(6,2,1,'Phone Grandfather to ask about the paradox','Added some _notes_.','2006-06-10 14:36:02','2006-06-02 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(7,6,3,'Get a book out of the library','Dinosaurs''R','2006-06-10 14:36:02',NULL,NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(8,4,NULL,'Upgrade to Rails 0.9.1',NULL,'2006-06-10 14:36:02','2006-06-09 23:00:00',NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(9,1,NULL,'This should be due today',NULL,'2006-06-10 14:36:02','2006-06-09 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(10,1,NULL,'foo',NULL,'2006-06-10 14:36:02','2005-01-05 00:00:00',NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(11,1,2,'Buy shares',NULL,'2006-06-10 14:36:02','2005-02-01 00:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(12,1,3,'Buy stegosaurus bait',NULL,'2006-06-10 14:36:02','2006-06-16 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(13,1,3,'New action in context','Some notes','2006-06-10 14:36:02','2006-06-16 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
INSERT INTO "todos" VALUES(14,2,2,'Call stock broker','tel: 12345','2006-06-03 14:36:02',NULL,NULL,1,NULL,'active',NULL,'2006-06-03 14:36:02');

View file

@ -1,19 +1,18 @@
= Tracks: a GTD web application, built with Ruby on Rails
* Homepage: http://www.rousette.org.uk/projects/
* Author: bsag (http://www.rousette.org.uk/)
* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson, Eric Allen
* Version: 1.6
* Copyright: (cc) 2004-2008 rousette.org.uk
* License: GNU GPL
* Project homepage: http://www.rousette.org.uk/projects/
* GitHub: http://github.com/bsag/tracks/
* Trac (for bug reports and feature requests): http://dev.rousette.org.uk/report/6
* Wiki (community contributed information): http://www.rousette.org.uk/projects/wiki/
* Forum: http://www.rousette.org.uk/projects/forums/
* Mailing list: http://lists.rousette.org.uk/mailman/listinfo/tracks-discuss
* Original developer: bsag (http://www.rousette.org.uk/)
* Contributors: http://dev.rousette.org.uk/wiki/Tracks/Contributing/Contributors
* Version: 1.7
* Copyright: (cc) 2004-2008 rousette.org.uk.
* License: GNU GPL
Main project site: http://www.rousette.org.uk/projects/
Trac (for bug reports and feature requests): http://dev.rousette.org.uk/report/6
Wiki (deprecated - please use Trac): http://www.rousette.org.uk/projects/wiki/
== Version 1.7dev
== Version 1.7RC
New features:
1. Recurring todos
@ -23,10 +22,12 @@ New features:
5. New buttons to quickly defer an action 1 or 7 days
6. Calendar view to review due actions, includes iCal feed to use in your calendar app (tested with Google Calendar, Evolution, Outlook 2007)
7. You can now sort projects on number of active todos
8. Support for OpenSearch. This means you can add a Tracks as a search provider in your webbrowser (tested on FF3 and IE7)
Under the hood:
1. Move selenium tests to RSpec
2. Bugfixes
1. Started to move selenium tests to RSpec
2. Upgrade to Rails 2.2.2
2. Bugfixes, including fixing OpenID
== Version 1.6
1. upgrade to rails 2.0.2

View file

@ -11,27 +11,25 @@
<meta name="Copyright" content="2008 rousette.org.uk
This work is licensed under a Creative Commons License.
http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<meta name="Date" content="2008-04-07" />
<meta name="Date" content="2008-12-14" />
<meta name="Format" content="complete" />
<meta name="LaTeXXSLT" content="memoir-twosided-manual.xslt" />
<meta name="Revision" content="$Id: manual.markdown 864 2008-06-03 17:01:00Z bsag $" />
<title>Tracks 1.6 Manual</title>
<meta name="Version" content="1.6" />
<meta name="Revision" content="$Id: manual.markdown 2008-12-14 11:50:00Z bsag $" />
<title>Tracks 1.7 Manual</title>
<meta name="Version" content="1.7" />
<meta name="XMP" content="CCAttributionShareAlike" />
</head>
<body>
<!-- The HTML file manual.html is generated from manual.markdown, so make edits to the *.markdown file -->
<h2 id="installingtracks1.6">Installing Tracks 1.6</h2>
<h2 id="installingtracks1.7">Installing Tracks 1.7</h2>
<h3 id="introduction">Introduction</h3>
<p><strong>An important note for version 1.6: OpenID support is broken in this release. The fix isn&#8217;t trivial because of changes to the <code>ruby-openid</code> gem, so we wanted to get this version out now and fix OpenID for the next release. If you depend on OpenID integration, we recommend waiting until the next release.</strong></p>
<p>Tracks 1.7 has been thoroughly beta tested by a large number of people, and should be fully stable for everyday use. However, once set up, Tracks will contain the majority of your plans for your work and personal life, so it&#8217;s only sensible to make sure that you have frequent, reliable backups of your data. Full changenotes on the release can be found in <code>doc/CHANGELOG</code>. Full API documentation can be found at <code>doc/app/index.html</code>, once you have run <code>rake appdoc</code></p>
<p>Tracks 1.6 has been thoroughly beta tested by a large number of people, and should be fully stable for everyday use. However, once set up, Tracks will contain the majority of your plans for your work and personal life, so it&#8217;s only sensible to make sure that you have frequent, reliable backups of your data. Full changenotes on the release can be found in <code>doc/CHANGELOG</code>. Full API documentation can be found at <code>doc/app/index.html</code>, once you have run <code>rake appdoc</code></p>
<p>There are two methods of downloading Tracks 1.6:</p>
<p>There are two methods of downloading Tracks 1.7:</p>
<ol>
<li>(Recommended for most people) Download the <a href="http://www.rousette.org.uk/projects/files/tracks-current.zip">zipped package</a>, and unzip in your preferred location (e.g. <code>~/Sites</code> for Mac OS X users).</li>
@ -62,7 +60,7 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<ol>
<li>Tracks itself</li>
<li>Rails 2.0.2 (installed in the <code>/vendor/rails</code> directory, so you do not need to install Rails yourself)</li>
<li>Rails 2.2.2 (installed in the <code>/vendor/rails</code> directory, so you do not need to install Rails yourself)</li>
<li>An empty SQLite3 database, set up with the correct database schema</li>
</ol>
@ -76,19 +74,19 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<li><strong>Database</strong>. The easiest option is to use SQLite3, as the database is included in the package. All you need then is the <code>sqlite3-ruby</code> gem, as described in step 2, and the SQLite3 libraries and binary (see <a href="http://sqlite.org/download.html">sqlite.org</a> for downloads and installation instructions). If you want to use MySQL, download and install a package for your platform from <a href="http://dev.mysql.com/downloads/mysql/5.0.html">MySQL.com</a>. The basic steps for Postgresql should be similar to those for MySQL, but they will not be discussed further here.</li>
</ol>
<p>If you are using Unix, you might find <a href="http://www.cooldown.com.ar/2006/12/16/install-tracks-on-ubuntu-or-debian/">this guide</a> by c00i90wn helpful. It was written for Tracks 1.043, but it should work for Tracks 1.6.</p>
<p>You can find several installation howtos for specific setups <a href="http://dev.rousette.org.uk/wiki/Tracks/Install">here</a>. They were contributed by various Tracks users.</p>
<h3 id="installation">Installation</h3>
<p>This description is intended for people installing Tracks from scratch. If you would like to upgrade an existing installation, please see <a href="#upgrading" title="Upgrading to Tracks 1.6">Upgrading to Tracks 1.6</a>.</p>
<p>This description is intended for people installing Tracks from scratch. If you would like to upgrade an existing installation, please see <a href="#upgrading" title="Upgrading to Tracks 1.7">Upgrading to Tracks 1.7</a>.</p>
<ol>
<li><a href="#unzip_install" title="Unzip Tracks and install">Unzip tracks</a> and install in a directory</li>
<li>Decide on a <a href="#database_install" title="Decide on a database">database</a> to use
<ol><li>SQLite3 - change database.yml to point to SQLite3 database</li>
<ol><li>SQLite3 - change database.yml to point to SQLite3 database. Make sure you add the complete path to the database</li>
<li>MySQL - create new MySQL db and grant all privileges </li></ol></li>
<li><a href="#config_install" title="Configure variables">Configure some variables</a></li>
<li>Populate the database with the <a href="#rake_install" title="Populate your database with the Tracks 1.6 schema">Tracks 1.6 schema</a></li>
<li>Populate the database with the <a href="#rake_install" title="Populate your database with the Tracks 1.7 schema">Tracks 1.7 schema</a></li>
<li><a href="#startserver_install" title="Start the server">Start the server</a></li>
<li><a href="#signup_install" title="Visit Tracks in a browser">Visit Tracks in a browser</a></li>
<li><a href="#customise_install" title="Customise Tracks">Customise Tracks</a></li>
@ -104,14 +102,14 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<ol>
<li><strong>SQLite3</strong>. All you need to do is make sure that you point Tracks to the included SQLite3 database in <code>/db</code> in the next step, <a href="#config_install" title="Configure variables">Configure variables</a>.</li>
<li><strong>MySQL</strong>. Once you have MySQL installed, you need to create a database to use with Tracks 1.6. Go into a terminal and issue the following commands:</li>
<li><strong>MySQL</strong>. Once you have MySQL installed, you need to create a database and database-user to use with Tracks 1.7. For this, you can use MySQL Administrator or go into a terminal and issue the following commands:</li>
</ol>
<pre>
<code>
mysql -uroot -p
mysql> CREATE DATABASE tracks15;
mysql> GRANT ALL PRIVILEGES ON tracks15.* TO yourmysqluser@localhost \
mysql> CREATE DATABASE tracks16;
mysql> GRANT ALL PRIVILEGES ON tracks16.* TO yourmysqluser@localhost \
IDENTIFIED BY 'password-goes-here' WITH GRANT OPTION;
</code>
</pre>
@ -119,23 +117,23 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<h4 id="config_install">Configure variables</h4>
<ol>
<li>If you downloaded Tracks 1.6 via Subversion, you need to duplicate the files <code>database.yml.tmpl</code> and <code>environment.yml.tmpl</code> and remove the <code>*.tmpl</code> extension from the duplicates. Similarly, duplicate <code>/log.tmpl</code> and remove the <code>*.tmpl</code> extension, then edit the files as described in steps 2 and 3.</li>
<li>If you downloaded Tracks 1.7 via Subversion, you need to duplicate the files <code>database.yml.tmpl</code> and <code>environment.yml.tmpl</code> and remove the <code>*.tmpl</code> extension from the duplicates. Similarly, duplicate <code>/log.tmpl</code> and remove the <code>*.tmpl</code> extension, then edit the files as described in steps 2 and 3.</li>
<li>Open the file <code>/config/database.yml</code> and edit the <code>production:</code> section with the details of your database. If you are using MySQL the <code>adapter:</code> line should read <code>adapter: mysql</code>, <code>host: localhost</code> (in the majority of cases), and your username and password should match those you assigned when you created the database. If you are using SQLite3, you should have only two lines under the production section: <code>adapter: sqlite3</code> and <code>database: db/tracks-15-blank.db</code>. If you downloaded the zipped file, the database.yml file is already configured to use the provided SQLite3 file.</li>
<li>Open the file <code>/config/environment.rb</code>, and read through the settings to make sure that they suit your setup. In most cases, all you need to change is the <code>SALT = "change-me"</code> line (change the string &#8220;change-me&#8221; to some other string of your choice), and the time zone setting.</li>
<li>Open the file <code>/config/environment.rb</code>, and read through the settings to make sure that they suit your setup. In most cases, all you need to change is the <code>SALT = "change-me"</code> line (change the string &#8220;change-me&#8221; to some other string of your choice), and the time zone setting. For the time zone setting, most people will only want to change the config.time_zone option and leave the timezone to use in your database to :utc</li>
<li>If you are using Windows, you may need to check the &#8216;shebang&#8217; lines (<code>#!/usr/bin/env ruby</code>) of the <code>/public/dispatch.*</code> files and all the files in the <code>/script</code> directory. They are set to <code>#!/usr/bin/env ruby</code> by default. This should work for all *nix based setups (Linux or Mac OS X), but Windows users will probably have to change it to something like <code>#c:/ruby/bin/ruby</code> to point to the Ruby binary on your system.</li>
</ol>
<h4 id="rake_install">Populate your database with the Tracks 1.6 schema</h4>
<h4 id="rake_install">Populate your database with the Tracks 1.7 schema</h4>
<p>Open a terminal and change into the root of your Tracks 1.6 directory. Enter the following command:</p>
<p>Open a terminal and change into the root of your Tracks 1.7 directory. Enter the following command:</p>
<p><code>rake db:migrate RAILS_ENV=production</code></p>
<p>This will update your database with the required schema for Tracks 1.6. If you are using SQLite3, it is not strictly necessary, because the SQLite3 database included with Tracks already has the schema included in it, but it should not do any harm to run the command (nothing will happen if it is up to date).</p>
<p>This will update your database with the required schema for Tracks 1.7. If you are using SQLite3, it is not strictly necessary, because the SQLite3 database included with Tracks already has the schema included in it, but it should not do any harm to run the command (nothing will happen if it is up to date).</p>
<h4 id="startserver_install">Start the server</h4>
<p>While still in the Terminal inside the Tracks 1.6 root directory, issue the following command:</p>
<p>While still in the Terminal inside the Tracks 1.7 root directory, issue the following command:</p>
<p><code>script/server -e production</code></p>
@ -149,7 +147,21 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<p>Once logged in, add some Contexts and Projects, and then go ahead and add your actions. You might also want to visit the Preferences page to edit various settings to your liking. Have fun!</p>
<h2 id="upgrading">Upgrading to Tracks 1.6</h2>
<h2 id="upgrading">Upgrading to Tracks 1.7</h2>
<h3 id="upgrading_1.6">Upgrading from Tracks 1.6</h3>
<p>You will need to upgrade your <code>config/environment.rb</code> with the new content from <code>config/environment.rb.tmpl</code> included in 1.7, as the format of this file has changed a bit between 1.6 and 1.7. Also, there were some database changes in Tracks 1.7, so you need to migrate to them.</p>
<ol>
<li><a href="#backup_upgrade" title="Backing up">Back up</a> your existing database and installation of Tracks</li>
<li><a href="#install_upgrade" title="Install Tracks 1.7">Install Tracks 1.7</a> in a new directory</li>
<li><a href="#config_upgrade" title="Copy over old configuration files">Copy over</a> a few configuration files from your Tracks 1.6 directory. If using SQLite3, copy the old database into the new Tracks 1.7 directory.</li>
<li>Rebuild your environment.rb from the updated environment.rb.tmpl</li>
<li>Run <code>rake db:migrate RAILS_ENV=production</code> to <a href="#rake_upgrade" title="Update your old database to the new format">update your old database</a> to the new schema &#8211; you did back up your database didn&#8217;t you?</li>
<li>Run <code>script/server</code> inside your Tracks 1.7 directory to <a href="#startserver_upgrade" title="Start the server">start up Tracks 1.7</a>.</li>
<li>Once you are happy that everything is working well, <a href="#cleanup_upgrade" title="Clean up your old installation">delete your old Tracks directory</a>.</li>
</ol>
<h3 id="upgrading_1.5">Upgrading from Tracks 1.5</h3>
@ -157,8 +169,9 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<ol>
<li><a href="#backup_upgrade" title="Backing up">Back up</a> your existing database and installation of Tracks</li>
<li><a href="#install_upgrade" title="Install Tracks 1.6">Install Tracks 1.6</a> in a new directory</li>
<li><a href="#install_upgrade" title="Install Tracks 1.7">Install Tracks 1.6</a> in a new directory</li>
<li><a href="#config_upgrade" title="Copy over old configuration files">Copy over</a> a few configuration files from your Tracks 1.043 directory. If using SQLite3, copy the old database into the new Tracks 1.6 directory</li>
<li>Rebuild your environment.rb from the updated environment.rb.tmpl</li>
<li>Run <code>script/server</code> inside your Tracks 1.6 directory to <a href="#startserver_upgrade" title="Start the server">start up Tracks 1.6</a>.</li>
<li>Once you are happy that everything is working well, <a href="#cleanup_upgrade" title="Clean up your old installation">delete your old Tracks directory</a>.</li>
</ol>
@ -169,7 +182,7 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<ol>
<li><a href="#backup_upgrade" title="Backing up">Back up</a> your existing database and installation of Tracks</li>
<li><a href="#install_upgrade" title="Install Tracks 1.6">Install Tracks 1.6</a> in a new directory</li>
<li><a href="#install_upgrade" title="Install Tracks 1.7">Install Tracks 1.6</a> in a new directory</li>
<li><a href="#config_upgrade" title="Copy over old configuration files">Copy over</a> a few configuration files from your Tracks 1.043 directory. If using SQLite3, copy the old database into the new Tracks 1.6 directory</li>
<li>Run <code>rake db:migrate RAILS_ENV=production</code> to <a href="#rake_upgrade" title="Update your old database to the new format">update your old database</a> to the new schema &#8211; you did back up your database didn&#8217;t you?</li>
<li>Run <code>script/server</code> inside your Tracks 1.6 directory to <a href="#startserver_upgrade" title="Start the server">start up Tracks 1.6</a>.</li>
@ -182,11 +195,11 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<p><code>mysqldump -user [user name] -password=[password] [database name] &gt; [dump file]</code></p>
<p>Rename your old Tracks installation (e.g. to &#8216;tracks-old&#8217;) so that you can install Tracks 1.6 along side it.</p>
<p>Rename your old Tracks installation (e.g. to &#8216;tracks-old&#8217;) so that you can install Tracks 1.7 along side it.</p>
<h4 id="install_upgrade">Install Tracks 1.6</h4>
<h4 id="install_upgrade">Install Tracks 1.7</h4>
<p>There are two methods of downloading Tracks 1.6:</p>
<p>There are two methods of downloading Tracks 1.7:</p>
<ol>
<li>(Recommended for most people) Download the <a href="http://www.rousette.org.uk/projects/files/tracks-current.zip">zipped package</a>, and unzip in your preferred location (e.g. <code>~/Sites</code> for Mac OS X users).</li>
@ -208,7 +221,7 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<ol>
<li>Copy <code>/config/database.yml</code> from your old Tracks directory to the same location in the new one. Double check that the information there is still correct.</li>
<li>Duplicate <code>/config/environment.rb.tmpl</code> in the Tracks 1.6 directory, and rename the file to <code>environment.rb</code>. Open the file and alter the line <code>SALT = "change-me"</code> so that it matches what you had in this file in your old installation. You may also want to change the time zone setting as appropriate for your location (<code>ENV['TZ'] = 'US/Eastern'</code>). If you have made any other customisations to <code>environment.rb</code> in the past, copy those over, but the contents of the file have changed quite a lot since 1.043, so check it carefully.</li>
<li>Duplicate <code>/config/environment.rb.tmpl</code> in the Tracks 1.7 directory, and rename the file to <code>environment.rb</code>. Open the file and alter the line <code>SALT = "change-me"</code> so that it matches what you had in this file in your old installation. You may also want to change the time zone setting as appropriate for your location. If you have made any other customisations to <code>environment.rb</code> in the past, copy those over, but the contents of the file have slightly changed since 1.5, so check it carefully.</li>
<li>Copy your <code>/log</code> directory over from your old installation to the root of the new one, or just rename <code>/log.tmpl</code> to <code>log</code> to start afresh.</li>
<li>If you are using SQLite3, copy your database from <code>/db</code> in your old Tracks directory to the same location in the new one.</li>
<li>If you are using Windows, you may need to check the &#8216;shebang&#8217; lines (<code>#!/usr/bin/env ruby</code>)<a href="#fn:env" id="fnref:env" class="footnote">1</a> of the <code>/public/dispatch.*</code> files and all the files in the <code>/script</code> directory. They are set to <code>#!/usr/bin/env ruby</code> by default. Check the format of those lines in your old installation, and change the new ones as necessary.</li>
@ -216,7 +229,7 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<h4 id="rake_upgrade">Update your old database to the new format</h4>
<p>In a terminal, change directories so that you are inside the Tracks 1.6 directory. Then issue the command to update your Tracks 1.043 database to the format required for Tracks 1.6:</p>
<p>In a terminal, change directories so that you are inside the Tracks 1.7 directory. Then issue the command to update your Tracks 1.6 database to the format required for Tracks 1.7:</p>
<p><code>rake db:migrate RAILS_ENV=production</code></p>
@ -224,7 +237,7 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<h4 id="startserver_upgrade">Start the server</h4>
<p>If you&#8217;re still in the Tracks 1.6 root directory in a terminal, enter the following command to start up Tracks in production mode:</p>
<p>If you&#8217;re still in the Tracks 1.7 root directory in a terminal, enter the following command to start up Tracks in production mode:</p>
<p><code>script/server -e production</code></p>
@ -233,7 +246,7 @@ http://creativecommons.org/licenses/by-nc-sa/3.0/" />
<h4 id="cleanup_upgrade">Clean up your old installation</h4>
<p>Once you&#8217;re certain that your new Tracks 1.6 installation is working perfectly, you can delete your old Tracks directory.</p>
<p>Once you&#8217;re certain that your new Tracks 1.7 installation is working perfectly, you can delete your old Tracks directory.</p>
<h3 id="upgradingfromversionspriorto1.043">Upgrading from versions prior to 1.043</h3>

View file

@ -1,8 +1,8 @@
Title: Tracks 1.6 Manual
Title: Tracks 1.7 Manual
Author: Tracks Development Team
Date: 2008-04-07
Revision: $Id: manual.markdown 864 2008-06-03 17:01:00Z bsag $
Version: 1.6
Date: 2008-12-14
Revision: $Id: manual.markdown 2008-12-14 11:50:00Z bsag $
Version: 1.7
Copyright: 2008 rousette.org.uk
This work is licensed under a Creative Commons License.
http://creativecommons.org/licenses/by-nc-sa/3.0/
@ -14,15 +14,13 @@ CSS: manual.css
<!-- The HTML file manual.html is generated from manual.markdown, so make edits to the *.markdown file -->
# Installing Tracks 1.6 #
# Installing Tracks 1.7 #
## Introduction ##
**An important note for version 1.6: OpenID support is broken in this release. The fix isn't trivial because of changes to the `ruby-openid` gem, so we wanted to get this version out now and fix OpenID for the next release. If you depend on OpenID integration, we recommend waiting until the next release.**
Tracks 1.7 has been thoroughly beta tested by a large number of people, and should be fully stable for everyday use. However, once set up, Tracks will contain the majority of your plans for your work and personal life, so it's only sensible to make sure that you have frequent, reliable backups of your data. Full changenotes on the release can be found in `doc/CHANGELOG`. Full API documentation can be found at `doc/app/index.html`, once you have run `rake appdoc`
Tracks 1.6 has been thoroughly beta tested by a large number of people, and should be fully stable for everyday use. However, once set up, Tracks will contain the majority of your plans for your work and personal life, so it's only sensible to make sure that you have frequent, reliable backups of your data. Full changenotes on the release can be found in `doc/CHANGELOG`. Full API documentation can be found at `doc/app/index.html`, once you have run `rake appdoc`
There are two methods of downloading Tracks 1.6:
There are two methods of downloading Tracks 1.7:
1. (Recommended for most people) Download the [zipped package](http://www.rousette.org.uk/projects/files/tracks-current.zip), and unzip in your preferred location (e.g. `~/Sites` for Mac OS X users).
2. If you want to live on the edge, you can get the latest development version from GitHub using git (bear in mind that this may be less stable than the released versions):
@ -50,7 +48,7 @@ If you'd like an easy way to access Tracks from any internet-connected computer,
### What is included with the Tracks package ###
1. Tracks itself
2. Rails 2.0.2 (installed in the `/vendor/rails` directory, so you do not need to install Rails yourself)
2. Rails 2.2.2 (installed in the `/vendor/rails` directory, so you do not need to install Rails yourself)
3. An empty SQLite3 database, set up with the correct database schema
### What you need to install [whatyouneed] ###
@ -61,18 +59,18 @@ If you don't want to (or can't) use one of the all in one installations, you'll
2. **RubyGems**. The gems needed by Rails to interact with the database have to be compiled on the platform on which they will be run, so we cannot include them with the Tracks package, unlike some other gems. So you will need to [download](http://rubyforge.org/frs/?group_id=126) and install RubyGems (run `ruby setup.rb` after extracting the package). Note that once again, Mac OS X Leopard users get an easy life, because RubyGems and the SQLite3 gem is already installed. Once installed you can use RubyGems to install the gems you need for your database. If you are using SQLite3, run `sudo gem install sqlite3-ruby`, then select the appropriate package for your platform (version 1.2.1 recommended). You can use MySQL without installing a gem, but installing the gem can speed things up a bit: `sudo install gem mysql`. If you're using Leopard, there are a few work-arounds necessary, which are explained on [Mac OS Forge](http://trac.macosforge.org/projects/ruby/wiki/Troubleshooting#IcannotbuildrubymysqlonLeopardwithmysql.combinaries). The ruby-mysql bindings can sometimes be a bit troublesome to install, so to be honest, it's probably not worth the bother unless you are trying to wring maximum speed out of your system. If you are using PostgreSQL, then you can install a postgres gem: `gem install postgres`.
3. **Database**. The easiest option is to use SQLite3, as the database is included in the package. All you need then is the `sqlite3-ruby` gem, as described in step 2, and the SQLite3 libraries and binary (see [sqlite.org](http://sqlite.org/download.html) for downloads and installation instructions). If you want to use MySQL, download and install a package for your platform from [MySQL.com](http://dev.mysql.com/downloads/mysql/5.0.html). The basic steps for Postgresql should be similar to those for MySQL, but they will not be discussed further here.
If you are using Unix, you might find [this guide](http://www.cooldown.com.ar/2006/12/16/install-tracks-on-ubuntu-or-debian/) by c00i90wn helpful. It was written for Tracks 1.043, but it should work for Tracks 1.6.
You can find several installation howtos for specific setups [here](http://dev.rousette.org.uk/wiki/Tracks/Install). They were contributed by various Tracks users.
## Installation ##
This description is intended for people installing Tracks from scratch. If you would like to upgrade an existing installation, please see [Upgrading to Tracks 1.6][upgrading].
This description is intended for people installing Tracks from scratch. If you would like to upgrade an existing installation, please see [Upgrading to Tracks 1.7][upgrading].
1. [Unzip tracks][unzip_install] and install in a directory
2. Decide on a [database][database_install] to use
1. SQLite3 - change database.yml to point to SQLite3 database
1. SQLite3 - change database.yml to point to SQLite3 database. Make sure you add the complete path to the database
2. MySQL - create new MySQL db and grant all privileges
3. [Configure some variables][config_install]
4. Populate the database with the [Tracks 1.6 schema][rake_install]
4. Populate the database with the [Tracks 1.7 schema][rake_install]
5. [Start the server][startserver_install]
6. [Visit Tracks in a browser][signup_install]
7. [Customise Tracks][customise_install]
@ -86,34 +84,34 @@ Unzip the package and move Tracks into the directory you want to run it from. Fo
Before you go any further, you need to decide which database you will use. See the [What you need to install][whatyouneed] section for details on installing the required components for you choice of database.
1. **SQLite3**. All you need to do is make sure that you point Tracks to the included SQLite3 database in `/db` in the next step, [Configure variables][config_install].
2. **MySQL**. Once you have MySQL installed, you need to create a database to use with Tracks 1.6. Go into a terminal and issue the following commands:
2. **MySQL**. Once you have MySQL installed, you need to create a database and database-user to use with Tracks 1.7. For this, you can use MySQL Administrator or go into a terminal and issue the following commands:
<pre>
<code>
mysql -uroot -p
mysql> CREATE DATABASE tracks15;
mysql> GRANT ALL PRIVILEGES ON tracks15.* TO yourmysqluser@localhost \
mysql> CREATE DATABASE tracks16;
mysql> GRANT ALL PRIVILEGES ON tracks16.* TO yourmysqluser@localhost \
IDENTIFIED BY 'password-goes-here' WITH GRANT OPTION;
</code>
</pre>
### Configure variables [config_install] ###
1. If you downloaded Tracks 1.6 via Subversion, you need to duplicate the files `database.yml.tmpl` and `environment.yml.tmpl` and remove the `*.tmpl` extension from the duplicates. Similarly, duplicate `/log.tmpl` and remove the `*.tmpl` extension, then edit the files as described in steps 2 and 3.
1. If you downloaded Tracks 1.7 via Subversion, you need to duplicate the files `database.yml.tmpl` and `environment.yml.tmpl` and remove the `*.tmpl` extension from the duplicates. Similarly, duplicate `/log.tmpl` and remove the `*.tmpl` extension, then edit the files as described in steps 2 and 3.
2. Open the file `/config/database.yml` and edit the `production:` section with the details of your database. If you are using MySQL the `adapter:` line should read `adapter: mysql`, `host: localhost` (in the majority of cases), and your username and password should match those you assigned when you created the database. If you are using SQLite3, you should have only two lines under the production section: `adapter: sqlite3` and `database: db/tracks-15-blank.db`. If you downloaded the zipped file, the database.yml file is already configured to use the provided SQLite3 file.
3. Open the file `/config/environment.rb`, and read through the settings to make sure that they suit your setup. In most cases, all you need to change is the `SALT = "change-me"` line (change the string "change-me" to some other string of your choice), and the time zone setting.
3. Open the file `/config/environment.rb`, and read through the settings to make sure that they suit your setup. In most cases, all you need to change is the `SALT = "change-me"` line (change the string "change-me" to some other string of your choice), and the time zone setting. For the time zone setting, most people will only want to change the config.time_zone option and leave the timezone to use in your database to :utc
4. If you are using Windows, you may need to check the 'shebang' lines (`#!/usr/bin/env ruby`) of the `/public/dispatch.*` files and all the files in the `/script` directory. They are set to `#!/usr/bin/env ruby` by default. This should work for all *nix based setups (Linux or Mac OS X), but Windows users will probably have to change it to something like `#c:/ruby/bin/ruby` to point to the Ruby binary on your system.
### Populate your database with the Tracks 1.6 schema [rake_install] ###
### Populate your database with the Tracks 1.7 schema [rake_install] ###
Open a terminal and change into the root of your Tracks 1.6 directory. Enter the following command:
Open a terminal and change into the root of your Tracks 1.7 directory. Enter the following command:
`rake db:migrate RAILS_ENV=production`
This will update your database with the required schema for Tracks 1.6. If you are using SQLite3, it is not strictly necessary, because the SQLite3 database included with Tracks already has the schema included in it, but it should not do any harm to run the command (nothing will happen if it is up to date).
This will update your database with the required schema for Tracks 1.7. If you are using SQLite3, it is not strictly necessary, because the SQLite3 database included with Tracks already has the schema included in it, but it should not do any harm to run the command (nothing will happen if it is up to date).
### Start the server [startserver_install] ###
While still in the Terminal inside the Tracks 1.6 root directory, issue the following command:
While still in the Terminal inside the Tracks 1.7 root directory, issue the following command:
`script/server -e production`
@ -127,7 +125,19 @@ Visit `http://0.0.0.0:3000/signup` in a browser (or whatever URL and port was re
Once logged in, add some Contexts and Projects, and then go ahead and add your actions. You might also want to visit the Preferences page to edit various settings to your liking. Have fun!
# Upgrading to Tracks 1.6 [upgrading] #
# Upgrading to Tracks 1.7 [upgrading] #
## Upgrading from Tracks 1.6 [upgrading_1.6] ##
You will need to upgrade your `config/environment.rb` with the new content from `config/environment.rb.tmpl` included in 1.7, as the format of this file has changed a bit between 1.6 and 1.7. Also, there were some database changes in Tracks 1.7, so you need to migrate to them.
1. [Back up][backup_upgrade] your existing database and installation of Tracks
2. [Install Tracks 1.7][install_upgrade] in a new directory
3. [Copy over][config_upgrade] a few configuration files from your Tracks 1.6 directory. If using SQLite3, copy the old database into the new Tracks 1.7 directory.
4. Rebuild your environment.rb from the updated environment.rb.tmpl
5. Run `rake db:migrate RAILS_ENV=production` to [update your old database][rake_upgrade] to the new schema -- you did back up your database didn't you?
6. Run `script/server` inside your Tracks 1.7 directory to [start up Tracks 1.7][startserver_upgrade].
7. Once you are happy that everything is working well, [delete your old Tracks directory][cleanup_upgrade].
## Upgrading from Tracks 1.5 [upgrading_1.5] ##
@ -136,9 +146,9 @@ There are no changes to the database between 1.5 and 1.6, but you will need to u
1. [Back up][backup_upgrade] your existing database and installation of Tracks
2. [Install Tracks 1.6][install_upgrade] in a new directory
3. [Copy over][config_upgrade] a few configuration files from your Tracks 1.043 directory. If using SQLite3, copy the old database into the new Tracks 1.6 directory
4. Run `script/server` inside your Tracks 1.6 directory to [start up Tracks 1.6][startserver_upgrade].
5. Once you are happy that everything is working well, [delete your old Tracks directory][cleanup_upgrade].
4. Rebuild your environment.rb from the updated environment.rb.tmpl
5. Run `script/server` inside your Tracks 1.6 directory to [start up Tracks 1.6][startserver_upgrade].
6. Once you are happy that everything is working well, [delete your old Tracks directory][cleanup_upgrade].
## Upgrading from Tracks 1.043 [upgrading_1043] ##
@ -157,11 +167,11 @@ It's very important that you **back up your database** before you start the upgr
`mysqldump -user [user name] -password=[password] [database name] > [dump file]`
Rename your old Tracks installation (e.g. to 'tracks-old') so that you can install Tracks 1.6 along side it.
Rename your old Tracks installation (e.g. to 'tracks-old') so that you can install Tracks 1.7 along side it.
### Install Tracks 1.6 [install_upgrade] ###
### Install Tracks 1.7 [install_upgrade] ###
There are two methods of downloading Tracks 1.6:
There are two methods of downloading Tracks 1.7:
1. (Recommended for most people) Download the [zipped package](http://www.rousette.org.uk/projects/files/tracks-current.zip), and unzip in your preferred location (e.g. `~/Sites` for Mac OS X users).
2. If you want to live on the edge, you can get the latest development version from GitHub using git (bear in mind that this may be less stable than the released versions):
@ -179,14 +189,14 @@ There are two methods of downloading Tracks 1.6:
There are a few files you need to copy over from your old installation. If you copy them over rather than moving them, you can still run your old version of Tracks if anything goes awry with the installation process.
1. Copy `/config/database.yml` from your old Tracks directory to the same location in the new one. Double check that the information there is still correct.
2. Duplicate `/config/environment.rb.tmpl` in the Tracks 1.6 directory, and rename the file to `environment.rb`. Open the file and alter the line `SALT = "change-me"` so that it matches what you had in this file in your old installation. You may also want to change the time zone setting as appropriate for your location (`ENV['TZ'] = 'US/Eastern'`). If you have made any other customisations to `environment.rb` in the past, copy those over, but the contents of the file have changed quite a lot since 1.043, so check it carefully.
2. Duplicate `/config/environment.rb.tmpl` in the Tracks 1.7 directory, and rename the file to `environment.rb`. Open the file and alter the line `SALT = "change-me"` so that it matches what you had in this file in your old installation. You may also want to change the time zone setting as appropriate for your location. If you have made any other customisations to `environment.rb` in the past, copy those over, but the contents of the file have slightly changed since 1.5, so check it carefully.
3. Copy your `/log` directory over from your old installation to the root of the new one, or just rename `/log.tmpl` to `log` to start afresh.
4. If you are using SQLite3, copy your database from `/db` in your old Tracks directory to the same location in the new one.
5. If you are using Windows, you may need to check the 'shebang' lines (`#!/usr/bin/env ruby`)[^env] of the `/public/dispatch.*` files and all the files in the `/script` directory. They are set to `#!/usr/bin/env ruby` by default. Check the format of those lines in your old installation, and change the new ones as necessary.
### Update your old database to the new format [rake_upgrade] ###
In a terminal, change directories so that you are inside the Tracks 1.6 directory. Then issue the command to update your Tracks 1.043 database to the format required for Tracks 1.6:
In a terminal, change directories so that you are inside the Tracks 1.7 directory. Then issue the command to update your Tracks 1.6 database to the format required for Tracks 1.7:
`rake db:migrate RAILS_ENV=production`
@ -194,7 +204,7 @@ Watch the output carefully for errors, but it should report at the end of the pr
### Start the server [startserver_upgrade] ###
If you're still in the Tracks 1.6 root directory in a terminal, enter the following command to start up Tracks in production mode:
If you're still in the Tracks 1.7 root directory in a terminal, enter the following command to start up Tracks in production mode:
`script/server -e production`
@ -203,7 +213,7 @@ Visit the URL indicated by the output (e.g. `** Mongrel available at 0.0.0.0:300
### Clean up your old installation [cleanup_upgrade] ###
Once you're certain that your new Tracks 1.6 installation is working perfectly, you can delete your old Tracks directory.
Once you're certain that your new Tracks 1.7 installation is working perfectly, you can delete your old Tracks directory.
[^env]: The `env` binary helps to locate other binaries, regardless of their location. If you don't have `env` installed, you'll need to change this line to point to the location of your Ruby binary.

Binary file not shown.

View file

@ -112,12 +112,12 @@
\usepackage{booktabs} % Better tables
\usepackage{tabulary} % Support longer table cells
\usepackage[utf8]{inputenc} % For UTF-8 support
% \usepackage{xcolor} % Allow for color (annotations)
%
% %\geometry{landscape} % Activate for rotated page geometry
%
% \usepackage[parfill]{parskip} % Activate to begin paragraphs with an empty
% % line rather than an indent
%\usepackage{xcolor} % Allow for color (annotations)
%\geometry{landscape} % Activate for rotated page geometry
%\usepackage[parfill]{parskip} % Activate to begin paragraphs with an empty
% line rather than an indent
\def\myauthor{Author} % In case these were not included in metadata
@ -130,14 +130,14 @@
\def\myauthor{Tracks Development Team}
\def\baseheaderlevel{2}
\def\mycopyright{2008 rousette.org.uk \\ This work is licensed under a Creative Commons License. \\ http://creativecommons.org/licenses/by-nc-sa/3.0/}
\date{2008-04-07}
\date{2008-12-14}
\def\format{complete}
\def\latexxslt{memoir-twosided-manual.xslt}
\def\revision{Revision: \$Id: manual.markdown 864 2008-06-03 17:01:00Z bsag \$}
\def\mytitle{Tracks 1.6 Manual}
\def\version{1.6}
% \usepackage{xmpincl}
% \includexmp{CCAttributionShareAlike}
\def\revision{Revision: \$Id: manual.markdown 2008-12-14 11:50:00Z bsag \$}
\def\mytitle{Tracks 1.7 Manual}
\def\version{1.7}
\usepackage{xmpincl}
\includexmp{CCAttributionShareAlike}
%
@ -254,19 +254,16 @@
\setlength{\parskip}{\baselineskip/2}
\mainmatter
\chapter{Installing Tracks 1.6}
\label{installingtracks1.6}
\chapter{Installing Tracks 1.7}
\label{installingtracks1.7}
\section{Introduction}
\label{introduction}
\textbf{An important note for version 1.6: OpenID support is broken in this release. The fix isn't trivial because of changes to the \texttt{ruby-openid} gem, so we wanted to get this version out now and fix OpenID for the next release. If you depend on OpenID integration, we recommend waiting until the next release.}
Tracks 1.7 has been thoroughly beta tested by a large number of people, and should be fully stable for everyday use. However, once set up, Tracks will contain the majority of your plans for your work and personal life, so it's only sensible to make sure that you have frequent, reliable backups of your data. Full changenotes on the release can be found in \texttt{doc/CHANGELOG}. Full API documentation can be found at \texttt{doc/app/index.html}, once you have run \texttt{rake appdoc}
Tracks 1.6 has been thoroughly beta tested by a large number of people, and should be fully stable for everyday use. However, once set up, Tracks will contain the majority of your plans for your work and personal life, so it's only sensible to make sure that you have frequent, reliable backups of your data. Full changenotes on the release can be found in \texttt{doc/CHANGELOG}. Full API documentation can be found at \texttt{doc/app/index.html}, once you have run \texttt{rake appdoc}
There are two methods of downloading Tracks 1.6:
There are two methods of downloading Tracks 1.7:
\begin{enumerate}
@ -315,7 +312,7 @@ If you'd like an easy way to access Tracks from any internet-connected computer,
\item Tracks itself
\item Rails 2.0.2 (installed in the \texttt{/vendor/rails} directory, so you do not need to install Rails yourself)
\item Rails 2.2.2 (installed in the \texttt{/vendor/rails} directory, so you do not need to install Rails yourself)
\item An empty SQLite3 database, set up with the correct database schema
\end{enumerate}
@ -336,13 +333,13 @@ If you don't want to (or can't) use one of the all in one installations, you'll
\item \textbf{Database}. The easiest option is to use SQLite3, as the database is included in the package. All you need then is the \texttt{sqlite3-ruby} gem, as described in step 2, and the SQLite3 libraries and binary (see \href{http://sqlite.org/download.html}{sqlite.org} for downloads and installation instructions). If you want to use MySQL, download and install a package for your platform from \href{http://dev.mysql.com/downloads/mysql/5.0.html}{MySQL.com}. The basic steps for Postgresql should be similar to those for MySQL, but they will not be discussed further here.
\end{enumerate}
If you are using Unix, you might find \href{http://www.cooldown.com.ar/2006/12/16/install-tracks-on-ubuntu-or-debian/}{this guide} by c00i90wn helpful. It was written for Tracks 1.043, but it should work for Tracks 1.6.
You can find several installation howtos for specific setups \href{http://dev.rousette.org.uk/wiki/Tracks/Install}{here}. They were contributed by various Tracks users.
\section{Installation}
\label{installation}
This description is intended for people installing Tracks from scratch. If you would like to upgrade an existing installation, please see Upgrading to Tracks 1.6 (\autoref{upgrading}).
This description is intended for people installing Tracks from scratch. If you would like to upgrade an existing installation, please see Upgrading to Tracks 1.7 (\autoref{upgrading}).
\begin{enumerate}
@ -354,7 +351,7 @@ This description is intended for people installing Tracks from scratch. If you w
\begin{enumerate}
\item SQLite3 - change database.yml to point to SQLite3 database
\item SQLite3 - change database.yml to point to SQLite3 database. Make sure you add the complete path to the database
\item MySQL - create new MySQL db and grant all privileges
\end{enumerate}
@ -363,7 +360,7 @@ This description is intended for people installing Tracks from scratch. If you w
\item Configure some variables (\autoref{config_install})
\item Populate the database with the Tracks 1.6 schema (\autoref{rake_install})
\item Populate the database with the Tracks 1.7 schema (\autoref{rake_install})
\item Start the server (\autoref{startserver_install})
@ -389,7 +386,7 @@ Before you go any further, you need to decide which database you will use. See t
\item \textbf{SQLite3}. All you need to do is make sure that you point Tracks to the included SQLite3 database in \texttt{/db} in the next step, Configure variables (\autoref{config_install}).
\item \textbf{MySQL}. Once you have MySQL installed, you need to create a database to use with Tracks 1.6. Go into a terminal and issue the following commands:
\item \textbf{MySQL}. Once you have MySQL installed, you need to create a database and database-user to use with Tracks 1.7. For this, you can use MySQL Administrator or go into a terminal and issue the following commands:
\end{enumerate}
\begin{adjustwidth}{2.5em}{2.5em}
@ -397,8 +394,8 @@ Before you go any further, you need to decide which database you will use. See t
mysql -uroot -p
mysql> CREATE DATABASE tracks15;
mysql> GRANT ALL PRIVILEGES ON tracks15.* TO yourmysqluser@localhost \
mysql> CREATE DATABASE tracks16;
mysql> GRANT ALL PRIVILEGES ON tracks16.* TO yourmysqluser@localhost \
IDENTIFIED BY 'password-goes-here' WITH GRANT OPTION;
@ -411,31 +408,31 @@ Before you go any further, you need to decide which database you will use. See t
\begin{enumerate}
\item If you downloaded Tracks 1.6 via Subversion, you need to duplicate the files \texttt{database.yml.tmpl} and \texttt{environment.yml.tmpl} and remove the \texttt{*.tmpl} extension from the duplicates. Similarly, duplicate \texttt{/log.tmpl} and remove the \texttt{*.tmpl} extension, then edit the files as described in steps 2 and 3.
\item If you downloaded Tracks 1.7 via Subversion, you need to duplicate the files \texttt{database.yml.tmpl} and \texttt{environment.yml.tmpl} and remove the \texttt{*.tmpl} extension from the duplicates. Similarly, duplicate \texttt{/log.tmpl} and remove the \texttt{*.tmpl} extension, then edit the files as described in steps 2 and 3.
\item Open the file \texttt{/config/database.yml} and edit the \texttt{production:} section with the details of your database. If you are using MySQL the \texttt{adapter:} line should read \texttt{adapter: mysql}, \texttt{host: localhost} (in the majority of cases), and your username and password should match those you assigned when you created the database. If you are using SQLite3, you should have only two lines under the production section: \texttt{adapter: sqlite3} and \texttt{database: db/tracks-15-blank.db}. If you downloaded the zipped file, the database.yml file is already configured to use the provided SQLite3 file.
\item Open the file \texttt{/config/environment.rb}, and read through the settings to make sure that they suit your setup. In most cases, all you need to change is the \texttt{SALT = "change-me"} line (change the string ``change-me" to some other string of your choice), and the time zone setting.
\item Open the file \texttt{/config/environment.rb}, and read through the settings to make sure that they suit your setup. In most cases, all you need to change is the \texttt{SALT = "change-me"} line (change the string ``change-me" to some other string of your choice), and the time zone setting. For the time zone setting, most people will only want to change the config.time\_zone option and leave the timezone to use in your database to :utc
\item If you are using Windows, you may need to check the `shebang' lines (\texttt{\#!/usr/bin/env ruby}) of the \texttt{/public/dispatch.*} files and all the files in the \texttt{/script} directory. They are set to \texttt{\#!/usr/bin/env ruby} by default. This should work for all *nix based setups (Linux or Mac OS X), but Windows users will probably have to change it to something like \texttt{\#c:/ruby/bin/ruby} to point to the Ruby binary on your system.
\end{enumerate}
\subsection{Populate your database with the Tracks 1.6 schema}
\subsection{Populate your database with the Tracks 1.7 schema}
\label{rake_install}
Open a terminal and change into the root of your Tracks 1.6 directory. Enter the following command:
Open a terminal and change into the root of your Tracks 1.7 directory. Enter the following command:
\texttt{rake db:migrate RAILS\_ENV=production}
This will update your database with the required schema for Tracks 1.6. If you are using SQLite3, it is not strictly necessary, because the SQLite3 database included with Tracks already has the schema included in it, but it should not do any harm to run the command (nothing will happen if it is up to date).
This will update your database with the required schema for Tracks 1.7. If you are using SQLite3, it is not strictly necessary, because the SQLite3 database included with Tracks already has the schema included in it, but it should not do any harm to run the command (nothing will happen if it is up to date).
\subsection{Start the server}
\label{startserver_install}
While still in the Terminal inside the Tracks 1.6 root directory, issue the following command:
While still in the Terminal inside the Tracks 1.7 root directory, issue the following command:
\texttt{script/server -e production}
@ -456,9 +453,33 @@ Visit \texttt{http://0.0.0.0:3000/signup} in a browser (or whatever URL and port
Once logged in, add some Contexts and Projects, and then go ahead and add your actions. You might also want to visit the Preferences page to edit various settings to your liking. Have fun!
\chapter{Upgrading to Tracks 1.6}
\chapter{Upgrading to Tracks 1.7}
\label{upgrading}
\section{Upgrading from Tracks 1.6}
\label{upgrading_1.6}
You will need to upgrade your \texttt{config/environment.rb} with the new content from \texttt{config/environment.rb.tmpl} included in 1.7, as the format of this file has changed a bit between 1.6 and 1.7. Also, there were some database changes in Tracks 1.7, so you need to migrate to them.
\begin{enumerate}
\item Back up (\autoref{backup_upgrade}) your existing database and installation of Tracks
\item Install Tracks 1.7 (\autoref{install_upgrade}) in a new directory
\item Copy over (\autoref{config_upgrade}) a few configuration files from your Tracks 1.6 directory. If using SQLite3, copy the old database into the new Tracks 1.7 directory.
\item Rebuild your environment.rb from the updated environment.rb.tmpl
\item Run \texttt{rake db:migrate RAILS\_ENV=production} to update your old database (\autoref{rake_upgrade}) to the new schema -- you did back up your database didn't you?
\item Run \texttt{script/server} inside your Tracks 1.7 directory to start up Tracks 1.7 (\autoref{startserver_upgrade}).
\item Once you are happy that everything is working well, delete your old Tracks directory (\autoref{cleanup_upgrade}).
\end{enumerate}
\section{Upgrading from Tracks 1.5}
\label{upgrading_1.5}
@ -474,6 +495,8 @@ There are no changes to the database between 1.5 and 1.6, but you will need to u
\item Copy over (\autoref{config_upgrade}) a few configuration files from your Tracks 1.043 directory. If using SQLite3, copy the old database into the new Tracks 1.6 directory
\item Rebuild your environment.rb from the updated environment.rb.tmpl
\item Run \texttt{script/server} inside your Tracks 1.6 directory to start up Tracks 1.6 (\autoref{startserver_upgrade}).
\item Once you are happy that everything is working well, delete your old Tracks directory (\autoref{cleanup_upgrade}).
@ -510,13 +533,13 @@ It's very important that you \textbf{back up your database} before you start the
\texttt{mysqldump ---user [user name] ---password=[password] [database name] $>$ [dump file]}
Rename your old Tracks installation (e.g.\ to `tracks-old') so that you can install Tracks 1.6 along side it.
Rename your old Tracks installation (e.g.\ to `tracks-old') so that you can install Tracks 1.7 along side it.
\subsection{Install Tracks 1.6}
\subsection{Install Tracks 1.7}
\label{install_upgrade}
There are two methods of downloading Tracks 1.6:
There are two methods of downloading Tracks 1.7:
\begin{enumerate}
@ -550,7 +573,7 @@ There are a few files you need to copy over from your old installation. If you c
\item Copy \texttt{/config/database.yml} from your old Tracks directory to the same location in the new one. Double check that the information there is still correct.
\item Duplicate \texttt{/config/environment.rb.tmpl} in the Tracks 1.6 directory, and rename the file to \texttt{environment.rb}. Open the file and alter the line \texttt{SALT = "change-me"} so that it matches what you had in this file in your old installation. You may also want to change the time zone setting as appropriate for your location (\texttt{ENV['TZ'] = 'US/Eastern'}). If you have made any other customisations to \texttt{environment.rb} in the past, copy those over, but the contents of the file have changed quite a lot since 1.043, so check it carefully.
\item Duplicate \texttt{/config/environment.rb.tmpl} in the Tracks 1.7 directory, and rename the file to \texttt{environment.rb}. Open the file and alter the line \texttt{SALT = "change-me"} so that it matches what you had in this file in your old installation. You may also want to change the time zone setting as appropriate for your location. If you have made any other customisations to \texttt{environment.rb} in the past, copy those over, but the contents of the file have slightly changed since 1.5, so check it carefully.
\item Copy your \texttt{/log} directory over from your old installation to the root of the new one, or just rename \texttt{/log.tmpl} to \texttt{log} to start afresh.
@ -562,7 +585,7 @@ There are a few files you need to copy over from your old installation. If you c
\subsection{Update your old database to the new format}
\label{rake_upgrade}
In a terminal, change directories so that you are inside the Tracks 1.6 directory. Then issue the command to update your Tracks 1.043 database to the format required for Tracks 1.6:
In a terminal, change directories so that you are inside the Tracks 1.7 directory. Then issue the command to update your Tracks 1.6 database to the format required for Tracks 1.7:
\texttt{rake db:migrate RAILS\_ENV=production}
@ -574,7 +597,7 @@ Watch the output carefully for errors, but it should report at the end of the pr
\subsection{Start the server}
\label{startserver_upgrade}
If you're still in the Tracks 1.6 root directory in a terminal, enter the following command to start up Tracks in production mode:
If you're still in the Tracks 1.7 root directory in a terminal, enter the following command to start up Tracks in production mode:
\texttt{script/server -e production}
@ -587,7 +610,7 @@ Visit the URL indicated by the output (e.g.\ \texttt{** Mongrel available at 0.0
\subsection{Clean up your old installation}
\label{cleanup_upgrade}
Once you're certain that your new Tracks 1.6 installation is working perfectly, you can delete your old Tracks directory.
Once you're certain that your new Tracks 1.7 installation is working perfectly, you can delete your old Tracks directory.
\section{Upgrading from versions prior to 1.043}

View file

@ -2,7 +2,7 @@ namespace :tracks do
desc 'Replace the password of USER with a new one.'
task :password => :environment do
Dependencies.load_paths.unshift(File.dirname(__FILE__) + "/..../vendor/gems/highline-1.4.0/lib")
Dependencies.load_paths.unshift(File.dirname(__FILE__) + "/../../vendor/gems/highline-1.5.0/lib")
require "highline/import"
user = User.find_by_login(ENV['USER'])

View file

@ -1,10 +1,12 @@
raise "To avoid rake task loading problems: run 'rake clobber' in vendor/plugins/rspec" if File.directory?(File.join(File.dirname(__FILE__), *%w[.. .. vendor plugins rspec pkg]))
raise "To avoid rake task loading problems: run 'rake clobber' in vendor/plugins/rspec-rails" if File.directory?(File.join(File.dirname(__FILE__), *%w[.. .. vendor plugins rspec-rails pkg]))
# In rails 1.2, plugins aren't available in the path until they're loaded.
# Check to see if the rspec plugin is installed first and require
# it if it is. If not, use the gem version.
rspec_base = File.expand_path(File.dirname(__FILE__) + '/../../rspec/lib')
rspec_base = File.expand_path(File.dirname(__FILE__) + '/../../vendor/plugins/rspec/lib')
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
require 'spec/rake/spectask'
require 'spec/translator'
spec_prereq = File.exist?(File.join(RAILS_ROOT, 'config', 'database.yml')) ? "db:test:prepare" : :noop
task :noop do
@ -64,13 +66,6 @@ namespace :spec do
end
end
desc "Translate/upgrade specs using the built-in translator"
task :translate do
translator = ::Spec::Translator.new
dir = RAILS_ROOT + '/spec'
translator.translate(dir, dir)
end
# Setup specs for stats
task :statsetup do
require 'code_statistics'

View file

@ -39,4 +39,4 @@ RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
# Example:
# ErrorDocument 500 /500.html
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

View file

@ -1,22 +1,22 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// Autocompleter.Base handles all the autocompletion functionality
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
@ -30,23 +30,23 @@
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
var Autocompleter = { }
var Autocompleter = { };
Autocompleter.Base = Class.create({
baseInitialize: function(element, update, options) {
element = $(element)
this.element = element;
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
element = $(element);
this.element = element;
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
this.oldElementValue = this.element.value;
@ -59,28 +59,28 @@ Autocompleter.Base = Class.create({
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {
setHeight: false,
setHeight: false,
offsetTop: element.offsetHeight
});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if(typeof(this.options.tokens) == 'string')
if(typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
// Force carriage returns as token delimiters anyway
if (!this.options.tokens.include('\n'))
this.options.tokens.push('\n');
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
@ -91,10 +91,10 @@ Autocompleter.Base = Class.create({
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
if(!this.iefix &&
(Prototype.Browser.IE) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
@ -102,7 +102,7 @@ Autocompleter.Base = Class.create({
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
this.iefix.style.zIndex = 1;
@ -150,15 +150,15 @@ Autocompleter.Base = Class.create({
Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
@ -170,35 +170,35 @@ Autocompleter.Base = Class.create({
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
if(this.hasFocus) {
this.show();
this.active = true;
}
@ -207,27 +207,27 @@ Autocompleter.Base = Class.create({
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
if(this.index > 0) this.index--;
else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(false);
this.getEntry(this.index).scrollIntoView(true);
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
if(this.index < this.entryCount-1) this.index++;
else this.index = 0;
this.getEntry(this.index).scrollIntoView(false);
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
@ -244,7 +244,7 @@ Autocompleter.Base = Class.create({
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var bounds = this.getTokenBounds();
if (bounds[0] != -1) {
var newValue = this.element.value.substr(0, bounds[0]);
@ -257,7 +257,7 @@ Autocompleter.Base = Class.create({
}
this.oldElementValue = this.element.value;
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
@ -269,20 +269,20 @@ Autocompleter.Base = Class.create({
Element.cleanWhitespace(this.update.down());
if(this.update.firstChild && this.update.down().childNodes) {
this.entryCount =
this.entryCount =
this.update.down().childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
if(this.entryCount==1 && this.options.autoSelect) {
this.selectEntry();
this.hide();
@ -298,7 +298,7 @@ Autocompleter.Base = Class.create({
},
onObserverEvent: function() {
this.changed = false;
this.changed = false;
this.tokenBounds = null;
if(this.getToken().length>=this.options.minChars) {
this.getUpdatedChoices();
@ -351,16 +351,16 @@ Ajax.Autocompleter = Class.create(Autocompleter.Base, {
getUpdatedChoices: function() {
this.startIndicator();
var entry = encodeURIComponent(this.options.paramName) + '=' +
var entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
@ -382,7 +382,7 @@ Ajax.Autocompleter = Class.create(Autocompleter.Base, {
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
@ -399,7 +399,7 @@ Ajax.Autocompleter = Class.create(Autocompleter.Base, {
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
@ -427,20 +427,20 @@ Autocompleter.Local = Class.create(Autocompleter.Base, {
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
@ -450,14 +450,14 @@ Autocompleter.Local = Class.create(Autocompleter.Base, {
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
return "<ul>" + ret.join('') + "</ul>";
}
}, options || { });
@ -474,7 +474,7 @@ Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
};
Ajax.InPlaceEditor = Class.create({
initialize: function(element, url, options) {
@ -604,7 +604,7 @@ Ajax.InPlaceEditor = Class.create({
this.triggerCallback('onEnterHover');
},
getText: function() {
return this.element.innerHTML;
return this.element.innerHTML.unescapeHTML();
},
handleAJAXFailure: function(transport) {
this.triggerCallback('onFailure', transport);
@ -780,7 +780,7 @@ Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
onSuccess: function(transport) {
var js = transport.responseText.strip();
if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
throw 'Server returned an invalid collection representation.';
throw('Server returned an invalid collection representation.');
this._collection = eval(js);
this.checkForExternalText();
}.bind(this),
@ -937,7 +937,7 @@ Ajax.InPlaceCollectionEditor.DefaultOptions = {
loadingCollectionText: 'Loading options...'
};
// Delayed observer, like Form.Element.Observer,
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
@ -947,7 +947,7 @@ Form.Element.DelayedObserver = Class.create({
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
@ -960,4 +960,4 @@ Form.Element.DelayedObserver = Class.create({
this.timer = null;
this.callback(this.element, $F(this.element));
}
});
});

View file

@ -1,6 +1,6 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
@ -32,7 +32,7 @@ var Droppables = {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
@ -40,34 +40,34 @@ var Droppables = {
this.drops.push(options);
},
findDeepestChild: function(drops) {
deepest = drops[0];
for (i = 1; i < drops.length; ++i)
if (Element.isParent(drops[i].element, deepest.element))
deepest = drops[i];
return deepest;
},
isContained: function(element, drop) {
var containmentNode;
if(drop.tree) {
containmentNode = element.treeNode;
containmentNode = element.treeNode;
} else {
containmentNode = element.parentNode;
}
return drop._containers.detect(function(c) { return containmentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
@ -87,12 +87,12 @@ var Droppables = {
show: function(point, element) {
if(!this.drops.length) return;
var drop, affected = [];
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop))
affected.push(drop);
});
if(affected.length>0)
drop = Droppables.findDeepestChild(affected);
@ -101,7 +101,7 @@ var Droppables = {
Position.within(drop.element, point[0], point[1]);
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if (drop != this.last_active) Droppables.activate(drop);
}
},
@ -112,8 +112,8 @@ var Droppables = {
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop) {
this.last_active.onDrop(element, this.last_active.element, event);
return true;
this.last_active.onDrop(element, this.last_active.element, event);
return true;
}
},
@ -121,25 +121,25 @@ var Droppables = {
if(this.last_active)
this.deactivate(this.last_active);
}
}
};
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
@ -148,24 +148,24 @@ var Draggables = {
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
if(draggable.options.delay) {
this._timeout = setTimeout(function() {
Draggables._timeout = null;
window.focus();
Draggables.activeDraggable = draggable;
}.bind(this), draggable.options.delay);
if(draggable.options.delay) {
this._timeout = setTimeout(function() {
Draggables._timeout = null;
window.focus();
Draggables.activeDraggable = draggable;
}.bind(this), draggable.options.delay);
} else {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
}
},
deactivate: function() {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
@ -173,36 +173,36 @@ var Draggables = {
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
if(this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
this.activeDraggable = null;
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
@ -210,7 +210,7 @@ var Draggables = {
});
if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
@ -218,7 +218,7 @@ var Draggables = {
).length;
});
}
}
};
/*--------------------------------------------------------------------------*/
@ -234,12 +234,12 @@ var Draggable = Class.create({
},
endeffect: function(element) {
var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
queue: {scope:'_draggable', position:'end'},
afterFinish: function(){
Draggable._dragging[element] = false
afterFinish: function(){
Draggable._dragging[element] = false
}
});
});
},
zindex: 1000,
revert: false,
@ -250,57 +250,57 @@ var Draggable = Class.create({
snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
delay: 0
};
if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
Object.extend(defaults, {
starteffect: function(element) {
element._opacity = Element.getOpacity(element);
Draggable._dragging[element] = true;
new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
}
});
var options = Object.extend(defaults, arguments[1] || { });
this.element = $(element);
if(options.handle && Object.isString(options.handle))
this.handle = this.element.down('.'+options.handle, 0);
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
options.scroll = $(options.scroll);
this._isScrollChild = Element.childOf(this.element, options.scroll);
}
Element.makePositioned(this.element); // fix IE
Element.makePositioned(this.element); // fix IE
this.options = options;
this.dragging = false;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(Element.getStyle(this.element,'left') || '0'),
parseInt(Element.getStyle(this.element,'top') || '0')]);
},
initDrag: function(event) {
if(!Object.isUndefined(Draggable._dragging[this.element]) &&
Draggable._dragging[this.element]) return;
if(Event.isLeftClick(event)) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if((tag_name = src.tagName.toUpperCase()) && (
@ -309,34 +309,34 @@ var Draggable = Class.create({
tag_name=='OPTION' ||
tag_name=='BUTTON' ||
tag_name=='TEXTAREA')) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(!this.delta)
this.delta = this.currentDelta();
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
if (!this.element._originallyAbsolute)
this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
if (!this._originallyAbsolute)
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
if(this.options.scroll) {
if (this.options.scroll == window) {
var where = this._getWindowScroll(this.options.scroll);
@ -347,28 +347,28 @@ var Draggable = Class.create({
this.originalScrollTop = this.options.scroll.scrollTop;
}
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
if(!this.options.quiet){
Position.prepare();
Droppables.show(pointer, this.element);
}
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
if(this.options.scroll) {
this.stopScrolling();
var p;
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
@ -386,16 +386,16 @@ var Draggable = Class.create({
if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
this.startScrolling(speed);
}
// fix AppleWebKit rendering
if(Prototype.Browser.WebKit) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.quiet){
Position.prepare();
var pointer = [Event.pointerX(event), Event.pointerY(event)];
@ -403,24 +403,24 @@ var Draggable = Class.create({
}
if(this.options.ghosting) {
if (!this.element._originallyAbsolute)
if (!this._originallyAbsolute)
Position.relativize(this.element);
delete this.element._originallyAbsolute;
delete this._originallyAbsolute;
Element.remove(this._clone);
this._clone = null;
}
var dropped = false;
if(success) {
dropped = Droppables.fire(event, this.element);
if (!dropped) dropped = false;
var dropped = false;
if(success) {
dropped = Droppables.fire(event, this.element);
if (!dropped) dropped = false;
}
if(dropped && this.options.onDropped) this.options.onDropped(this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && Object.isFunction(revert)) revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
if (dropped == 0 || revert != 'failure')
@ -433,67 +433,67 @@ var Draggable = Class.create({
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(event.keyCode!=Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.stopScrolling();
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
if(this.options.ghosting) {
var r = Position.realOffset(this.element);
pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
}
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
}.bind(this));
if(this.options.snap) {
if(Object.isFunction(this.options.snap)) {
p = this.options.snap(p[0],p[1],this);
} else {
if(Object.isArray(this.options.snap)) {
p = p.map( function(v, i) {
return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
} else {
p = p.map( function(v) {
return (v/this.options.snap).round()*this.options.snap }.bind(this))
return (v/this.options.snap).round()*this.options.snap }.bind(this));
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
stopScrolling: function() {
if(this.scrollInterval) {
clearInterval(this.scrollInterval);
@ -501,14 +501,14 @@ var Draggable = Class.create({
Draggables._lastScrollPointer = null;
}
},
startScrolling: function(speed) {
if(!(speed[0] || speed[1])) return;
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
},
scroll: function() {
var current = new Date();
var delta = current - this.lastScrolled;
@ -524,7 +524,7 @@ var Draggable = Class.create({
this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
}
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
@ -538,10 +538,10 @@ var Draggable = Class.create({
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
}
if(this.options.change) this.options.change(this);
},
_getWindowScroll: function(w) {
var T, L, W, H;
with (w.document) {
@ -560,7 +560,7 @@ var Draggable = Class.create({
H = documentElement.clientHeight;
} else {
W = body.offsetWidth;
H = body.offsetHeight
H = body.offsetHeight;
}
}
return { top: T, left: L, width: W, height: H };
@ -577,11 +577,11 @@ var SortableObserver = Class.create({
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
@ -591,11 +591,11 @@ var SortableObserver = Class.create({
var Sortable = {
SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
sortables: { },
_findRootElement: function(element) {
while (element.tagName.toUpperCase() != "BODY") {
while (element.tagName.toUpperCase() != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
@ -606,22 +606,23 @@ var Sortable = {
if(!element) return;
return Sortable.sortables[element.id];
},
destroy: function(element){
var s = Sortable.options(element);
element = $(element);
var s = Sortable.sortables[element.id];
if(s) {
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
delete Sortable.sortables[s.element.id];
}
},
create: function(element) {
element = $(element);
var options = Object.extend({
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
@ -635,17 +636,17 @@ var Sortable = {
delay: 0,
hoverclass: null,
ghosting: false,
quiet: false,
quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: this.SERIALIZE_RULE,
// these take arrays of elements or ids and can be
// these take arrays of elements or ids and can be
// used for better initialization performance
elements: false,
handles: false,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || { });
@ -682,24 +683,24 @@ var Sortable = {
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
}
};
var options_for_tree = {
onHover: Sortable.onEmptyHover,
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass
}
};
// fix for gecko engine
Element.cleanWhitespace(element);
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
@ -712,14 +713,14 @@ var Sortable = {
(options.elements || this.findElements(element, options) || []).each( function(e,i) {
var handle = options.handles ? $(options.handles[i]) :
(options.handle ? $(e).select('.' + options.handle)[0] : e);
(options.handle ? $(e).select('.' + options.handle)[0] : e);
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
if(options.tree) e.treeNode = element;
options.droppables.push(e);
options.droppables.push(e);
});
if(options.tree) {
(Sortable.findTreeElements(element, options) || []).each( function(e) {
Droppables.add(e, options_for_tree);
@ -741,7 +742,7 @@ var Sortable = {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.tag);
},
findTreeElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.treeTag);
@ -758,7 +759,7 @@ var Sortable = {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
@ -769,26 +770,26 @@ var Sortable = {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
var child = null;
if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
@ -801,9 +802,9 @@ var Sortable = {
}
}
}
dropon.insertBefore(element, child);
Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
@ -816,34 +817,34 @@ var Sortable = {
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker =
Sortable._marker =
($('dropmarker') || Element.extend(document.createElement('DIV'))).
hide().addClassName('dropmarker').setStyle({position:'absolute'});
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
if(position=='after')
if(sortable.overlap == 'horizontal')
if(sortable.overlap == 'horizontal')
Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
else
Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
Sortable._marker.show();
},
_tree: function(element, options, parent) {
var children = Sortable.findElements(element, options) || [];
for (var i = 0; i < children.length; ++i) {
var match = children[i].id.match(options.format);
if (!match) continue;
var child = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
@ -851,16 +852,16 @@ var Sortable = {
children: [],
position: parent.children.length,
container: $(children[i]).down(options.treeTag)
}
};
/* Get the element containing the children and recurse over it */
if (child.container)
this._tree(child.container, options, child)
this._tree(child.container, options, child);
parent.children.push (child);
}
return parent;
return parent;
},
tree: function(element) {
@ -873,15 +874,15 @@ var Sortable = {
name: element.id,
format: sortableOptions.format
}, arguments[1] || { });
var root = {
id: null,
parent: null,
children: [],
container: element,
position: 0
}
};
return Sortable._tree(element, options, root);
},
@ -897,7 +898,7 @@ var Sortable = {
sequence: function(element) {
element = $(element);
var options = Object.extend(this.options(element), arguments[1] || { });
return $(this.findElements(element, options) || []).map( function(item) {
return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
});
@ -906,14 +907,14 @@ var Sortable = {
setSequence: function(element, new_sequence) {
element = $(element);
var options = Object.extend(this.options(element), arguments[2] || { });
var nodeMap = { };
this.findElements(element, options).each( function(n) {
if (n.id.match(options.format))
nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
n.parentNode.removeChild(n);
});
new_sequence.each(function(ident) {
var n = nodeMap[ident];
if (n) {
@ -922,16 +923,16 @@ var Sortable = {
}
});
},
serialize: function(element) {
element = $(element);
var options = Object.extend(Sortable.options(element), arguments[1] || { });
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "[id]=" +
return [name + Sortable._constructIndex(item) + "[id]=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&');
} else {
@ -940,16 +941,16 @@ var Sortable = {
}).join('&');
}
}
}
};
// Returns true if child is contained within element
Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
}
};
Element.findChildren = function(element, only, recursive, tagName) {
Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
@ -965,8 +966,8 @@ Element.findChildren = function(element, only, recursive, tagName) {
});
return (elements.length>0 ? elements.flatten() : []);
}
};
Element.offsetSize = function (element, type) {
return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}
};

View file

@ -3,46 +3,46 @@
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki
//
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// For details, see the script.aculo.us web site: http://script.aculo.us/
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
if (this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if (this.slice(0,1) == '#') {
if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if (this.length==7) color = this.toLowerCase();
}
}
return (color.length==7 ? color : (arguments[0] || this));
if (this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if (this.slice(0,1) == '#') {
if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if (this.length==7) color = this.toLowerCase();
}
}
return (color.length==7 ? color : (arguments[0] || this));
};
/*--------------------------------------------------------------------------*/
Element.collectTextNodes = function(element) {
Element.collectTextNodes = function(element) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
return (node.nodeType==3 ? node.nodeValue :
(node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
}).flatten().join('');
};
Element.collectTextNodesIgnoreClass = function(element, className) {
Element.collectTextNodesIgnoreClass = function(element, className) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
return (node.nodeType==3 ? node.nodeValue :
((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
Element.collectTextNodesIgnoreClass(node, className) : ''));
}).flatten().join('');
};
Element.setContentZoom = function(element, percent) {
element = $(element);
element.setStyle({fontSize: (percent/100) + 'em'});
element = $(element);
element.setStyle({fontSize: (percent/100) + 'em'});
if (Prototype.Browser.WebKit) window.scrollBy(0,0);
return element;
};
@ -70,28 +70,23 @@ var Effect = {
Transitions: {
linear: Prototype.K,
sinoidal: function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
return (-Math.cos(pos*Math.PI)/2) + .5;
},
reverse: function(pos) {
return 1-pos;
},
flicker: function(pos) {
var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
return pos > 1 ? 1 : pos;
},
wobble: function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
},
pulse: function(pos, pulses) {
pulses = pulses || 5;
return (
((pos % (1/pulses)) * pulses).round() == 0 ?
((pos * pulses * 2) - (pos * pulses * 2).floor()) :
1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
);
pulse: function(pos, pulses) {
return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
},
spring: function(pos) {
return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
spring: function(pos) {
return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
},
none: function(pos) {
return 0;
@ -112,14 +107,14 @@ var Effect = {
tagifyText: function(element) {
var tagifyStyle = 'position:relative';
if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
if (child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
new Element('span', {style: tagifyStyle}).update(
character == ' ' ? String.fromCharCode(160) : character),
character == ' ' ? String.fromCharCode(160) : character),
child);
});
Element.remove(child);
@ -128,13 +123,13 @@ var Effect = {
},
multiple: function(element, effect) {
var elements;
if (((typeof element == 'object') ||
Object.isFunction(element)) &&
if (((typeof element == 'object') ||
Object.isFunction(element)) &&
(element.length))
elements = element;
else
elements = $(element).childNodes;
var options = Object.extend({
speed: 0.1,
delay: 0.0
@ -156,7 +151,7 @@ var Effect = {
var options = Object.extend({
queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
}, arguments[2] || { });
Effect[element.visible() ?
Effect[element.visible() ?
Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
}
};
@ -168,20 +163,20 @@ Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
Effect.ScopedQueue = Class.create(Enumerable, {
initialize: function() {
this.effects = [];
this.interval = null;
this.interval = null;
},
_each: function(iterator) {
this.effects._each(iterator);
},
add: function(effect) {
var timestamp = new Date().getTime();
var position = Object.isString(effect.options.queue) ?
var position = Object.isString(effect.options.queue) ?
effect.options.queue : effect.options.queue.position;
switch(position) {
case 'front':
// move unstarted effects after this effect
// move unstarted effects after this effect
this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
e.startOn += effect.finishOn;
e.finishOn += effect.finishOn;
@ -195,13 +190,13 @@ Effect.ScopedQueue = Class.create(Enumerable, {
timestamp = this.effects.pluck('finishOn').max() || timestamp;
break;
}
effect.startOn += timestamp;
effect.finishOn += timestamp;
if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
this.effects.push(effect);
if (!this.interval)
this.interval = setInterval(this.loop.bind(this), 15);
},
@ -214,7 +209,7 @@ Effect.ScopedQueue = Class.create(Enumerable, {
},
loop: function() {
var timePos = new Date().getTime();
for(var i=0, len=this.effects.length;i<len;i++)
for(var i=0, len=this.effects.length;i<len;i++)
this.effects[i] && this.effects[i].loop(timePos);
}
});
@ -223,7 +218,7 @@ Effect.Queues = {
instances: $H(),
get: function(queueName) {
if (!Object.isString(queueName)) return queueName;
return this.instances.get(queueName) ||
this.instances.set(queueName, new Effect.ScopedQueue());
}
@ -248,23 +243,35 @@ Effect.Base = Class.create({
this.fromToDelta = this.options.to-this.options.from;
this.totalTime = this.finishOn-this.startOn;
this.totalFrames = this.options.fps*this.options.duration;
eval('this.render = function(pos){ '+
'if (this.state=="idle"){this.state="running";'+
codeForEvent(this.options,'beforeSetup')+
(this.setup ? 'this.setup();':'')+
codeForEvent(this.options,'afterSetup')+
'};if (this.state=="running"){'+
'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
'this.position=pos;'+
codeForEvent(this.options,'beforeUpdate')+
(this.update ? 'this.update(pos);':'')+
codeForEvent(this.options,'afterUpdate')+
'}}');
this.render = (function() {
function dispatch(effect, eventName) {
if (effect.options[eventName + 'Internal'])
effect.options[eventName + 'Internal'](effect);
if (effect.options[eventName])
effect.options[eventName](effect);
}
return function(pos) {
if (this.state === "idle") {
this.state = "running";
dispatch(this, 'beforeSetup');
if (this.setup) this.setup();
dispatch(this, 'afterSetup');
}
if (this.state === "running") {
pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
this.position = pos;
dispatch(this, 'beforeUpdate');
if (this.update) this.update(pos);
dispatch(this, 'afterUpdate');
}
};
})();
this.event('beforeStart');
if (!this.options.sync)
Effect.Queues.get(Object.isString(this.options.queue) ?
Effect.Queues.get(Object.isString(this.options.queue) ?
'global' : this.options.queue.scope).add(this);
},
loop: function(timePos) {
@ -273,9 +280,9 @@ Effect.Base = Class.create({
this.render(1.0);
this.cancel();
this.event('beforeFinish');
if (this.finish) this.finish();
if (this.finish) this.finish();
this.event('afterFinish');
return;
return;
}
var pos = (timePos - this.startOn) / this.totalTime,
frame = (pos * this.totalFrames).round();
@ -287,7 +294,7 @@ Effect.Base = Class.create({
},
cancel: function() {
if (!this.options.sync)
Effect.Queues.get(Object.isString(this.options.queue) ?
Effect.Queues.get(Object.isString(this.options.queue) ?
'global' : this.options.queue.scope).remove(this);
this.state = 'finished';
},
@ -325,10 +332,10 @@ Effect.Parallel = Class.create(Effect.Base, {
Effect.Tween = Class.create(Effect.Base, {
initialize: function(object, from, to) {
object = Object.isString(object) ? $(object) : object;
var args = $A(arguments), method = args.last(),
var args = $A(arguments), method = args.last(),
options = args.length == 5 ? args[3] : null;
this.method = Object.isFunction(method) ? method.bind(object) :
Object.isFunction(object[method]) ? object[method].bind(object) :
Object.isFunction(object[method]) ? object[method].bind(object) :
function(value) { object[method] = value };
this.start(Object.extend({ from: from, to: to }, options || { }));
},
@ -392,7 +399,7 @@ Effect.Move = Class.create(Effect.Base, {
// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
return new Effect.Move(element,
return new Effect.Move(element,
Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};
@ -414,15 +421,15 @@ Effect.Scale = Class.create(Effect.Base, {
setup: function() {
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = this.element.getStyle('position');
this.originalStyle = { };
['top','left','width','height','fontSize'].each( function(k) {
this.originalStyle[k] = this.element.style[k];
}.bind(this));
this.originalTop = this.element.offsetTop;
this.originalLeft = this.element.offsetLeft;
var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%','pt'].each( function(fontSizeType) {
if (fontSize.indexOf(fontSizeType)>0) {
@ -430,9 +437,9 @@ Effect.Scale = Class.create(Effect.Base, {
this.fontSizeType = fontSizeType;
}
}.bind(this));
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
if (this.options.scaleMode=='box')
this.dims = [this.element.offsetHeight, this.element.offsetWidth];
@ -507,17 +514,16 @@ Effect.Highlight = Class.create(Effect.Base, {
Effect.ScrollTo = function(element) {
var options = arguments[1] || { },
scrollOffsets = document.viewport.getScrollOffsets(),
elementOffsets = $(element).cumulativeOffset(),
max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();
scrollOffsets = document.viewport.getScrollOffsets(),
elementOffsets = $(element).cumulativeOffset();
if (options.offset) elementOffsets[1] += options.offset;
return new Effect.Tween(null,
scrollOffsets.top,
elementOffsets[1] > max ? max : elementOffsets[1],
elementOffsets[1],
options,
function(p){ scrollTo(scrollOffsets.left, p.round()) }
function(p){ scrollTo(scrollOffsets.left, p.round()); }
);
};
@ -529,9 +535,9 @@ Effect.Fade = function(element) {
var options = Object.extend({
from: element.getOpacity() || 1.0,
to: 0.0,
afterFinishInternal: function(effect) {
afterFinishInternal: function(effect) {
if (effect.options.to!=0) return;
effect.element.hide().setStyle({opacity: oldOpacity});
effect.element.hide().setStyle({opacity: oldOpacity});
}
}, arguments[1] || { });
return new Effect.Opacity(element,options);
@ -547,15 +553,15 @@ Effect.Appear = function(element) {
effect.element.forceRerendering();
},
beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from).show();
effect.element.setOpacity(effect.options.from).show();
}}, arguments[1] || { });
return new Effect.Opacity(element,options);
};
Effect.Puff = function(element) {
element = $(element);
var oldStyle = {
opacity: element.getInlineOpacity(),
var oldStyle = {
opacity: element.getInlineOpacity(),
position: element.getStyle('position'),
top: element.style.top,
left: element.style.left,
@ -563,12 +569,12 @@ Effect.Puff = function(element) {
height: element.style.height
};
return new Effect.Parallel(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) {
Position.absolutize(effect.effects[0].element)
Position.absolutize(effect.effects[0].element);
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide().setStyle(oldStyle); }
@ -580,12 +586,12 @@ Effect.BlindUp = function(element) {
element = $(element);
element.makeClipping();
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping();
}
}
}, arguments[1] || { })
);
};
@ -593,15 +599,15 @@ Effect.BlindUp = function(element) {
Effect.BlindDown = function(element) {
element = $(element);
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makeClipping().setStyle({height: '0px'}).show();
},
effect.element.makeClipping().setStyle({height: '0px'}).show();
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
@ -616,16 +622,16 @@ Effect.SwitchOff = function(element) {
from: 0,
transition: Effect.Transitions.flicker,
afterFinishInternal: function(effect) {
new Effect.Scale(effect.element, 1, {
new Effect.Scale(effect.element, 1, {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) {
beforeSetup: function(effect) {
effect.element.makePositioned().makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
}
})
});
}
}, arguments[1] || { }));
};
@ -637,16 +643,16 @@ Effect.DropOut = function(element) {
left: element.getStyle('left'),
opacity: element.getInlineOpacity() };
return new Effect.Parallel(
[ new Effect.Move(element, {x: 0, y: 100, sync: true }),
[ new Effect.Move(element, {x: 0, y: 100, sync: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
Object.extend(
{ duration: 0.5,
beforeSetup: function(effect) {
effect.effects[0].element.makePositioned();
effect.effects[0].element.makePositioned();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
}
}
}, arguments[1] || { }));
};
@ -674,7 +680,7 @@ Effect.Shake = function(element) {
new Effect.Move(effect.element,
{ x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
effect.element.undoPositioned().setStyle(oldStyle);
}}) }}) }}) }}) }}) }});
}}); }}); }}); }}); }}); }});
};
Effect.SlideDown = function(element) {
@ -682,9 +688,9 @@ Effect.SlideDown = function(element) {
// SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = element.down().getStyle('bottom');
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: window.opera ? 0 : 1,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
@ -692,11 +698,11 @@ Effect.SlideDown = function(element) {
effect.element.makePositioned();
effect.element.down().makePositioned();
if (window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping().setStyle({height: '0px'}).show();
effect.element.makeClipping().setStyle({height: '0px'}).show();
},
afterUpdateInternal: function(effect) {
effect.element.down().setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
effect.element.undoClipping().undoPositioned();
@ -710,8 +716,8 @@ Effect.SlideUp = function(element) {
var oldInnerBottom = element.down().getStyle('bottom');
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, window.opera ? 0 : 1,
Object.extend({ scaleContent: false,
scaleX: false,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
@ -721,7 +727,7 @@ Effect.SlideUp = function(element) {
effect.element.down().makePositioned();
if (window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping().show();
},
},
afterUpdateInternal: function(effect) {
effect.element.down().setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
@ -734,15 +740,15 @@ Effect.SlideUp = function(element) {
);
};
// Bug in opera makes the TD containing this element expand for a instance after finish
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0, {
return new Effect.Scale(element, window.opera ? 1 : 0, {
restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makeClipping();
},
effect.element.makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping();
effect.element.hide().undoClipping();
}
});
};
@ -762,13 +768,13 @@ Effect.Grow = function(element) {
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var dims = element.getDimensions();
var initialMoveX, initialMoveY;
var moveX, moveY;
switch (options.direction) {
case 'top-left':
initialMoveX = initialMoveY = moveX = moveY = 0;
initialMoveX = initialMoveY = moveX = moveY = 0;
break;
case 'top-right':
initialMoveX = dims.width;
@ -793,11 +799,11 @@ Effect.Grow = function(element) {
moveY = -dims.height / 2;
break;
}
return new Effect.Move(element, {
x: initialMoveX,
y: initialMoveY,
duration: 0.01,
duration: 0.01,
beforeSetup: function(effect) {
effect.element.hide().makeClipping().makePositioned();
},
@ -806,17 +812,17 @@ Effect.Grow = function(element) {
[ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
new Effect.Scale(effect.element, 100, {
scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) {
effect.effects[0].element.setStyle({height: '0px'}).show();
effect.effects[0].element.setStyle({height: '0px'}).show();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
}
}, options)
)
);
}
});
};
@ -838,7 +844,7 @@ Effect.Shrink = function(element) {
var dims = element.getDimensions();
var moveX, moveY;
switch (options.direction) {
case 'top-left':
moveX = moveY = 0;
@ -855,19 +861,19 @@ Effect.Shrink = function(element) {
moveX = dims.width;
moveY = dims.height;
break;
case 'center':
case 'center':
moveX = dims.width / 2;
moveY = dims.height / 2;
break;
}
return new Effect.Parallel(
[ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
], Object.extend({
], Object.extend({
beforeStartInternal: function(effect) {
effect.effects[0].element.makePositioned().makeClipping();
effect.effects[0].element.makePositioned().makeClipping();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
@ -877,12 +883,14 @@ Effect.Shrink = function(element) {
Effect.Pulsate = function(element) {
element = $(element);
var options = arguments[1] || { };
var oldOpacity = element.getInlineOpacity();
var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
reverser.bind(transition);
return new Effect.Opacity(element,
var options = arguments[1] || { },
oldOpacity = element.getInlineOpacity(),
transition = options.transition || Effect.Transitions.linear,
reverser = function(pos){
return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
};
return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 2.0, from: 0,
afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
}, options), {transition: reverser}));
@ -896,12 +904,12 @@ Effect.Fold = function(element) {
width: element.style.width,
height: element.style.height };
element.makeClipping();
return new Effect.Scale(element, 5, Object.extend({
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
afterFinishInternal: function(effect) {
new Effect.Scale(element, 1, {
scaleContent: false,
new Effect.Scale(element, 1, {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping().setStyle(oldStyle);
@ -916,7 +924,7 @@ Effect.Morph = Class.create(Effect.Base, {
var options = Object.extend({
style: { }
}, arguments[1] || { });
if (!Object.isString(options.style)) this.style = $H(options.style);
else {
if (options.style.include(':'))
@ -934,18 +942,18 @@ Effect.Morph = Class.create(Effect.Base, {
effect.transforms.each(function(transform) {
effect.element.style[transform.style] = '';
});
}
};
}
}
this.start(options);
},
setup: function(){
function parseColor(color){
if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
color = color.parseColor();
return $R(0,2).map(function(i){
return parseInt( color.slice(i*2+1,i*2+3), 16 )
return parseInt( color.slice(i*2+1,i*2+3), 16 );
});
}
this.transforms = this.style.map(function(pair){
@ -965,9 +973,9 @@ Effect.Morph = Class.create(Effect.Base, {
}
var originalValue = this.element.getStyle(property);
return {
style: property.camelize(),
originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
return {
style: property.camelize(),
originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
targetValue: unit=='color' ? parseColor(value) : value,
unit: unit
};
@ -978,13 +986,13 @@ Effect.Morph = Class.create(Effect.Base, {
transform.unit != 'color' &&
(isNaN(transform.originalValue) || isNaN(transform.targetValue))
)
)
);
});
},
update: function(position) {
var style = { }, transform, i = this.transforms.length;
while(i--)
style[(transform = this.transforms[i]).style] =
style[(transform = this.transforms[i]).style] =
transform.unit=='color' ? '#'+
(Math.round(transform.originalValue[0]+
(transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
@ -993,7 +1001,7 @@ Effect.Morph = Class.create(Effect.Base, {
(Math.round(transform.originalValue[2]+
(transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
(transform.originalValue +
(transform.targetValue - transform.originalValue) * position).toFixed(3) +
(transform.targetValue - transform.originalValue) * position).toFixed(3) +
(transform.unit === null ? '' : transform.unit);
this.element.setStyle(style, true);
}
@ -1030,7 +1038,7 @@ Effect.Transform = Class.create({
});
Element.CSS_PROPERTIES = $w(
'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
@ -1039,7 +1047,7 @@ Element.CSS_PROPERTIES = $w(
'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
'right textIndent top width wordSpacing zIndex');
Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
String.__parseStyleElement = document.createElement('div');
@ -1051,11 +1059,11 @@ String.prototype.parseStyle = function(){
String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
style = String.__parseStyleElement.childNodes[0].style;
}
Element.CSS_PROPERTIES.each(function(property){
if (style[property]) styleRules.set(property, style[property]);
if (style[property]) styleRules.set(property, style[property]);
});
if (Prototype.Browser.IE && this.include('opacity'))
styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
@ -1074,14 +1082,14 @@ if (document.defaultView && document.defaultView.getComputedStyle) {
Element.getStyles = function(element) {
element = $(element);
var css = element.currentStyle, styles;
styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) {
hash.set(property, css[property]);
return hash;
styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
results[property] = css[property];
return results;
});
if (!styles.opacity) styles.set('opacity', element.getOpacity());
if (!styles.opacity) styles.opacity = element.getOpacity();
return styles;
};
};
}
Effect.Methods = {
morph: function(element, style) {
@ -1090,7 +1098,7 @@ Effect.Methods = {
return element;
},
visualEffect: function(element, effect, options) {
element = $(element)
element = $(element);
var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
new Effect[klass](element, options);
return element;
@ -1104,17 +1112,17 @@ Effect.Methods = {
$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
'pulsate shake puff squish switchOff dropOut').each(
function(effect) {
function(effect) {
Effect.Methods[effect] = function(element, options){
element = $(element);
Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
return element;
}
};
}
);
$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
function(f) { Effect.Methods[f] = Element[f]; }
);
Element.addMethods(Effect.Methods);
Element.addMethods(Effect.Methods);

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -29,15 +29,18 @@ div.footer a {
}
h1 {
color: #f00;
font-size: small;
color: #f00;
font-size: small;
margin-top:.3em;
margin-bottom:.3em;
}
h2 {
background-color: #cccccc;
font-size : small;
margin-top: 10px;
margin-bottom: 0;
background-color: #aaaaaa;
font-size : small;
margin: .3em 0;
padding: .3em 0 .1em .3em;
border-top: 1px solid #777777;
}
h4.alert {
@ -60,12 +63,8 @@ h4.notice {
color: #007E00;
}
div.t {
padding-left:5px;
}
span.tag {
font-size: XX-small;
font-size: x-small;
background-color: #CCE7FF;
color: #000;
padding: 1px;
@ -86,43 +85,78 @@ span.prj, span.ctx{
color: #fff;
background: #f00;
padding: 1px;
font-size: 10px;
font-size: 1em;
}
.amber {
color: #fff;
background: #ff6600;
padding: 1px;
font-size: 10px;
font-size: 1em;
}
.orange {
color: #fff;
background: #FFA500;
padding: 1px;
font-size: 10px;
font-size: 1em;
}
.green {
color: #fff;
background: #33cc00;
padding: 1px;
font-size: 10px;
font-size: 1em;
}
.grey {
color: #fff;
background: #999;
padding: 1px;
font-size: 10px;
font-size: 1em;
}
.count {
color: #fff;
background: #000;
font-size: medium;
font-size: medium;
}
.errors {
background: #FFC2C2;
}
}
ul.c li.star {
list-style-image:url(../images/menustar_small.gif)
}
ul.c {
padding: 0;
margin: 0;
padding-left: 1.1em;
}
ul.c li {
padding: 0;
margin: 0;
}
ul.c li span.r {
display: none;
}
span.r {
display:none;
}
.nav {
font-size: x-small;
}
#database_auth_form table td {
width:7em;
}
table.c {
margin-left: 5px;
}

View file

@ -1,293 +0,0 @@
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {margin:0; padding:0}
table {border-collapse:collapse; border-spacing:0}
fieldset,img {border:0}
address,caption,cite,code,dfn,em,strong,th,var {font-style:normal; font-weight:normal}
ol,ul {list-style:none}
caption,th {text-align:left}
h1,h2,h3,h4,h5,h6 {font-size:100%; font-weight:normal}
q:before,q:after {content:''}
abbr,acronym {border:0}
body {font-family: "Lucida Grande", Verdana, Geneva, Arial, sans-serif; font-size: 80%; padding: 0px 10px; margin: 0px; background: #eee}
p {padding: 2px; font-size: 92%; line-height: 140%}
a, a:link, a:active, a:visited {color: #cc3334; text-decoration: none; padding-left: 1px; padding-right: 1px}
a:hover {color: #fff; background-color: #cc3334}
h1 {font-size: 304%; font-weight: bold}
h2 {font-size: 148%; font-weight: bold}
h3 {font-size: 129%; font-weight: bold}
img.edit_item {background-image: url(../images/edit_off.png); background-repeat: no-repeat; border: none;}
a:hover img.edit_item {background-image: url(../images/edit_on.png); background-color: transparent; background-repeat: no-repeat; border: none;}
img.delete_item {background-image: url(../images/delete_off.png); background-repeat: no-repeat; border: none;}
a:hover img.delete_item {background-image: url(../images/delete_on.png);background-color: transparent;background-repeat: no-repeat; border: none;}
img.starred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: 0px 0px;}
a:hover img.starred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -16px 0px;}
img.unstarred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -32px 0px;}
a:hover img.unstarred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -48px 0px;}
a.to_top {background: transparent url(../images/top_off.png) no-repeat;}
a.to_top:hover {background: transparent url(../images/top_on.png) no-repeat;}
a.up {background: transparent url(../images/up_off.png) no-repeat;}
a.up:hover {background: transparent url(../images/up_on.png) no-repeat;}
a.down {background: transparent url(../images/down_off.png) no-repeat;}
a.down:hover {background: transparent url(../images/down_on.png) no-repeat;}
a.to_bottom {background: transparent url(../images/bottom_off.png) no-repeat;}
a.to_bottom:hover {background: transparent url(../images/bottom_on.png) no-repeat;}
a.show_notes, a.link_to_notes {background-image: url(../images/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;}
a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;}
#content {margin-top: 90px}
#display_box {float: left; width: 55%; margin: 0px 10px 50px 15px}
#single_box {width: 60%; margin: 80px auto}
#full_width_display {float: left; width: 95%; margin: 0px 15px 90px 15px}
#display_box_projects {float: left; width: 95%; margin: 0px 15px 90px 15px}
#recurring_timespan, #recurring_target {border: none; clear: both}
#recurring_target {border-top-style: dotted; padding: 15px 0px 0px 0px}
#recurring_daily, #recurring_weekly, #recurring_monthly, #recurring_yearly, #recurring_edit_daily, #recurring_edit_weekly, #recurring_edit_monthly, #recurring_edit_yearly {border: none; border-top-style: dotted; clear: both; padding: 15px 0px 15px 0px}
#recurring_period_id, #recurring_edit_period_id {border: none; float: left; margin: 0px 50px 0px 0px; padding: 0px 25px 15px 50px; border-right-style: none}
#recurring_todo {width: 270px}
#recurring_todo_form_container {border-right-style: dotted; padding: 0px 50px 15px 0px; float: left}
#recurring_todo input, #recurring_todo textarea {width: 100%}
#overlay {visibility: hidden; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; z-index: 102; text-align: center; background-image:url("../images/trans70.png")}
#overlay #new-recurring-todo, #overlay #edit-recurring-todo {width:750px; background-color: #fff; border:1px solid #000; padding: 15px; margin: 70px auto}
.recurring_container {padding: 0px 5px 0px 5px; border: 1px solid #999; margin: 0px 0px 0px 0px; background: #fff; text-align: left}
.recurring_submit_box {height: 25px; padding: 5px 0; text-align: center; clear: both; border: none}
#navcontainer {position: fixed; top: 48px; left: 0px}
#navlist {margin: 0; padding: 0 0 20px 5px}
#navlist ul, #navlist li {margin: 0; padding: 0; display: inline; list-style-type: none}
#navlist a:link, #navlist a:visited {float: left; line-height: 14px; font-weight: bold; margin: 0 10px 4px 10px; text-decoration: none; color: #eee}
#navlist a:link#current, #navlist a:visited#current, #navlist a:hover {border-bottom: 4px solid #CCC; padding-bottom: 2px; background: transparent; color: #CCC}
#navlist a:hover {color: #CCC}
#topbar {position: fixed; top: 0px; left: 0px; height: 68px; margin-bottom: 20px; clear: both; background-color: #000; filter: alpha(opacity=75); -moz-opacity: .75; opacity: .75; color: #eee; width: 100%; z-index:101}
body.stats #topbar {filter: alpha(opacity=100); -moz-opacity: 1; opacity: 1}
#date {float: left; width: 45%; padding-left: 15px; margin-top: 15px; margin-bottom: 5px; white-space: nowrap}
#date h1 {font-size: 152%}
#minilinks {text-align: right; position: fixed; right: 15px; top: 10px; font-size: 0.9em}
.container {padding: 0px 5px 0px 5px; border: 1px solid #999; margin: 0px 0px 15px 0px; background: #fff}
.completed {background: #eee}
.container h2 {background: #ccc; padding: 5px; margin-top: 0px; margin-left: -5px; margin-right: -5px; margin-bottom: 0px; color: #666; position:static}
.container_toggle img {height:20px; width:20px; border:0px}
h2 a, h2 a:link, h2 a:active, h2 a:visited {color: #666; text-decoration: none}
h2 a:hover {color: #cc3334; background-color: transparent; text-decoration: none}
div#input_box {margin: 0px 15px 0px 58%; padding: 0px 15px 0px 0px}
#input_box h2 {color: #999}
#input_box ul {list-style-type: circle; font-size: 0.9em;}
.show_from_input, .due_input, .project_input, .context_input {float:left}
.box {float: left; width: 20px}
div.item-container {padding: 2px 0px; line-height:20px; clear: both}
a.recurring_icon {vertical-align: middle; background-color: transparent}
a.icon {float: left; vertical-align: middle; background-color: transparent}
input.item-checkbox {float: left; margin-left: 10px; vertical-align: middle}
.description {margin-left: 85px; position:relative }
.stale_l1, .stale_l2, .stale_l3 {margin-left: 82px; padding-left: 3px}
.stale_l1 {background: #ffC}
.tools {margin-left: 25px; width: 40px; border-top: 1px solid #999}
#footer {clear: both; font-size: 85%; text-align: center; color: #999; margin: 20px 20px 5px 20px; padding: 0px}
.todo_notes {margin: 5px; padding: 3px; border: 1px solid #F5ED59; background: #FAF6AE; color: #666666}
.todo_notes p, .todo_notes li {padding: 1px; margin: 0px; font-size: 12px}
.todo_notes ul, .note_wrapper ul {list-style-type: disc; margin-left:20px}
.todo_notes ol {list-style-type: decimal; margin-left:20px}
div.note_wrapper {margin: 3px; padding: 2px}
div.note_wrapper p {display: inline}
div.note_footer {border-top: 1px solid #999; padding-top: 3px; font-style: italic; font-size: 0.9em; color: #666}
div.note_footer a, div.note_footer a:hover {border-top: none; padding-top: 0px; vertical-align: middle; background-color: transparent}
div.add_note_link {margin-top:12px; float: right}
div#project_status > div {padding: 10px}
#project_status span {margin-right:5px; background-color:white}
#project_status .active_state {font-weight:bold}
div#default_context > div{ padding:10px}
a.footer_link {color: #cc3334; font-style: normal;}
a.footer_link:hover {color: #fff; background-color: #cc3334 !important;}
span.tag {font-size: 0.8em; background-color: #CCE7FF; color: #000; padding: 1px; margin-right: 2px}
span.tag a, span.tag a:link, span.tag a:active, span.tag a:visited {color: #000}
span.tag a:hover {background-color: #99CCFF; color: #333}
div#message_holder {position: absolute; z-index: 100; left: 60%; top: 30px; right: 0px; margin: 0px}
h4.alert {font-size: 1em; margin: 0px; padding: 5px; text-align: center}
h4.warning {border: 1px solid #ED2E38; background-color: #F6979C; color: #000}
h4.error {color:#fff; background:#c00}
h4.notice {border: 1px solid #007E00; background-color: #c2ffc2; color: #007E00}
.project_completed {border: 1px solid #007E00; background-color: #c2ffc2; padding: 5px; color: #007E00; text-align: center}
.red {color: #fff; background: #f00; padding: 1px; font-size: 85%}
.amber {color: #fff; background: #ff6600; padding: 1px; font-size: 85%}
.orange {color: #fff; background: #FFA500; padding: 1px; font-size: 85%}
.green {color: #fff; background: #33cc00; padding: 1px; font-size: 85%}
.grey {color: #fff; background: #999; padding: 2px; font-size: 85%}
.info {color: #fff; background: #CCC; border: 1px solid #999; padding: 5px; text-align: center}
.highlight {background: #ffC; padding: 2px}
.stale_l1 {background: #ffC}
.stale_l2 {background: #ff6}
.stale_l3 {background: #ff0}
.badge {color: #fff; background: #f00; padding: 3px 5px; font-size: 12pt; margin: 10px 10px 0px 0px; height:26px}
ul {list-style-type: none}
#sidebar h3 {margin-top:15px; margin-bottom:5px}
#sidebar ul {margin-left: auto; list-style-position: inside}
li {font-size: 1.1em; padding: 3px 0px}
#sidebar .integrations-link {margin-top:10px; padding-top:10px; font-size: 0.8em}
.sortable_row {background: #fff; _background: transparent; padding: 4px 4px 4px 8px; margin: 2px 2px; border: 1px solid #ccc}
.edit-form {background: #ccc; padding: 0 10px; border-top: 1px solid #999; border-bottom: 1px solid #999; position:relative}
.label {text-align: right}
input {vertical-align: middle}
img.position, a:hover img.position {text-align: left; vertical-align: middle; background-color: transparent}
.data {text-align: left; margin-left: 20px; float: left}
div.buttons, div.buttons a, div.buttons a:hover {text-align: right; margin-right: 0px; vertical-align: middle; background-color: transparent}
div#list-active-projects, div#list-hidden-projects, div#list-completed-projects, div#list-contexts, div#projects-empty-nd {clear:right; border: 1px solid #999}
.project-state-group h2 {margin:20px 0px 8px 13px}
.search-result-group h2 {margin:20px 0px 8px 13px }
div.menu_sort {margin-top:-20px; float:right}
div.alpha_sort, div.tasks_sort,span.sort_separator {float:left}
.container td {border: none; padding-bottom: 5px}
.container form {border: none}
div.project_description {background: #eee; padding: 5px; margin-top: 0px; margin-left: -5px; margin-right: -5px; color: #666; font-style: italic; font-size: 12px; font-weight: normal}
#project-next-prev {text-align:right}
form {border: 1px solid #CCC; padding: 10px; margin: 0px}
.inline-form {border: none; padding: 3px}
.inline-form table {padding-right: 3px}
.inline-form table, .inline-form textarea#item_notes, .inline-form input#item_description {width: 100%}
.inline-form table td.label {width: 13ex}
#todo_new_action_container, #project_new_project_container, #context_new_container, #recurring_new_container {background: #ddd; width: 270px; padding: 5px 10px; background-color: #000; filter: alpha(opacity=75); -moz-opacity: .75; opacity: .75; color: #eee}
#recurring_new_container img {vertical-align: middle}
#project_new_project_filler {padding-top: 50px}
#todo_new_action_container input, #todo_new_action_container textarea, #project_new_project_container input, #project_new_project_container textarea, #context_new_container input {width: 100%}
input#go_to_project, input#context_hide {width: 5%}
#todo_new_action_container .show_from_input, #todo_new_action_container .due_input {width: 45%}
#todo_new_action_container .show_from_input {float: right}
#todo-form-new-action .submit_box, #project_form .submit_box, #context_form .submit_box {height: 25px; padding: 5px 0; text-align: center; clear: right}
.edit_todo_form .submit_box {height: 25px; padding: 5px 0; text-align: center; clear: right}
.edit_todo_form input, .edit_todo_form textarea {width:100%}
.edit_todo_form .Date {width:89%}
.edit_todo_form a.date_clear:hover {background: #CCCCCC}
.edit_todo_form .tag_list_label {clear:both}
.edit_todo_form .due_input, .edit_todo_form .show_from_input, .edit_todo_form .project_input, .edit_todo_form .context_input {width:48%}
.edit_todo_form .show_from_input, .edit_todo_form .context_input {float: right}
.edit_todo_form .submit_box input {width:120px}
.hide_form {text-align:right}
#todo-form-new-action label, .edit_todo_form label {display: block; padding-bottom: 3px}
form.button-to {border: none; padding: 0px; margin: 0px}
label {font-weight: bold; padding: 0px 0px}
input, select, textarea {margin: 0px 0px 5px 0px}
#feedicons-project, #feedicons-context {background-color: #D2D3D6; margin: 0px 0px 0px 60px; padding: 0px 0px 0px 5px; width: 70%}
.feed {font-family: verdana, sans-serif; font-size: 10px; font-weight:bold; text-decoration:none; color: white; background-color: #F60; border:1px solid; border-color: #FC9 #630 #330 #F96; padding:0px 3px 0px 3px; margin:0px}
.position {float: left; margin-top:2px}
.handle {color: #fff; background: #000; padding: 2px; font-size: 92%; cursor: move}
div.message {margin: 5px 0px; background: #FAF4B5; padding: 2px}
.message p {margin: 0px; padding: 0px; text-align: center; font-size: 1em; color: #666}
.fieldWithErrors {padding: 2px; background-color: red; display: table}
#errorExplanation {border: 2px solid #ff0000; padding: 7px; padding-bottom: 12px; margin: 10px auto 20px auto; background-color: #f0f0f0}
#errorExplanation h2 {text-align: left; font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; background-color: #c00; color: #fff}
#errorExplanation > p {color: #333; margin-top: 5px; margin-bottom: 0; padding: 5px}
#errorExplanation ul li {font-size: 1em; list-style-type: disc; list-style-position: inside; margin-left:7px; color: #333}
ul#prefs {list-style-type: disc; margin-left: 15px;}
#token_area, #authentication_area {text-align:center; margin-top:20px; margin-bottom:10px}
#token_area .description{ font-weight:bold}
#token_area form {width:100%; text-align:center}
.prefscontainer .actions {text-align:center; margin-bottom:20px}
.authtype_container .actions {margin-top:20px; margin-bottom:20px}
#feedlegend {padding: 2px; background-color: #D2D3D6; color: #666; padding: 5px 20px; text-align: left}
#feedlegend h3, #feedlegend dl, #feedlegend dt, #feedlegend dd {display: inline}
#feedlegend dt {margin-left: 15px}
#feedlegend dd {margin-left: 3px}
#feedlegend p {margin-bottom: 0px}
#feeds img.rss-icon {margin-bottom: -4px}
#feeds li {font-size:13px; font-family: "Lucida Grande", Verdana, Geneva, Arial, sans-serif}
#feeds li h4 {margin-top: 12px; margin-bottom: 4px; font-weight: normal; font-style:oblique}
input.open_id {background: url(../images/open-id-login-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; color: #000; padding-left: 18px; width:182px}
div.page_name_auto_complete {width: 100%; background: #fff; display: inline; z-index: 100}
div.page_name_auto_complete ul {border: 1px solid #888; margin: 0; padding: 0; width: 100%; list-style-type: none}
div.page_name_auto_complete ul li {margin: 0; padding: 2px; font-weight:bold; list-style-type: none; color: #000}
div.page_name_auto_complete ul li.selected {background-color: #ffb}
div.page_name_auto_complete ul strong.highlight {color: #800; margin: 0; padding: 0}
table.next_actions td {padding:5px 3px 2px 0px}
table.users_table {width: 100%; text-align: center; border: 1px solid #666; background-color: #fff; border-spacing: 0px}
.users_table th {color: #fff; background-color: #000;}
.users_table td {border: none;}
table.export_table {border: 1px solid #666; background-color: #fff; border-spacing: 0px}
.export_table th {color: #fff; background-color: #000;}
.export_table td {border: 1px; padding: 5px 0px 5px 5px;}
.widgets a, .widgets button{ display:block; float: left; margin:0px 7px 0 0; background-color:#f5f5f5; border:1px solid #dedede; border-top:1px solid #eee; border-left:1px solid #eee; font-family:"Lucida Grande", Verdana, Geneva, Arial, sans-serif; font-size:80%; line-height:100%; text-decoration:none; font-weight:bold; color:#565656; cursor:pointer; padding:3px 5px 7px 5px}
.widgets button{ width:auto; overflow:visible; padding:3px 5px 5px 5px}
.widgets button[type]{ padding:3px 5px 4px 5px; line-height:15px}
.widgets button img, .widgets a img{ margin:0 3px -3px 0 !important; padding:0; border:none; width:16px; height:16px}
.widgets a:hover{ background-color:#dff4ff; border:1px solid #c2e1ef; color:#336699}
.widgets a:active{ background-color:#6299c5; border:1px solid #6299c5; color:#fff}
button.positive, .widgets a.positive{ color: #498111}
.widgets a.positive:hover, button.positive:hover{ background-color:#E6EFC2; border:1px solid #C6D880; color:#529214}
.widgets a.positive:active{ background-color:#529214; border:1px solid #529214; color:#fff}
.widgets a.negative, button.negative{ color:#d12f19}
.widgets a.negative:hover, button.negative:hover{ background:#fbe3e4; border:1px solid #fbc2c4; color:#d12f19}
.widgets a.negative:active{ background-color:#d12f19; border:1px solid #d12f19; color:#fff}
.tracks__waiting {background-image:url('../images/waiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:white}
.bigWaiting {background-image:url('../images/bigWaiting.gif'); background-repeat:no-repeat; background-position:center 20%; background-color:white}
.blackWaiting {background-image:url('../images/blackWaiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:black}
.bigBlackWaiting {background-image:url('../images/bigBlackWaiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:black}
.stats_content .open-flash-chart, .stats_content .stats_module {float: left; width: 450px; margin-right:20px; padding-bottom:20px}
.stats_content h2 {clear:both; margin-top:15px; margin-bottom:15px}
body.integrations h2 {margin-top:40px; padding-top:20px; margin-bottom:10px; border-top:1px solid #ccc}
body.integrations p, body.integrations li {font-size:1.0em}
body.integrations li {list-style-type: disc; list-style-position: inside; margin-left:30px}
body.integrations textarea {margin:10px; padding:3px; width:80%; background-color:#ddd}
.defer-container {float:right}
.defer-container a:hover {background-color: inherit}
div.calendar {position: relative}
.calendar, .calendar table {border: 1px solid #556; font-size: 11px; color: #000; cursor: default; background: #eef; z-index: 110; font-family: tahoma,verdana,sans-serif}
.calendar .button {text-align: center; padding: 2px}
.calendar .nav {background: #778 url(../images/menuarrow.gif) no-repeat 100% 100%}
.calendar thead .title {font-weight: bold; text-align: center; background: #fff; color: #000; padding: 2px}
.calendar thead .headrow {background: #778; color: #fff}
.calendar thead .daynames {background: #bdf}
.calendar thead .name {border-bottom: 1px solid #556; padding: 2px; text-align: center; color: #000}
.calendar thead .weekend {color: #a66}
.calendar thead .hilite {background-color: #aaf; color: #000; border: 1px solid #04f; padding: 1px}
.calendar thead .active {background-color: #77c; padding: 2px 0px 0px 2px}
.calendar tbody .day {width: 2em; color: #456; text-align: right; padding: 2px 4px 2px 2px}
.calendar tbody .day.othermonth {font-size: 80%; color: #bbb}
.calendar tbody .day.othermonth.oweekend {color: #fbb}
.calendar table .wn {padding: 2px 3px 2px 2px; border-right: 1px solid #000; background: #bdf}
.calendar tbody .rowhilite td {background: #def}
.calendar tbody .rowhilite td.wn {background: #eef}
.calendar tbody td.hilite {background: #def; padding: 1px 3px 1px 1px; border: 1px solid #bbb}
.calendar tbody td.active {background: #cde; padding: 2px 2px 0px 2px}
.calendar tbody td.selected {font-weight: bold; border: 1px solid #000; padding: 1px 3px 1px 1px; background: #fff; color: #000}
.calendar tbody td.weekend {color: #a66}
.calendar tbody td.today {font-weight: bold; color: #00f}
.calendar tbody .disabled {color: #999}
.calendar tbody .emptycell {visibility: hidden}
.calendar tbody .emptyrow {display: none}
.calendar tfoot .footrow {text-align: center; background: #556; color: #fff}
.calendar tfoot .ttip {background: #fff; color: #445; border-top: 1px solid #556; padding: 1px}
.calendar tfoot .hilite {background: #aaf; border: 1px solid #04f; color: #000; padding: 1px}
.calendar tfoot .active {background: #77c; padding: 2px 0px 0px 2px}
.calendar .combo {position: absolute; display: none; top: 0px; left: 0px; width: 4em; cursor: default; border: 1px solid #655; background: #def; color: #000; font-size: 90%; z-index: 100}
.calendar .combo .label, .calendar .combo .label-IEfix {text-align: center; padding: 1px}
.calendar .combo .label-IEfix {width: 4em}
.calendar .combo .hilite {background: #acf}
.calendar .combo .active {border-top: 1px solid #46a; border-bottom: 1px solid #46a; background: #eef; font-weight: bold}
.calendar td.time {border-top: 1px solid #000; padding: 1px 0px; text-align: center; background-color: #f4f0e8}
.calendar td.time .hour, .calendar td.time .minute, .calendar td.time .ampm {padding: 0px 3px 0px 4px; border: 1px solid #889; font-weight: bold; background-color: #fff}
.calendar td.time .ampm {text-align: center}
.calendar td.time .colon {padding: 0px 2px 0px 3px; font-weight: bold}
.calendar td.time span.hilite {border-color: #000; background-color: #667; color: #fff}
.calendar td.time span.active {border-color: #f00; background-color: #000; color: #0f0}
b.niftycorners,b.niftyfill{display:block}
b.niftycorners *{display:block;height: 1px;line-height:1px;font-size: 1px; overflow:hidden;border-style:solid;border-width: 0 1px}
b.r1{margin: 0 3px;border-width: 0 2px}
b.r2{margin: 0 2px}
b.r3{margin: 0 1px}
b.r4{height: 2px}
b.rb1{margin: 0 8px;border-width:0 2px}
b.rb2{margin: 0 6px;border-width:0 2px}
b.rb3{margin: 0 5px}
b.rb4{margin: 0 4px}
b.rb5{margin: 0 3px}
b.rb6{margin: 0 2px}
b.rb7{margin: 0 1px;height:2px}
b.rb8{margin: 0;height:2px}
b.rs1{margin: 0 1px}
b.t1{border-width: 0 5px}
b.t2{border-width: 0 3px}
b.t3{border-width: 0 2px}
b.t4{height: 2px}
b.tb1{border-width: 0 10px}
b.tb2{border-width: 0 8px}
b.tb3{border-width: 0 6px}
b.tb4{border-width: 0 5px}
b.tb5{border-width: 0 4px}
b.tb6{border-width: 0 3px}
b.tb7{border-width: 0 2px;height:2px}
b.tb8{border-width: 0 1px;height:2px}
b.ts1{border-width: 0 2px}

4
script/autospec Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
ENV['RSPEC'] = 'true' # allows autotest to discover rspec
ENV['AUTOTEST'] = 'true' # allows autotest to run w/ color on linux
system (RUBY_PLATFORM =~ /mswin|mingw/ ? 'autotest.bat' : 'autotest'), *ARGV

View file

@ -1,4 +1,5 @@
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../vendor/plugins/rspec/lib"))
require 'rubygems'
require 'spec'
exit ::Spec::Runner::CommandLine.run(::Spec::Runner::OptionParser.parse(ARGV, STDERR, STDOUT))

View file

@ -18,14 +18,14 @@ module LuckySneaks
it "should not be valid if #{attribute} length is more than #{maximum}" do
instance.send "#{attribute}=", 'x'*(maximum+1)
instance.errors_on(attribute).should include(
options[:message_too_long] || ActiveRecord::Errors.default_error_messages[:too_long] % maximum
options[:message_too_long] || I18n.t('activerecord.errors.messages.too_long', :count => maximum)
)
end if maximum
it "should not be valid if #{attribute} length is less than #{minimum}" do
instance.send "#{attribute}=", 'x'*(minimum-1)
instance.errors_on(attribute).should include(
options[:message_to_short] || ActiveRecord::Errors.default_error_messages[:too_short] % minimum
options[:message_to_short] || I18n.t('activerecord.errors.messages.too_short', :count => minimum)
)
end if minimum
end

View file

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

4
stories/all.rb Normal file
View file

@ -0,0 +1,4 @@
dir = File.dirname(__FILE__)
Dir[File.expand_path("#{dir}/**/*.rb")].uniq.each do |file|
require file
end

View file

@ -1,136 +1,3 @@
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
$:.unshift File.join(File.dirname(__FILE__), *%w[.. vendor plugings rspec lib])
require 'test_help'
require 'test/unit/testresult'
require 'spec'
require 'spec/rails'
require 'spec/story'
require 'webrat/selenium'
require 'action_controller/test_process'
module Spec
module Story
class StepGroup
def include_steps_for(name)
require File.expand_path(File.dirname(__FILE__) + "/steps/#{name}")
step_matchers = rspec_story_steps[name.to_sym]
warn "WARNING: 0 step matchers found for include_steps_for(:#{name}). Are you missing an include?" if step_matchers.empty?
self << step_matchers
end
end
end
end
Test::Unit.run = true
class SeleniumRailsStory < Test::Unit::TestCase
include Spec::Matchers
include Spec::Rails::Matchers
def initialize #:nodoc:
# TODO - eliminate this hack, which is here to stop
# Rails Stories from dumping the example summary.
Spec::Runner::Options.class_eval do
def examples_should_be_run?
false
end
end
@_result = Test::Unit::TestResult.new
end
def should_see(text_or_regexp)
if text_or_regexp.is_a?(Regexp)
response.should have_tag("*", text_or_regexp)
else
response.should have_tag("*", /#{Regexp.escape(text_or_regexp)}/i)
end
end
def should_not_see(text_or_regexp)
if text_or_regexp.is_a?(Regexp)
response.should_not have_tag("*", text_or_regexp)
else
response.should_not have_tag("*", /#{Regexp.escape(text_or_regexp)}/i)
end
end
def response
webrat_session.response_body
end
def logged_in_as(user)
visits("/selenium_helper/login?as=#{user.login}")
end
def selenium
SeleniumDriverManager.instance.running_selenium_driver
end
def badge_count_should_show(count)
response.should have_tag('#badge_count', count.to_s)
end
def method_missing(name, *args)
if webrat_session.respond_to?(name)
webrat_session.send(name, *args)
else
super
end
end
protected
def webrat_session
@webrat_session ||= begin
Webrat::SeleniumSession.new(SeleniumDriverManager.instance.running_selenium_driver)
end
end
end
class DatabaseResetListener
include Singleton
def scenario_started(*args)
if defined?(ActiveRecord::Base)
connection = ActiveRecord::Base.connection
%w[users].each do |table|
connection.execute "DELETE FROM #{table}"
end
end
end
def method_missing sym, *args, &block
# ignore all messages you don't care about
end
end
class CookieResetListener
include Singleton
def scenario_started(*args)
%w[tracks_login auth_token _session_id].each do |cookie_name|
SeleniumDriverManager.instance.running_selenium_driver.get_eval("window.document.cookie = '#{cookie_name}=;expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/';")
end
end
def method_missing sym, *args, &block
# ignore all messages you don't care about
end
end
class Spec::Story::Runner::ScenarioRunner
def initialize
@listeners = [DatabaseResetListener.instance, CookieResetListener.instance]
end
end
class Spec::Story::GivenScenario
def perform(instance, name = nil)
scenario = Spec::Story::Runner::StoryRunner.scenario_from_current_story @name
runner = Spec::Story::Runner::ScenarioRunner.new
runner.instance_variable_set(:@listeners,[])
runner.run(scenario, instance)
end
end
require 'spec/rails/story_adapter'

View file

@ -223,8 +223,8 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
login_as :admin_user
u = users(:admin_user)
post :actionize, :state => "active", :format => 'js'
assert_equal 1, projects(:gardenclean).position
assert_equal 2, projects(:moremoney).position
assert_equal 2, projects(:gardenclean).position
assert_equal 1, projects(:moremoney).position
assert_equal 3, projects(:timemachine).position
end

View file

@ -184,7 +184,7 @@ class TodosControllerTest < Test::Rails::TestCase
assert_xml_select 'rss[version="2.0"]' do
assert_select 'channel' do
assert_select '>title', 'Tracks Actions'
assert_select '>title', 'Actions'
assert_select '>description', "Actions for #{users(:admin_user).display_name}"
assert_select 'language', 'en-us'
assert_select 'ttl', '40'
@ -205,7 +205,7 @@ class TodosControllerTest < Test::Rails::TestCase
assert_xml_select 'rss[version="2.0"]' do
assert_select 'channel' do
assert_select '>title', 'Tracks Actions'
assert_select '>title', 'Actions'
assert_select '>description', "Actions for #{users(:admin_user).display_name}"
assert_select 'item', 5 do
assert_select 'title', /.+/
@ -240,7 +240,7 @@ class TodosControllerTest < Test::Rails::TestCase
# #puts @response.body
assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do
assert_xml_select '>title', 'Tracks Actions'
assert_xml_select '>title', 'Actions'
assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}"
assert_xml_select 'entry', 11 do
assert_xml_select 'title', /.+/
@ -450,7 +450,7 @@ class TodosControllerTest < Test::Rails::TestCase
# check that the new_todo is in the tickler to show next month
assert !new_todo.show_from.nil?
assert_equal Time.utc(today.year, today.month+1, today.day), new_todo.show_from
assert_equal Time.utc(today.year, today.month, today.day)+1.month, new_todo.show_from
end
def test_check_for_next_todo

View file

@ -20,8 +20,9 @@ class ContextXmlApiTest < ActionController::IntegrationTest
end
def test_fails_with_invalid_xml_format
authenticated_post_xml_to_context_create "<foo></bar>"
assert_equal 500, @integration_session.status
# Fails too hard for test to catch
# authenticated_post_xml_to_context_create "<foo></bar>"
# assert_equal 500, @integration_session.status
end
def test_fails_with_invalid_xml_format2

View file

@ -20,8 +20,9 @@ class ProjectXmlApiTest < ActionController::IntegrationTest
end
def test_fails_with_invalid_xml_format
authenticated_post_xml_to_project_create "<foo></bar>"
assert_equal 500, @integration_session.status
#Fails too hard for test to catch
# authenticated_post_xml_to_project_create "<foo></bar>"
# assert_equal 500, @integration_session.status
end
def test_fails_with_invalid_xml_format2

View file

@ -28,11 +28,12 @@ class UsersXmlApiTest < ActionController::IntegrationTest
authenticated_post_xml_to_user_create @@foobar_postdata, users(:admin_user).login, 'abracadabra', {'CONTENT_TYPE' => "application/x-www-form-urlencoded"}
assert_404_invalid_xml
end
def test_fails_with_invalid_xml_format
authenticated_post_xml_to_user_create "<foo></bar>"
assert_equal 500, @integration_session.status
end
# Fails too hard for test to catch
# def test_fails_with_invalid_xml_format
# authenticated_post_xml_to_user_create "<foo></bar>"
# assert_equal 500, @integration_session.status
# end
def test_fails_with_invalid_xml_format2
authenticated_post_xml_to_user_create "<request><username>foo</username></request>"

View file

@ -18,4 +18,7 @@ click "css=#toggle_c1"
wait_for_not_visible "todo_9"
# check that newly added action is not there
wait_for_not_visible "xpath=//span[text()='a new action']"
wait_for_not_visible "xpath=//span[text()='a new action']"
# toggle c1 back. Selenium does not reset cookies
click "css=#toggle_c1"

View file

@ -3,7 +3,7 @@ login :as => 'admin'
open "/"
# we should start with 10 actions on home page
assert_text 'badge_count', '10'
assert_text 'badge_count', '11'
# set project to hidden state
open "/projects/2"
@ -28,4 +28,4 @@ wait_for_visible "flash"
verify_text_not_present 'should be hidden'
# badge count should still be same
assert_text 'badge_count', '7'
assert_text 'badge_count', '7'

View file

@ -6,4 +6,4 @@ wait_for_element_present "show_from_todo_9"
type "show_from_todo_9", "1/1/2030"
click "css=#submit_todo_9"
wait_for_element_not_present "todo_9"
assert_text 'badge_count', '9'
assert_text 'badge_count', '10'

View file

@ -6,5 +6,5 @@ wait_for_element_present "show_from_todo_5"
type "show_from_todo_5", "1/1/2030"
click "css=#submit_todo_5"
wait_for_element_not_present "todo_5"
assert_text 'badge_count', '9'
assert_text 'badge_count', '10'
wait_for_not_visible "c5"

View file

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

View file

@ -2,7 +2,7 @@ setup :fixtures => :all
login :as => 'admin'
open '/m'
wait_for_text 'css=h1 span.count', '10'
wait_for_text 'css=h1 span.count', '11'
click_and_wait "link=0-Add new action"
@ -15,4 +15,4 @@ select "todo_due_2i", "label=January"
select "todo_due_1i", "label=2009"
click_and_wait "//input[@value='Create']"
wait_for_text 'css=h1 span.count', '11'
wait_for_text 'css=h1 span.count', '12'

View file

@ -2,10 +2,12 @@ setup :fixtures => :all
login :as => 'admin'
open '/m'
wait_for_text 'css=h1 span.count', '10'
wait_for_text 'css=h1 span.count', '11'
open '/todos/6.m'
wait_for_page_to_load 3000
open_and_wait '/todos/6.m'
click "done"
click_and_wait "//input[@value='Update']"
wait_for_text 'css=h1 span.count', '9'
wait_for_text 'css=h1 span.count', '10'

View file

@ -4,14 +4,14 @@ login :as => 'admin'
# open home page
open '/m'
wait_for_title "All actions"
wait_for_text 'css=h1 span.count', '10'
wait_for_text 'css=h1 span.count', '11'
# open context page
click_and_wait "link=2-Contexts"
# verify_title "All actions in context agenda"
# choose agenda context
click_and_wait "link=agenda"
wait_for_text 'css=h1 span.count', '5'
wait_for_text 'css=h1 span.count', '6'
# click on tag foo to go to tag page
click_and_wait "link=foo"

View file

@ -1,7 +1,7 @@
setup :fixtures => :all
login :as => 'admin'
open '/projects/1'
assert_checked 'project_state_active', 'ignored'
assert_checked 'project_state_active'
assert_attribute 'css=#project_status .active span', 'class', 'active_state'
assert_attribute 'css=#project_status .hidden span', 'class', 'inactive_state'
assert_text 'badge_count', '2'
@ -11,4 +11,4 @@ wait_for_attribute 'css=#project_status .hidden span', 'class', 'active_state'
assert_text 'badge_count', '2'
open '/projects/1'
assert_text 'badge_count', '2'
assert_checked 'project_state_hidden', 'ignored'
assert_checked 'project_state_hidden'

View file

@ -0,0 +1,13 @@
setup :fixtures => :all
login :as => 'admin'
open "/projects/1"
# change project name
click "project_name_in_place_editor"
wait_for_element_present "css=#project_name_in_place_editor-inplaceeditor input.editor_field"
type "css=#project_name_in_place_editor-inplaceeditor input.editor_field", "Test Foo"
click "css=#project_name_in_place_editor-inplaceeditor input.editor_ok_button"
wait_for_text "project_name_in_place_editor", "Test Foo"
# check that the default project name is changed too
assert_value "todo_project_name", "Test Foo"

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