Convert project_controller to use RESTful routes (required renaming project_controller -> projects_controller per rails convention).

This change also changes project detail page URLs from /project/my_project to /projects/my_project
Benefits include
 * built-in enforcement of proper HTTP methods
 * simple named routes (e.g. project_url(project) instead of { :controller => "project", :action => "show", :url_friendly_name => project.url_friendly_name })
 * built-in routes for formats like .js and .xml
 
Also fixed a http basic authentication bug that I introduced a couple of days ago
Add a database index on the users table, login column, to speed authentication
Made more use of simply_helpful dom_id() method and simplified _project_listing.rhtml




git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@398 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
lukemelia 2007-01-12 13:46:45 +00:00
parent e9e6c33599
commit 8b62e4c784
33 changed files with 128 additions and 131 deletions

View file

@ -3,6 +3,7 @@ class LoginController < ApplicationController
layout 'login'
skip_before_filter :set_session_expiration
skip_before_filter :login_required
before_filter :get_current_user
open_id_consumer if Tracks::Config.auth_schemes.include?('open_id')
def login

View file

@ -1,7 +1,7 @@
class ProjectController < ApplicationController
class ProjectsController < ApplicationController
helper :todo
before_filter :init, :except => [:create, :destroy, :order, :toggle_project_done]
before_filter :init, :except => [:create, :destroy, :order]
def index
init_project_hidden_todo_counts
@ -12,9 +12,6 @@ class ProjectController < ApplicationController
end
end
# Filter the projects to show just the one passed in the URL
# e.g. <home>/project/show/<project_name> shows just <project_name>.
#
def show
check_user_set_project
@page_title = "TRACKS::Project: #{@project.name}"
@ -27,7 +24,7 @@ class ProjectController < ApplicationController
# Example XML usage: curl -H 'Accept: application/xml' -H 'Content-Type: application/xml'
# -u username:password
# -d '<request><project><name>new project_name</name></project></request>'
# http://our.tracks.host/project/create
# http://our.tracks.host/projects
#
def create
if params[:format] == 'application/xml' && params['exception']
@ -90,17 +87,6 @@ class ProjectController < ApplicationController
end
end
# Toggles the 'done' status of a project
#
def toggle_project_done
check_user_set_project
@project.toggle!('done')
if @project.save
redirect_to :action => 'index'
end
end
# Delete a project
#
def destroy
@ -129,8 +115,10 @@ class ProjectController < ApplicationController
def check_user_set_project
if params["url_friendly_name"]
@project = @user.projects.find_by_url_friendly_name(params["url_friendly_name"])
elsif params['id']
elsif params['id'] && params['id'] =~ /\d+/
@project = @user.projects.find(params["id"])
elsif params['id']
@project = @user.projects.find_by_url_friendly_name(params["id"])
else
redirect_to :action => 'index'
end

View file

@ -133,7 +133,7 @@ module ApplicationHelper
end
def link_to_project(project, descriptor = sanitize(project.name))
link_to( descriptor, { :controller => "project", :action => "show", :url_friendly_name => project.url_friendly_name }, :title => "View project: #{project.name}" )
link_to( descriptor, project_url(project), :title => "View project: #{project.name}" )
end
def item_link_to_context(item)

View file

@ -1,11 +1,11 @@
module ProjectHelper
module ProjectsHelper
def get_listing_sortable_options
{
:tag => 'div',
:handle => 'handle',
:complete => visual_effect(:highlight, 'list-projects'),
:url => {:controller => 'project', :action => 'order'}
:url => order_projects_path
}
end

View file

@ -37,6 +37,10 @@ class Project < ActiveRecord::Base
NullProject.new
end
def to_param
url_friendly_name
end
def description_present?
attribute_present?("description")
end

View file

