Implemented a feature that give a project and optional default context. When set,

this context will be pre-populated when creating an action from the project's page.
When creating an action from the home page, the context will be auto-selected when
the project is selected if the context field has not yet been entered.

This implementation is a combination of the great patch submitted by James Kebinger
(thanks James!) and some of my modifications and additions.

Don't forget to rake db:migrate.

Fixes #162, originally suggested by Rolf one year ago!

Also in this commit:

 * Tweaked selenium tags test
 * Tweaked formatting of next/previous project HTML
 * Implemented Null Object pattern for context to support
   a Project having no default context
 * Removed tickler.rhtml, no longer in use
 * applying z-index values to project sortable list items (otherwise context
   autocomplete was appearing below next list item)
 * Swapped order of project and context in new action form (setting default context
   makes more sense this way)
 * Removed CSS width of for form elements, so form could be used in content area
   without being too narrow
   


git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@480 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
lukemelia 2007-03-21 07:12:14 +00:00
parent 11ed78abe2
commit 38b2e336a8
21 changed files with 189 additions and 75 deletions

View file

@ -111,6 +111,10 @@ class ApplicationController < ActionController::Base
def markdown(text)
RedCloth.new(text).to_html
end
def build_default_project_context_name_map(projects)
Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
end
protected

View file

@ -162,6 +162,7 @@ class ContextsController < ApplicationController
# Hides actions in hidden projects from context.
@not_done_todos = @context.todos.find(:all, :conditions => ['todos.state = ?', 'active'], :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => :project)
@count = @not_done_todos.size
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end
end

View file

@ -3,6 +3,7 @@ class ProjectsController < ApplicationController
helper :application, :todos, :notes
before_filter :init, :except => [:create, :destroy, :order]
before_filter :check_user_set_project, :only => [:update, :destroy, :show]
before_filter :default_context_filter, :only => [:create,:update]
skip_before_filter :login_required, :only => [:index]
prepend_before_filter :login_or_feed_token_required, :only => [:index]
session :off, :only => :index, :if => Proc.new { |req| ['rss','atom','txt'].include?(req.parameters[:format]) }
@ -28,6 +29,7 @@ class ProjectsController < ApplicationController
@count = @not_done.size
@next_project = @user.projects.next_from(@project)
@previous_project = @user.projects.previous_from(@project)
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end
# Example XML usage: curl -H 'Accept: application/xml' -H 'Content-Type: application/xml'
@ -50,6 +52,7 @@ class ProjectsController < ApplicationController
@saved = @project.save
@project_not_done_counts = { @project.id => 0 }
@active_projects_count = @user.projects.count(:conditions => "state = 'active'")
@contexts = @user.contexts
respond_to do |wants|
wants.js
wants.xml do
@ -93,6 +96,8 @@ class ProjectsController < ApplicationController
render
elsif boolean_param('update_status')
render :action => 'update_status'
elsif boolean_param('update_default_context')
render :action => 'update_default_context'
else
render :text => success_text || 'Success'
end
@ -181,6 +186,19 @@ class ProjectsController < ApplicationController
@done = @user.todos.find_in_state(:all, :completed, :order => "completed_at DESC")
init_data_for_sidebar
end
def default_context_filter
p = params['project']
p = params['request']['project'] if p.nil? && params['request']
p = {} if p.nil?
default_context_name = p['default_context_name']
p.delete('default_context_name')
unless default_context_name.blank?
default_context = Context.find_or_create_by_name(default_context_name)
p['default_context_id'] = default_context.id
end
end
def summary(project)
project_description = ''

View file

@ -224,6 +224,7 @@ class TodosController < ApplicationController
@page_title = "TRACKS::Tickler"
@tickles = @user.deferred_todos
@count = @tickles.size
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end
# Check for any due tickler items, activate them
@ -262,6 +263,7 @@ class TodosController < ApplicationController
@done = @user.completed_todos.find(:all, :limit => max_completed, :include => [ :context, :project, :tags ]) unless max_completed == 0
# Set count badge to number of items with this tag
@not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end
@ -421,7 +423,9 @@ class TodosController < ApplicationController
# Set count badge to number of not-done, not hidden context items
@count = @todos.reject { |x| !x.active? || x.context.hide? }.size
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
render
end
end

View file

@ -23,7 +23,7 @@ module ProjectsHelper
project_name = truncate(@previous_project.name, 40, "...")
html << link_to_project(@previous_project, "&laquo; #{project_name}")
end
html << '|' if @previous_project && @next_project
html << ' | ' if @previous_project && @next_project
unless @next_project.nil?
project_name = truncate(@next_project.name, 40, "...")
html << link_to_project(@next_project, "#{project_name} &raquo;")

View file

@ -22,6 +22,10 @@ class Context < ActiveRecord::Base
:description => "Lists all the contexts for #{user.display_name}"
}
end
def self.null_object
NullContext.new
end
def hidden?
self.hide == true || self.hide == 1
@ -43,3 +47,19 @@ class Context < ActiveRecord::Base
end
end
class NullContext
def nil?
true
end
def id
nil
end
def name
''
end
end

View file

@ -1,6 +1,7 @@
class Project < ActiveRecord::Base
has_many :todos, :dependent => :delete_all, :include => :context
has_many :notes, :dependent => :delete_all, :order => "created_at DESC"
belongs_to :default_context, :dependent => :nullify, :class_name => "Context", :foreign_key => "default_context_id"
belongs_to :user
validates_presence_of :name, :message => "project must have a name"
@ -65,7 +66,13 @@ class Project < ActiveRecord::Base
end
end
end
alias_method :original_default_context, :default_context
def default_context
original_default_context.nil? ? Context.null_object : original_default_context
end
# would prefer to call this method state=(), but that causes an endless loop
# as a result of acts_as_state_machine calling state=() to update the attribute
def transition_to(candidate_state)
@ -96,5 +103,5 @@ class NullProject
def id
nil
end
end

View file

@ -0,0 +1,6 @@
<div class="page_name_auto_complete" id="default_context_list" style="display:none;z-index:9999"></div>
<script type="text/javascript">
defaultContextAutoCompleter = new Autocompleter.Local('project[default_context_name]', 'default_context_list', <%= context_names_for_autocomplete %>, {choices:100,autoSelect:true});
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

@ -17,3 +17,10 @@
<% end %>
</td>
</tr>
<tr>
<td width="150"><label for="project[default_context_name]">Default Context</label></td>
<td width="300">
<%= text_field_tag("project[default_context_name]", @project.default_context.name, {:tabindex=>1,:size=> 25}) %>
<%= render :partial => 'default_context_autocomplete' %>
</td>
</tr>

View file

@ -1,6 +1,8 @@
<% project = project_listing %>
<div id="<%= dom_id(project, "container") %>" class="list">
<div id="<%= dom_id(project) %>" class="project sortable_row" style="display:'';">
<% project = project_listing
@project_listing_zindex = @project_listing_zindex.nil? ? 200 : @project_listing_zindex - 1
-%>
<div id="<%= dom_id(project, "container") %>" class="list" style="z-index:<%= @project_listing_zindex %>">
<div id="<%= dom_id(project) %>" class="project sortable_row" style="display:''">
<div class="position">
<span class="handle">DRAG</span>
</div>

View file