@ -42,7 +42,7 @@
<ul id="navlist">
<li><%= navigation_link("Home", {:controller => "todo", :action => "index"}, {:accesskey => "t", :title => "Home"} ) %></li>
<li><%= navigation_link( "Contexts", {:controller => "context", :action => "index"}, {:accesskey=>"c", :title=>"Contexts"} ) %></li>
<li><%= navigation_link( "Projects", {:controller => "project", :action => "index"}, {:accesskey=>"p", :title=>"Projects"} ) %></li>
<li><%= navigation_link( "Projects", projects_path, {:accesskey=>"p", :title=>"Projects"} ) %></li>
<li><%= navigation_link( "Tickler", {:controller => "todo", :action => "tickler"}, :title => "Tickler" ) %></li>
<li><%= navigation_link( "Done", {:controller => "todo", :action => "completed"}, {:accesskey=>"d", :title=>"Completed"} ) %></li>
<li><%= navigation_link( "Notes", {:controller => "note", :action => "index"}, {:accesskey => "o", :title => "Show all notes"} ) %></li>

View file

@ -1,47 +0,0 @@
<% project = project_listing %>
<div id="container_<%= project.id %>" class="list">
<!-- %= error_messages_for 'project' % -->
<div id="project-<%= project.id %>" class="even_row" style="display:'';">
<div class="position">
<span class="handle">DRAG</span>
</div>
<div class="data">
<%= link_to_project( project ) %><%= " (" + count_undone_todos(project,"actions") + ")" %>
</div>
<div class="buttons">
<% if project.completed? -%>
<span class="grey">COMPLETED</span>
<% elsif project.hidden? -%>
<span class="grey">HIDDEN</span>
<% else -%>
<span class="grey">ACTIVE</span>
<% end -%>
<%= link_to_remote( image_tag("blank.png", :title =>"Delete project", :class=>"delete_item"),
:update => "container_#{project.id}",
:loading => visual_effect(:fade, "container_#{project.id}"),
:url => { :controller => "project", :action => "destroy", :id => project.id },
:confirm => "Are you sure that you want to delete the project \'#{project.name}\'?" ) + " " +
link_to_function(image_tag( "blank.png", :title => "Edit item", :class=>"edit_item"), "Element.toggle('project-#{project.id}'); Element.toggle('project-#{project.id}-edit-form'); new Effect.Appear('project-#{project.id}-edit-form'); Form.focusFirstElement('form-project-#{project.id}');" ) %>
</div>
</div><!-- [end:project-project.id] -->
<div id="project-<%= project.id %>-edit-form" class="edit-form" style="display:none;">
<% form_remote_tag :url => { :controller => 'project', :action => 'update', :id => project.id },
:html => { :id => "form-project-#{project.id}", :class => "form" },
:complete => visual_effect(:appear, 'container_#{project.id}') do -%>
<table style="table-layout: fixed;" width="450">
<%= render :partial => 'project_form', :object => project %>
<tr>
<td width="150">&nbsp; <input type="hidden" name="wants_render" value="false" /></td>
<td width="300"><input type="submit" value="Update" />&nbsp;<a href="javascript:void(0);" onclick="Element.toggle('project-<%= project.id %>'); Element.toggle('project-<%= project.id %>-edit-form');Form.reset('form-project-<%= project.id %>');">Cancel</a></td>
</tr>
</table>
<% end -%>
</div><!-- [end:project-project.id-edit-form] -->
</div><!-- [end:container_project.id] -->
<% if controller.action_name == 'create' %>
<script>
new Effect.Appear('project-<%= project.id %>');
</script>
<% end %>

View file

@ -1,29 +0,0 @@
<div id="display_box">
<div id="list-projects">
<%= render :partial => 'project_listing', :collection => @projects %>
</div>
<%= sortable_element 'list-projects', get_listing_sortable_options %>
</div>
<div id="input_box">
<a href="#" onClick="Element.toggle('project_new'); Form.focusFirstElement('project-form');return false" accesskey="n" title="Create a new project">Create new project &#187;</a>
<div id="project_new" class="project_new" style="display:none">
<!--[form:project]-->
<% form_remote_tag :url => { :action => "create" },
:html=> { :id=>'project-form', :name=>'project', :class => 'inline-form' } do -%>
<div id="status"><%= error_messages_for('project') %></div>
<label for="project_name">Name:</label>
<br />
<%= text_field 'project', 'name' %>
<br />
<label for="project_description">Description (optional):</label>
<br />
<%= text_area 'project', 'description', "cols" => 30, "rows" => 4 %>
<br />
<input type="submit" value="Add" />
<% end -%>
<!--[eoform:project]-->
</div>
</div>

View file

@ -0,0 +1,42 @@
<% project = project_listing %>
<div id="<%= dom_id(project, "container") %>" class="list">
<div id="<%= dom_id(project) %>" class="even_row" style="display:'';">
<div class="position">
<span class="handle">DRAG</span>
</div>
<div class="data">
<%= link_to_project( project ) %><%= " (" + count_undone_todos(project,"actions") + ")" %>
</div>
<div class="buttons">
<span class="grey"><%= project.current_state.to_s.upcase %></span>
<%= link_to_remote( image_tag("blank.png", :title =>"Delete project", :class=>"delete_item"),
:update => dom_id(project, "container"),
:loading => visual_effect(:fade, dom_id(project, "container")),
:url => project_path(project),
:method => :delete,
:confirm => "Are you sure that you want to delete the project \'#{project.name}\'?" ) %>
<%= link_to_function( image_tag("blank.png", :title => "Edit item", :class=>"edit_item"),
"Element.toggle('#{dom_id(project)}'); Element.toggle('#{dom_id(project, 'edit')}'); new Effect.Appear('#{dom_id(project, 'edit')}'); Form.focusFirstElement('#{dom_id(project, "edit_form")}');" ) %>
</div>
</div>
<div id="<%= dom_id(project, 'edit') %>" class="edit-form" style="display:none;">
<% form_remote_tag :url => project_path(project),
:method => :put,
:html => { :id => dom_id(project, 'edit_form'), :class => "form" },
:complete => visual_effect(:appear, dom_id(project, 'container')) do -%>
<table style="table-layout: fixed;" width="450">
<%= render :partial => 'project_form', :object => project %>
<tr>
<td width="150">&nbsp; <input type="hidden" name="wants_render" value="false" /></td>
<td width="300"><input type="submit" value="Update" />&nbsp;<a href="javascript:void(0);" onclick="Element.toggle('<%= dom_id(project) %>'); Element.toggle('<%= dom_id(project, 'edit') %>');Form.reset('<%= dom_id(project, 'edit_form') %>');">Cancel</a></td>
</tr>
</table>
<% end -%>
</div>
</div>
<% if controller.action_name == 'create' %>
<script>
new Effect.Appear('<%= dom_id(project) %>');
</script>
<% end %>

View file

@ -0,0 +1,27 @@
<div id="display_box">
<div id="list-projects">
<%= render :partial => 'project_listing', :collection => @projects %>
</div>
<%= sortable_element 'list-projects', get_listing_sortable_options %>
</div>
<div id="input_box">
<a href="#" onClick="Element.toggle('project_new'); Form.focusFirstElement('project-form');return false" accesskey="n" title="Create a new project">Create new project &#187;</a>
<div id="project_new" class="project_new" style="display:none">
<% form_remote_tag :url => projects_path, :method => :post,
:html=> { :id=>'project-form', :name=>'project', :class => 'inline-form' } do -%>
<div id="status"><%= error_messages_for('project') %></div>
<label for="project_name">Name:</label><br />
<%= text_field 'project', 'name' %><br />
<label for="project_description">Description (optional):</label><br />
<%= text_area 'project', 'description', "cols" => 30, "rows" => 4 %><br />
<input type="submit" value="Add" />
<% end -%>
</div>
</div>