@ -21,14 +21,19 @@
<div id="status"><%= error_messages_for('project') %></div>
<label for="project_name">Name:</label><br />
<%= text_field 'project', 'name' %><br />
<%= text_field 'project', 'name', "tabindex" => 1 %><br />
<label for="project_description">Description (optional):</label><br />
<%= text_area 'project', 'description', "cols" => 30, "rows" => 4 %><br />
<%= check_box_tag 'go_to_project' %><label for="go_to_project">Go to the project page</label><br />
<%= text_area 'project', 'description', "cols" => 30, "rows" => 4, "tabindex" => 2 %><br />
<input type="submit" value="Add" />
<label for="default_context_name">Default Context (optional):</label><br />
<%= text_field_tag("project[default_context_name]", @project.default_context.name, :tabindex => 3) %>
<%= render :partial => 'default_context_autocomplete' %>
<br />
<input type="submit" value="Add Project" tabindex="4" /><br />
<input id="go_to_project" type="checkbox" tabindex="5" name="go_to_project"/><label for="go_to_project"> Take me to the new project page</label><br />
<% end -%>
</div>

View file

@ -18,21 +18,21 @@
<%= render :partial => "notes/notes_summary", :collection => @project.notes %>
</div>
</div>
<div>
<div id="new-note" style="display:none;">
<% form_remote_tag :url => notes_path,
:method => :post,
<div id="new-note" style="display:none;">
<% form_remote_tag :url => notes_path,
:method => :post,
:update => "notes",
:position => "bottom",
:complete => "new Effect.Highlight('notes');$('empty-n').hide();",
:html => {:id=>'form-new-note', :class => 'inline-form'} do %>
<%= hidden_field( "new_note", "project_id", "value" => "#{@project.id}" ) %>
<%= text_area( "new_note", "body", "cols" => 50, "rows" => 3, "tabindex" => 1 ) %>
<br /><br />
<input type="submit" value="Add note" tabindex="2" />
<% end -%>
</div>
<%= hidden_field( "new_note", "project_id", "value" => "#{@project.id}" ) %>
<%= text_area( "new_note", "body", "cols" => 50, "rows" => 3, "tabindex" => 1 ) %>
<br /><br />
<input type="submit" value="Add note" tabindex="2" />
<% end -%>
</div>
<div class="container">
<div id="project_status">
@ -49,6 +49,24 @@
</div>
</div>
</div>
<div class="container">
<div id="default_context">
<h2>Default Context</h2>
<div>
<% form_remote_tag( :url => project_path(@project), :method => :put,
:html=> { :id => 'set-default-context-action',
:name => 'default_context',
:class => 'inline-form' }) do -%>
<%= hidden_field_tag("update_default_context", true) %>
<%= text_field_tag("project[default_context_name]",
@project.default_context.name,
{ :tabindex => 9,:size => 25 }) %>
<%= submit_tag "Set Default Context for this Project", { :tabindex => 10 } %>
<%= render :partial => 'default_context_autocomplete' %>
<% end -%>
</div>
</div>
</div>
</div><!-- [end:display_box] -->

View file

@ -0,0 +1,10 @@
if @project.default_context.nil?
page.notify :notice, "Removed default context", 5.0
else
if source_view_is :project
page['todo_context_name'].value = @project.default_context.name
end
page.notify :notice, "Set project's default context to #{@project.default_context.name}", 5.0
end
page.hide "busy"

View file

@ -8,3 +8,4 @@ page.select('#project_status .completed span').each do |element|
element.className = @project.current_state == :completed ? 'active_state' : 'inactive_state'
end
page.notify :notice, "Set project status to #{@project.current_state}", 5.0
page.hide 'busy'

View file

@ -1,6 +1,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_project_name = @project.name unless @project.nil?
%>
@ -24,24 +25,34 @@
<label for="todo_notes">Notes</label>
<%= text_area( "todo", "notes", "cols" => 25, "rows" => 6, "tabindex" => 2) %>
<label for="todo_context_name">Context</label>
<input id="todo_context_name" name="context_name" autocomplete="off" tabindex="3" size="25" type="text" value="<%= @initial_context_name %>" />
<div class="page_name_auto_complete" id="context_list" style="display:none"></div>
<script type="text/javascript">
contextAutoCompleter = new Autocompleter.Local('todo_context_name', 'context_list', <%= context_names_for_autocomplete %>, {choices:100,autoSelect:true});
Event.observe($('todo_context_name'), "focus", contextAutoCompleter.activate.bind(contextAutoCompleter));
Event.observe($('todo_context_name'), "click", contextAutoCompleter.activate.bind(contextAutoCompleter));
</script>
<label for="todo_project_name">Project</label>
<input id="todo_project_name" name="project_name" autocomplete="off" tabindex="4" size="25" type="text" value="<%= @initial_project_name %>" />
<input id="todo_project_name" name="project_name" autocomplete="off" tabindex="3" size="25" type="text" value="<%= @initial_project_name %>" />
<div class="page_name_auto_complete" id="project_list" style="display:none"></div>
<script type="text/javascript">
projectAutoCompleter = new Autocompleter.Local('todo_project_name', 'project_list', <%= project_names_for_autocomplete %>, {choices:100,autoSelect:true});
function selectDefaultContext() {
todoContextNameElement = $('todo_context_name');
defaultContextName = todoContextNameElement.projectDefaultContextsMap[this.value];
if (defaultContextName && !todoContextNameElement.editedByTracksUser) {
todoContextNameElement.value = defaultContextName;
}
}
Event.observe($('todo_project_name'), "focus", projectAutoCompleter.activate.bind(projectAutoCompleter));
Event.observe($('todo_project_name'), "click", projectAutoCompleter.activate.bind(projectAutoCompleter));
Event.observe($('todo_project_name'), "blur", selectDefaultContext.bind($('todo_project_name')));
</script>
<label for="todo_context_name">Context</label>
<input id="todo_context_name" name="context_name" autocomplete="off" tabindex="4" size="25" type="text" value="<%= @initial_context_name %>" />
<div class="page_name_auto_complete" id="context_list" style="display:none"></div>
<script type="text/javascript">
contextAutoCompleter = new Autocompleter.Local('todo_context_name', 'context_list', <%= context_names_for_autocomplete %>, {choices:100,autoSelect:true});
$('todo_context_name').projectDefaultContextsMap = <%= @default_project_context_name_map %>;
Event.observe($('todo_context_name'), "focus", function(){ $('todo_context_name').editedByTracksUser = true; });
Event.observe($('todo_context_name'), "focus", contextAutoCompleter.activate.bind(contextAutoCompleter));
Event.observe($('todo_context_name'), "click", contextAutoCompleter.activate.bind(contextAutoCompleter));
</script>
<label for="tag_list">Tags (separate with commas)</label>

View file

@ -1,21 +0,0 @@
<div id="display_box">
<div id="tickler" class="container project">
<h2>Deferred actions</h2>
<div id="tickler-items" class="items toggle_target">
<div id="tickler-empty-nd" style="display:<%= @tickles.empty? ? 'block' : 'none'%>;">
<div class="message"><p>Currently there are no deferred actions</p></div>
</div>
<%= render :partial => "todos/todo", :collection => @tickles, :locals => { :parent_container_type => 'tickler' } %>
</div><!-- [end:items] -->
</div><!-- [end:tickler] -->
</div><!-- End of display_box -->
<div id="input_box">
<%= render :partial => "shared/add_new_item_form" %>
<%= render "sidebar/sidebar" %>
</div><!-- End of input box -->

View file

@ -0,0 +1,9 @@
class AddDefaultContextToProject < ActiveRecord::Migration
def self.up
add_column :projects, :default_context_id, :integer
end
def self.down
remove_column :projects, :default_context_id, :integer
end
end

View file