View file

@ -1,6 +1,6 @@
<div id="display_box">
<%= render :partial => "project/project", :locals => { :project => @project, :collapsible => false } %>
<%= render :partial => "projects/project", :locals => { :project => @project, :collapsible => false } %>
<%= render :partial => "todo/deferred", :locals => { :deferred => @deferred, :collapsible => false, :append_descriptor => "in this project" } %>
<%= render :partial => "todo/completed", :locals => { :done => @done, :collapsible => false, :append_descriptor => "in this project" } %>
@ -35,7 +35,7 @@
<h2>Status</h2>
<div>
<% ['active', 'hidden', 'completed'].each do | state | %>
<% js = "new Ajax.Request('#{ url_for :controller => 'project', :action => 'update', :id => @project.id, :wants_render => false, :update_status => true, 'project[state]' => state }', {asynchronous:true, evalScripts:true});"
<% js = "new Ajax.Request('#{ url_for :controller => 'projects', :action => 'update', :id => @project.url_friendly_name, :wants_render => false, :update_status => true, 'project[state]' => state }', {method: 'put', asynchronous:true, evalScripts:true});"
span_class = @project.current_state.to_s == state ? 'active_state' : 'inactive_state'
%>
<span class="<%= state %>"><%= radio_button(:project, 'state', state, {:onclick => js}) %> <span class="<%= span_class %>"><%= state.titlecase %></span></span>

View file

@ -45,14 +45,8 @@ ActionController::Routing::Routes.draw do |map|
map.connect 'contexts/feed/:feedtype/:login/:token', :controller => 'feed', :action => 'list_contexts_only'
# Projects Routes
map.connect 'project/create', :controller => 'project', :action => 'create'
map.connect 'project/toggle_check/:id', :controller => 'project', :action => 'toggle_check'
map.connect 'project/order', :controller => 'project', :action => 'order'
map.resources :projects, :collection => {:order => :post}
map.connect 'project/:project/feed/:action/:login/:token', :controller => 'feed'
map.connect 'project/:id', :controller => 'project', :action => 'show', :requirements => {:id => /\d+/}
map.connect 'project/:url_friendly_name', :controller => 'project', :action => 'show'
map.connect 'projects', :controller => 'project', :action => 'index'
map.connect 'projects/feed/:feedtype/:login/:token', :controller => 'feed', :action => 'list_projects_only'
# Notes Routes

View file

@ -0,0 +1,9 @@
class IndexOnUserLogin < ActiveRecord::Migration
def self.up
add_index :users, :login
end
def self.down
remove_index :users, :login
end
end

View file

@ -2,7 +2,7 @@
# migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
ActiveRecord::Schema.define(:version => 22) do
ActiveRecord::Schema.define(:version => 23) do
create_table "contexts", :force => true do |t|
t.column "name", :string, :default => "", :null => false
@ -106,4 +106,6 @@ ActiveRecord::Schema.define(:version => 22) do
t.column "open_id_url", :string
end
add_index "users", ["login"], :name => "index_users_on_login"
end

View file

@ -53,6 +53,7 @@ module LoginSystem
http_user, http_pass = get_basic_auth_data
if user = User.authenticate(http_user, http_pass)
session['user_id'] = user.id
get_current_user
return true
end

View file

@ -1,15 +1,15 @@
require File.dirname(__FILE__) + '/../test_helper'
require File.dirname(__FILE__) + '/todo_container_controller_test_base'
require 'project_controller'
require 'projects_controller'
# Re-raise errors caught by the controller.
class ProjectController; def rescue_action(e) raise e end; end
class ProjectsController; def rescue_action(e) raise e end; end
class ProjectControllerTest < TodoContainerControllerTestBase
class ProjectsControllerTest < TodoContainerControllerTestBase
fixtures :users, :todos, :preferences, :projects
def setup
perform_setup(Project, ProjectController)
perform_setup(Project, ProjectsController)
end
def test_projects_list