@ -2,17 +2,18 @@
# migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
ActiveRecord::Schema.define(:version => 30) do
ActiveRecord::Schema.define(:version => 31) do
create_table "contexts", :force => true do |t|
t.column "name", :string, :default => "", :null => false
t.column "position", :integer, :default => 0, :null => false
t.column "name", :string, :default => "", :null => false
t.column "hide", :boolean, :default => false
t.column "user_id", :integer, :default => 1
t.column "position", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 0, :null => false
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
add_index "contexts", ["user_id"], :name => "index_contexts_on_user_id"
add_index "contexts", ["user_id", "name"], :name => "index_contexts_on_user_id_and_name"
create_table "notes", :force => true do |t|
@ -63,15 +64,17 @@ ActiveRecord::Schema.define(:version => 30) do
add_index "preferences", ["user_id"], :name => "index_preferences_on_user_id"
create_table "projects", :force => true do |t|
t.column "name", :string, :default => "", :null => false
t.column "position", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 1
t.column "description", :text
t.column "state", :string, :limit => 20, :default => "active", :null => false
t.column "created_at", :datetime
t.column "updated_at", :datetime
t.column "name", :string, :default => "", :null => false
t.column "position", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 0, :null => false
t.column "description", :text
t.column "state", :string, :limit => 20, :default => "active", :null => false
t.column "created_at", :datetime
t.column "updated_at", :datetime
t.column "default_context_id", :integer
end
add_index "projects", ["user_id"], :name => "index_projects_on_user_id"
add_index "projects", ["user_id", "name"], :name => "index_projects_on_user_id_and_name"
create_table "sessions", :force => true do |t|
@ -100,16 +103,16 @@ ActiveRecord::Schema.define(:version => 30) do
add_index "tags", ["name"], :name => "index_tags_on_name"
create_table "todos", :force => true do |t|
t.column "context_id", :integer, :default => 0, :null => false
t.column "project_id", :integer
t.column "description", :string, :default => "", :null => false
t.column "context_id", :integer, :default => 0, :null => false
t.column "description", :string, :limit => 100, :default => "", :null => false
t.column "notes", :text
t.column "created_at", :datetime
t.column "due", :date
t.column "completed_at", :datetime
t.column "user_id", :integer, :default => 1
t.column "project_id", :integer
t.column "user_id", :integer, :default => 0, :null => false
t.column "show_from", :date
t.column "state", :string, :limit => 20, :default => "immediate", :null => false
t.column "state", :string, :limit => 20, :default => "immediate", :null => false
end
add_index "todos", ["user_id", "state"], :name => "index_todos_on_user_id_and_state"
@ -119,10 +122,10 @@ ActiveRecord::Schema.define(:version => 30) do
add_index "todos", ["user_id", "context_id"], :name => "index_todos_on_user_id_and_context_id"
create_table "users", :force => true do |t|
t.column "login", :string, :limit => 80, :default => "", :null => false
t.column "password", :string, :limit => 40, :default => "", :null => false
t.column "login", :string, :limit => 80
t.column "password", :string, :limit => 40
t.column "word", :string
t.column "is_admin", :boolean, :default => false, :null => false
t.column "is_admin", :integer, :limit => 4, :default => 0, :null => false
t.column "first_name", :string
t.column "last_name", :string
t.column "auth_type", :string, :default => "database", :null => false

View file

@ -313,6 +313,10 @@ div#project_status > div {
#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;}
@ -557,7 +561,6 @@ form {
border: 1px solid #CCC;
padding: 10px;
margin: 0px;
width: 313px;
}
.inline-form {
border: none;

View file

@ -1,6 +1,5 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/todos/tag/foo"
wait_for_element_present "xpath=//div[@id='t'] //h2"
assert_not_visible "t_empty-nd"
wait_for_element_present "xpath=//div[@id='c1'] //h2"
wait_for_text 'badge_count', '2'

View file

@ -192,5 +192,12 @@ class ProjectTest < Test::Unit::TestCase
@moremoney.todos[0].complete!
assert_equal 2, @moremoney.not_done_todo_count
end
def test_default_context_name
p = Project.new
assert_equal '', p.default_context.name
p.default_context = contexts(:agenda)
assert_equal 'agenda', p.default_context.name
end
end