View file

@ -1,10 +1,10 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'project_controller'
require 'projects_controller'
# Re-raise errors caught by the controller.
class ProjectController; def rescue_action(e) raise e end; end
class ProjectsController; def rescue_action(e) raise e end; end
class ProjectControllerXmlApiTest < ActionController::IntegrationTest
class ProjectsControllerXmlApiTest < ActionController::IntegrationTest
fixtures :users, :projects
@@project_name = "My New Project"
@ -56,15 +56,11 @@ class ProjectControllerXmlApiTest < ActionController::IntegrationTest
project1 = Project.find_by_name(@@project_name)
assert_not_nil project1, "expected project '#{@@project_name}' to be created"
end
def test_fails_with_get_verb
authenticated_get_xml "/project/create", users(:other_user).login, 'sesame', {}
end
private
def authenticated_post_xml_to_project_create(postdata = @@valid_postdata, user = users(:other_user).login, password = 'sesame')
authenticated_post_xml "/project/create", user, password, postdata
authenticated_post_xml "/projects", user, password, postdata
end
def assert_404_invalid_xml

View file

@ -1,8 +1,8 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
include_partial 'project_detail/add_deferred_todo'
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
click "edit_icon_todo_15"
wait_for_element_present "show_from_todo_15"
type "show_from_todo_15", ""

View file

@ -1,7 +1,6 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/project/Build_a_working_time_machine"
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
click "edit_icon_todo_15"
wait_for_element_present "show_from_todo_15"
type "show_from_todo_15", ""

View file

@ -1,6 +1,6 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open '/project/Build_a_working_time_machine'
open '/projects/Build_a_working_time_machine'
assert_checked 'project_state_active', 'ignored'
assert_attribute 'css=#project_status .active span', 'class', 'active_state'
assert_attribute 'css=#project_status .hidden span', 'class', 'inactive_state'
@ -9,6 +9,6 @@ click 'project_state_hidden'
wait_for_attribute 'css=#project_status .active span', 'class', 'inactive_state'
wait_for_attribute 'css=#project_status .hidden span', 'class', 'active_state'
assert_text 'badge_count', '2'
open '/project/Build_a_working_time_machine'
open '/projects/Build_a_working_time_machine'
assert_text 'badge_count', '2'
assert_checked 'project_state_hidden', 'ignored'

View file

@ -1,5 +1,5 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
include_partial 'project_detail/add_deferred_todo'
assert_not_visible "tickler-empty-nd"

View file

@ -1,6 +1,6 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
click "edit_icon_todo_5"
wait_for_element_present "show_from_todo_5"
type "show_from_todo_5", "1/1/2030"

View file

@ -1,6 +1,6 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
include_partial 'project_detail/add_deferred_todo'
click "xpath=//div[@id='tickler'] //div[@id='todo_15'] //input[@class='item-checkbox']"
wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_15']"

View file

@ -1,5 +1,5 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/project/Build_a_working_time_machine"
open "/projects/Build_a_working_time_machine"
click "xpath=//div[@id='tickler'] //div[@id='todo_15'] //input[@class='item-checkbox']"
wait_for_visible "tickler-empty-nd"

View file

@ -0,0 +1,6 @@
setup :fixtures => :all
include_partial 'login/login', :username => 'admin', :password => 'abracadabra'
open "/projects"
click "css=#project_2 .buttons img.delete_item"
assert_confirmation "Are you sure that you want to delete the project 'Make more money than Billy Gates'?"
wait_for_element_not_present "project_2"

View file

@ -139,4 +139,8 @@ class ProjectTest < Test::Unit::TestCase
assert_equal project.id, found_project.id
end
def test_to_param_returns_url_friendly_name
assert_equal 'Build_a_working_time_machine', @timemachine.to_param
end
end