diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 6477d384..a6635bdb 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -62,6 +62,26 @@ class ApplicationController < ActionController::Base end end + # Returns a count of next actions in the given context or project + # The result is count and a string descriptor, correctly pluralised if there are no + # actions or multiple actions + # + def count_undone_todos(todos_parent, string="actions") + if (todos_parent.is_a?(Project) && todos_parent.hidden?) + count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" + else + count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" + end + count = 0 if count == nil + #count = todos_parent.todos.select{|t| !t.done }.size + if count == 1 + word = string.singularize + else + word = string.pluralize + end + return count.to_s + " " + word + end + protected def admin_login_required @@ -101,7 +121,7 @@ class ApplicationController < ActionController::Base parents.each do |parent| eval("@#{parent}_project_hidden_todo_counts = Todo.count(:conditions => ['user_id = ? and state = ?', @user.id, 'project_hidden'], :group => :#{parent}_id)") end - end + end # Set the contents of the flash message from a controller # Usage: notify :warning, "This is the message" diff --git a/tracks/app/controllers/feed_controller.rb b/tracks/app/controllers/feed_controller.rb index 0c81c432..33f6f9f4 100644 --- a/tracks/app/controllers/feed_controller.rb +++ b/tracks/app/controllers/feed_controller.rb @@ -45,14 +45,6 @@ class FeedController < ApplicationController end headers["Content-Type"] = "text/calendar" end - - def list_projects_only - init_not_done_counts('project') - init_project_hidden_todo_counts - @projects = @user.projects - @description = "Lists all the projects for #{@user.login}." - render :action => 'projects_' + params['feedtype'] - end def list_contexts_only init_not_done_counts('context') diff --git a/tracks/app/controllers/projects_controller.rb b/tracks/app/controllers/projects_controller.rb index b3ed580a..e5395018 100644 --- a/tracks/app/controllers/projects_controller.rb +++ b/tracks/app/controllers/projects_controller.rb @@ -1,14 +1,31 @@ class ProjectsController < ApplicationController - helper :todos, :notes + helper :application, :todos, :notes before_filter :init, :except => [:create, :destroy, :order] + skip_before_filter :login_required, :only => [:index] + prepend_before_filter :login_or_feed_token_required, :only => [:index] + session :off, :only => :index, :if => Proc.new { |req| ['rss','atom','txt'].include?(req.parameters[:format]) } def index - init_project_hidden_todo_counts - @page_title = "TRACKS::List Projects" - respond_to do |wants| - wants.html - wants.xml { render :xml => @projects.to_xml( :except => :user_id ) } + respond_to do |format| + format.html do + init_project_hidden_todo_counts + @page_title = "TRACKS::List Projects" + render + end + format.xml { render :xml => @projects.to_xml( :except => :user_id ) } + format.rss do + render_rss_feed_for @projects, :feed => Project.feed_options(@user), + :item => { :description => lambda { |p| p.summary(count_undone_todos(p)) } } + end + format.atom do + render_atom_feed_for @projects, :feed => Project.feed_options(@user), + :item => { :description => lambda { |p| p.summary(count_undone_todos(p)) }, + :author => lambda { |p| nil } } + end + format.text do + render :action => 'index_text', :layout => false, :content_type => Mime::TEXT + end end end diff --git a/tracks/app/controllers/users_controller.rb b/tracks/app/controllers/users_controller.rb index 0a099172..e02d265b 100644 --- a/tracks/app/controllers/users_controller.rb +++ b/tracks/app/controllers/users_controller.rb @@ -12,6 +12,7 @@ class UsersController < ApplicationController # GET /users # GET /users.xml def index + @users = User.find(:all, :order => 'login') respond_to do |format| format.html do @page_title = "TRACKS::Manage Users" @@ -21,10 +22,7 @@ class UsersController < ApplicationController # we store the URL so that we get returned here when signup is successful store_location end - format.xml do - @users = User.find(:all) - render :xml => @users.to_xml(:except => [ :password ]) - end + format.xml { render :xml => @users.to_xml(:except => [ :password ]) } end end diff --git a/tracks/app/helpers/application_helper.rb b/tracks/app/helpers/application_helper.rb index db40ffed..e69b8c27 100644 --- a/tracks/app/helpers/application_helper.rb +++ b/tracks/app/helpers/application_helper.rb @@ -113,19 +113,7 @@ module ApplicationHelper # actions or multiple actions # def count_undone_todos(todos_parent, string="actions") - if (todos_parent.is_a?(Project) && todos_parent.hidden?) - count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" - else - count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" - end - count = 0 if count == nil - #count = todos_parent.todos.select{|t| !t.done }.size - if count == 1 - word = string.singularize - else - word = string.pluralize - end - return count.to_s + " " + word + @controller.count_undone_todos(todos_parent, string) end def link_to_context(context, descriptor = sanitize(context.name)) diff --git a/tracks/app/helpers/feed_helper.rb b/tracks/app/helpers/feed_helper.rb index a2683a0a..c38bc212 100644 --- a/tracks/app/helpers/feed_helper.rb +++ b/tracks/app/helpers/feed_helper.rb @@ -31,20 +31,6 @@ module FeedHelper return result_string end - def build_projects_text_page(projects) - result_string = "" - projects.each do |p| - result_string << "\n" + p.name.upcase + "\n" - - result_string << p.description + "\n" if p.description_present? - result_string << "#{count_undone_todos(p)}. Project is #{p.state}.\n" - result_string << "#{p.linkurl}\n" if p.linkurl_present? - result_string << "\n" - end - - return result_string - end - def build_contexts_text_page(contexts) result_string = "" contexts.each do |c| diff --git a/tracks/app/helpers/feedlist_helper.rb b/tracks/app/helpers/feedlist_helper.rb index 7ef3735b..ebbfd6b4 100644 --- a/tracks/app/helpers/feedlist_helper.rb +++ b/tracks/app/helpers/feedlist_helper.rb @@ -6,12 +6,26 @@ module FeedlistHelper linkoptions.merge!(options) link_to(image_tag, linkoptions, :title => "RSS feed") end + + def rss_formatted_link(options = {}) + image_tag = image_tag("feed-icon.png", :size => "16X16", :border => 0, :class => "rss-icon") + linkoptions = { :token => @user.word, :format => 'rss' } + linkoptions.merge!(options) + link_to(image_tag, linkoptions, :title => "RSS feed") + end def text_feed_link(options = {}) linkoptions = {:controller => 'feed', :action => 'text', :login => "#{@user.login}", :token => "#{@user.word}"} linkoptions.merge!(options) link_to('TXT', linkoptions, :title => "Plain text feed" ) end + + def text_formatted_link(options = {}) + linkoptions = { :token => @user.word, :format => 'txt' } + linkoptions.merge!(options) + link_to('TXT', linkoptions, :title => "Plain text feed" ) + end + def ical_feed_link(options = {}) linkoptions = {:controller => 'feed', :action => 'ical', :login => "#{@user.login}", :token => "#{@user.word}"} diff --git a/tracks/app/models/project.rb b/tracks/app/models/project.rb index 20d9eb3e..2eb7b250 100644 --- a/tracks/app/models/project.rb +++ b/tracks/app/models/project.rb @@ -37,6 +37,13 @@ class Project < ActiveRecord::Base NullProject.new end + def self.feed_options(user) + { + :title => 'Tracks Projects', + :description => "Lists all the projects for #{user.display_name}." + } + end + def to_param url_friendly_name end @@ -48,6 +55,20 @@ class Project < ActiveRecord::Base def linkurl_present? attribute_present?("linkurl") end + + def title + name + end + + def summary(undone_todo_count) + project_description = '' + project_description += sanitize(markdown( description )) if description_present? + project_description += "

#{undone_todo_count}. " + project_description += "Project is #{state}. " + project_description += "#{linkurl}" if linkurl_present? + project_description += "

" + project_description + end def hide_todos todos.each do |t| diff --git a/tracks/app/models/user.rb b/tracks/app/models/user.rb index 29a2f337..d740e155 100644 --- a/tracks/app/models/user.rb +++ b/tracks/app/models/user.rb @@ -8,12 +8,12 @@ class User < ActiveRecord::Base :order => 'position ASC', :dependent => :delete_all has_many :todos, - :order => 'completed_at DESC, created_at DESC', + :order => 'completed_at DESC, todos.created_at DESC', :dependent => :delete_all has_many :deferred_todos, :class_name => 'Todo', :conditions => [ 'state = ?', 'deferred' ], - :order => 'show_from ASC, created_at DESC' do + :order => 'show_from ASC, todos.created_at DESC' do def find_and_activate_ready find(:all, :conditions => ['show_from <= ?', Time.now.utc.to_date ]).collect { |t| t.activate_and_save! } end diff --git a/tracks/app/views/feed/projects_rss.rxml b/tracks/app/views/feed/projects_rss.rxml deleted file mode 100644 index 98b24906..00000000 --- a/tracks/app/views/feed/projects_rss.rxml +++ /dev/null @@ -1,21 +0,0 @@ -@headers["Content-Type"] = "text/xml; charset=utf-8" -xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do - xml.channel do - xml.title(@title) - xml.link(projects_url) - xml.description(@description) - @projects.each do |p| - xml.item do - xml.title(p.name) - xml.link(project_url(p)) - project_description = '' - project_description += sanitize(markdown( p.description )) if p.description_present? - project_description += "

#{count_undone_todos(p)}. " - project_description += "Project is #{p.state}. " - project_description += "#{p.linkurl}" if p.linkurl_present? - project_description += "

" - xml.description(project_description) - end - end - end -end \ No newline at end of file diff --git a/tracks/app/views/feed/projects_text.rhtml b/tracks/app/views/feed/projects_text.rhtml deleted file mode 100644 index cd17083e..00000000 --- a/tracks/app/views/feed/projects_text.rhtml +++ /dev/null @@ -1,2 +0,0 @@ -<% @headers["Content-Type"] = "text/plain; charset=utf-8" -%> -<%= build_projects_text_page( @projects ) -%> diff --git a/tracks/app/views/feedlist/index.rhtml b/tracks/app/views/feedlist/index.rhtml index 4a1b6f0e..37563476 100644 --- a/tracks/app/views/feedlist/index.rhtml +++ b/tracks/app/views/feedlist/index.rhtml @@ -45,8 +45,8 @@ All Contexts
  • - <%= rss_feed_link({ :action => 'list_projects_only', :feedtype => 'rss' }) %> - <%= text_feed_link({ :action => 'list_projects_only', :feedtype => 'text' }) %> + <%= rss_formatted_link({:controller => 'projects', :action => 'index'}) %> + <%= text_formatted_link({:controller => 'projects', :action => 'index'}) %> All Projects
  • Feeds for uncompleted actions in a specific context:

    diff --git a/tracks/app/views/projects/index_text.rhtml b/tracks/app/views/projects/index_text.rhtml new file mode 100644 index 00000000..e6caee2e --- /dev/null +++ b/tracks/app/views/projects/index_text.rhtml @@ -0,0 +1,7 @@ +<% @projects.each do |p| -%> + +<%= p.name.upcase %> +<%= p.description + "\n" if p.description_present? -%> +<%= count_undone_todos(p)%>. Project is <%= p.state %>. +<%= p.linkurl + "\n" if p.linkurl_present? -%> +<% end -%> \ No newline at end of file diff --git a/tracks/app/views/todos/_item.rhtml b/tracks/app/views/todos/_item.rhtml index c8c9dac1..72eaa1b4 100644 --- a/tracks/app/views/todos/_item.rhtml +++ b/tracks/app/views/todos/_item.rhtml @@ -35,7 +35,7 @@ <% end -%> <% if item.completed? -%> - (<%= item.context.name %><%= ", " + item.project.name if item.project_id %>) + (<%= item.context.name %><%= ", " + item.project.name unless item.project.nil? %>) <% else -%> <% if (parent_container_type == "project" || parent_container_type == "tickler") -%> <%= item_link_to_context( item ) %> diff --git a/tracks/config/routes.rb b/tracks/config/routes.rb index 9a4b3ae1..12908d19 100644 --- a/tracks/config/routes.rb +++ b/tracks/config/routes.rb @@ -36,7 +36,6 @@ ActionController::Routing::Routes.draw do |map| # Projects Routes map.resources :projects, :collection => {:order => :post} map.connect 'project/:project/feed/:action/:login/:token', :controller => 'feed' - map.connect 'projects/feed/:feedtype/:login/:token', :controller => 'feed', :action => 'list_projects_only' # Notes Routes map.resources :notes diff --git a/tracks/db/migrate/026_add_project_timestamps.rb b/tracks/db/migrate/026_add_project_timestamps.rb new file mode 100644 index 00000000..ebbfb0a9 --- /dev/null +++ b/tracks/db/migrate/026_add_project_timestamps.rb @@ -0,0 +1,12 @@ +class AddProjectTimestamps < ActiveRecord::Migration + def self.up + add_column :projects, :created_at, :timestamp + add_column :projects, :updated_at, :timestamp + end + + + def self.down + remove_column :projects, :created_at + remove_column :projects, :updated_at + end +end diff --git a/tracks/db/schema.rb b/tracks/db/schema.rb index f3fde4cc..388a2b41 100644 --- a/tracks/db/schema.rb +++ b/tracks/db/schema.rb @@ -2,7 +2,7 @@ # migrations feature of ActiveRecord to incrementally modify your database, and # then regenerate this schema definition. -ActiveRecord::Schema.define(:version => 25) do +ActiveRecord::Schema.define(:version => 26) do create_table "contexts", :force => true do |t| t.column "name", :string, :default => "", :null => false @@ -60,11 +60,13 @@ ActiveRecord::Schema.define(:version => 25) 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 => 0, :null => false + 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 "state", :string, :limit => 20, :default => "active", :null => false + t.column "created_at", :datetime + t.column "updated_at", :datetime end add_index "projects", ["user_id"], :name => "index_projects_on_user_id" diff --git a/tracks/lib/login_system.rb b/tracks/lib/login_system.rb index cd2edd52..d3a94d49 100644 --- a/tracks/lib/login_system.rb +++ b/tracks/lib/login_system.rb @@ -30,6 +30,16 @@ module LoginSystem def protect?(action) true end + + def login_or_feed_token_required + if ['rss', 'atom', 'txt'].include?(params[:format]) + if user = User.find_by_word(params[:token]) + set_current_user(user) + return true + end + end + login_required + end # login_required filter. add # @@ -53,7 +63,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 + set_current_user(user) return true end @@ -75,7 +85,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 + set_current_user(user) return true end @@ -89,6 +99,12 @@ module LoginSystem @prefs = @user.prefs unless @user.nil? @user end + + def set_current_user(user) + @user = user + @prefs = @user.prefs unless @user.nil? + @user + end # overwrite if you want to have special behavior in case the user is not authorized # to access the current operation. @@ -96,10 +112,13 @@ module LoginSystem # example use : # a popup window might just close itself for instance def access_denied - respond_to do |wants| - wants.html { redirect_to :controller=>"login", :action =>"login" } - wants.js { render :partial => 'login/redirect_to_login' } - wants.xml { basic_auth_denied } + respond_to do |format| + format.html { redirect_to :controller=>"login", :action =>"login" } + format.js { render :partial => 'login/redirect_to_login' } + format.xml { basic_auth_denied } + format.rss { basic_auth_denied } + format.atom { basic_auth_denied } + format.text { basic_auth_denied } end end diff --git a/tracks/test/fixtures/projects.yml b/tracks/test/fixtures/projects.yml index 5276c6df..36404845 100644 --- a/tracks/test/fixtures/projects.yml +++ b/tracks/test/fixtures/projects.yml @@ -1,4 +1,11 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +<% + +def today + Time.now.utc.to_s(:db) +end + +%> timemachine: id: 1 @@ -7,6 +14,8 @@ timemachine: position: 1 state: 'active' user_id: 1 + created_at: <%= today %> + updated_at: <%= today %> moremoney: id: 2 @@ -15,6 +24,8 @@ moremoney: position: 2 state: 'active' user_id: 1 + created_at: <%= today %> + updated_at: <%= today %> gardenclean: id: 3 @@ -23,3 +34,5 @@ gardenclean: position: 3 state: 'active' user_id: 1 + created_at: <%= today %> + updated_at: <%= today %> diff --git a/tracks/test/functional/feedlist_controller_test.rb b/tracks/test/functional/feedlist_controller_test.rb new file mode 100644 index 00000000..bcf346d8 --- /dev/null +++ b/tracks/test/functional/feedlist_controller_test.rb @@ -0,0 +1,30 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'feedlist_controller' + +# Re-raise errors caught by the controller. +class FeedlistController; def rescue_action(e) raise e end; end + +class FeedlistControllerTest < Test::Unit::TestCase + fixtures :users, :preferences, :projects, :contexts, :todos, :notes + + def setup + assert_equal "test", ENV['RAILS_ENV'] + assert_equal "change-me", Tracks::Config.salt + @controller = FeedlistController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_get_index_when_not_logged_in + get :index + assert_redirected_to :controller => 'login', :action => 'login' + end + + def test_get_index_by_logged_in_user + @request.session['user_id'] = users(:other_user).id + get :index + assert_response :success + assert_equal "TRACKS::Feeds", assigns['page_title'] + end + +end diff --git a/tracks/test/functional/projects_controller_test.rb b/tracks/test/functional/projects_controller_test.rb index b07b98b0..4fbec5ae 100644 --- a/tracks/test/functional/projects_controller_test.rb +++ b/tracks/test/functional/projects_controller_test.rb @@ -6,7 +6,7 @@ require 'projects_controller' class ProjectsController; def rescue_action(e) raise e end; end class ProjectsControllerTest < TodoContainerControllerTestBase - fixtures :users, :todos, :preferences, :projects + fixtures :users, :todos, :preferences, :projects, :contexts def setup perform_setup(Project, ProjectsController) @@ -78,5 +78,106 @@ class ProjectsControllerTest < TodoContainerControllerTestBase assert p.reload().active? end + def test_rss_feed_content + @request.session['user_id'] = users(:admin_user).id + get :index, { :format => "rss" } + assert_equal 'application/rss+xml; charset=utf-8', @response.headers["Content-Type"] + #puts @response.body + + assert_xml_select 'rss[version="2.0"]' do + assert_xml_select 'channel' do + assert_xml_select '>title', 'Tracks Projects' + assert_xml_select '>description', "Lists all the projects for #{users(:admin_user).display_name}." + assert_xml_select 'language', 'en-us' + assert_xml_select 'ttl', '40' + end + assert_xml_select 'item', 3 do + assert_xml_select 'title', /.+/ + assert_xml_select 'description', /<p>\d+ actions. Project is (active|hidden|completed). <\/p>/ + %w(guid link).each do |node| + assert_xml_select node, /http:\/\/test.host\/projects\/.+/ + end + assert_xml_select 'pubDate', projects(:timemachine).created_at.to_s(:rfc822) + end + end + end + + def test_rss_feed_not_accessible_to_anonymous_user_without_token + @request.session['user_id'] = nil + get :index, { :format => "rss" } + assert_response 401 + end + + def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token + @request.session['user_id'] = nil + get :index, { :format => "rss", :token => 'foo' } + assert_response 401 + end + + def test_rss_feed_accessible_to_anonymous_user_with_valid_token + @request.session['user_id'] = nil + get :index, { :format => "rss", :token => users(:admin_user).word } + assert_response :ok + end + + def test_atom_feed_content + @request.session['user_id'] = users(:admin_user).id + get :index, { :format => "atom" } + assert_equal 'application/atom+xml; charset=utf-8', @response.headers["Content-Type"] + #puts @response.body + + assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do + assert_xml_select '>title', 'Tracks Projects' + assert_xml_select '>subtitle', "Lists all the projects for #{users(:admin_user).display_name}." + assert_xml_select 'entry', 3 do + assert_xml_select 'title', /.+/ + assert_xml_select 'content[type="html"]', /<p>\d+ actions. Project is (active|hidden|completed). <\/p>/ + assert_xml_select 'published', projects(:timemachine).created_at.to_s(:rfc822) + end + end + end + + def test_atom_feed_not_accessible_to_anonymous_user_without_token + @request.session['user_id'] = nil + get :index, { :format => "atom" } + assert_response 401 + end + + def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token + @request.session['user_id'] = nil + get :index, { :format => "atom", :token => 'foo' } + assert_response 401 + end + + def test_atom_feed_accessible_to_anonymous_user_with_valid_token + @request.session['user_id'] = nil + get :index, { :format => "atom", :token => users(:admin_user).word } + assert_response :ok + end + + def test_text_feed_content + @request.session['user_id'] = users(:admin_user).id + get :index, { :format => "txt" } + assert_equal 'text/plain; charset=utf-8', @response.headers["Content-Type"] + #puts @response.body + end + + def test_text_feed_not_accessible_to_anonymous_user_without_token + @request.session['user_id'] = nil + get :index, { :format => "txt" } + assert_response 401 + end + + def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token + @request.session['user_id'] = nil + get :index, { :format => "txt", :token => 'foo' } + assert_response 401 + end + + def test_text_feed_accessible_to_anonymous_user_with_valid_token + @request.session['user_id'] = nil + get :index, { :format => "txt", :token => users(:admin_user).word } + assert_response :ok + end end diff --git a/tracks/test/integration/feed_smoke_test.rb b/tracks/test/integration/feed_smoke_test.rb index 27b6755a..438cd7f7 100644 --- a/tracks/test/integration/feed_smoke_test.rb +++ b/tracks/test/integration/feed_smoke_test.rb @@ -80,17 +80,17 @@ class FeedSmokeTest < ActionController::IntegrationTest end def test_all_projects_rss - assert_success "/projects/feed/rss/admin/#{ users(:admin_user).word }" + assert_success "/projects.rss?token=#{ users(:admin_user).word }" end def test_all_projects_txt - assert_success "/projects/feed/text/admin/#{ users(:admin_user).word }" + assert_success "/projects.txt?token=#{ users(:admin_user).word }" end def test_all_projects_txt_with_hidden_project p = projects(:timemachine) p.hide! - assert_success "/projects/feed/text/admin/#{ users(:admin_user).word }" + assert_success "/projects.txt?token=#{ users(:admin_user).word }" end private diff --git a/tracks/test/integration/project_xml_api_test.rb b/tracks/test/integration/project_xml_api_test.rb index 5cb17c6f..7f37daf6 100644 --- a/tracks/test/integration/project_xml_api_test.rb +++ b/tracks/test/integration/project_xml_api_test.rb @@ -4,7 +4,7 @@ require 'projects_controller' # Re-raise errors caught by the controller. class ProjectsController; def rescue_action(e) raise e end; end -class ProjectsControllerXmlApiTest < ActionController::IntegrationTest +class ProjectXmlApiTest < ActionController::IntegrationTest fixtures :users, :projects @@project_name = "My New Project" @@ -51,7 +51,15 @@ class ProjectsControllerXmlApiTest < ActionController::IntegrationTest def test_creates_new_project initial_count = Project.count authenticated_post_xml_to_project_create - assert_response_and_body_matches 200, %r|^<\?xml version="1\.0" encoding="UTF-8"\?>\n\n \n [0-9]+\n #{@@project_name}\n 1\n active\n$| + assert_response :success + assert_xml_select 'project' do + assert_xml_select "description" + assert_xml_select 'id[type="integer"]', /[0-9]+/ + assert_xml_select 'name', @@project_name + assert_xml_select 'position[type="integer"]', 1 + assert_xml_select 'state', 'active' + end + #assert_response_and_body_matches 200, %r|^<\?xml version="1\.0" encoding="UTF-8"\?>\n\n \n [0-9]+\n #{@@project_name}\n 1\n active\n$| assert_equal initial_count + 1, Project.count project1 = Project.find_by_name(@@project_name) assert_not_nil project1, "expected project '#{@@project_name}' to be created" diff --git a/tracks/test/integration/users_xml_api_test.rb b/tracks/test/integration/users_xml_api_test.rb index 634c3739..31bd5c63 100644 --- a/tracks/test/integration/users_xml_api_test.rb +++ b/tracks/test/integration/users_xml_api_test.rb @@ -76,7 +76,6 @@ class UsersXmlApiTest < ActionController::IntegrationTest def test_get_users_as_xml get '/users.xml', {}, basic_auth_headers() - #puts @response.body assert_response :success assert_tag :tag => "users", :children => { :count => 3, :only => { :tag => "user" } } @@ -85,7 +84,6 @@ class UsersXmlApiTest < ActionController::IntegrationTest def test_get_user_as_xml get "/users/#{users(:other_user).login}.xml", {}, basic_auth_headers() - puts @response.body assert_response :success assert_tag :tag => "user" assert_no_tag :tag => "password" diff --git a/tracks/test/test_helper.rb b/tracks/test/test_helper.rb index f2d21701..9d203af9 100644 --- a/tracks/test/test_helper.rb +++ b/tracks/test/test_helper.rb @@ -30,6 +30,16 @@ class Test::Unit::TestCase end return string end + + def xml_document + @xml_document ||= HTML::Document.new(@response.body, false, true) + end + + def assert_xml_select(*args) + @html_document = xml_document + assert_select(*args) + end + end class ActionController::IntegrationTest diff --git a/tracks/test/unit/project_test.rb b/tracks/test/unit/project_test.rb index 3963d85a..7b18729f 100644 --- a/tracks/test/unit/project_test.rb +++ b/tracks/test/unit/project_test.rb @@ -143,4 +143,8 @@ class ProjectTest < Test::Unit::TestCase assert_equal 'Build_a_working_time_machine', @timemachine.to_param end + def test_title_reader_returns_name + assert_equal @timemachine.name, @timemachine.title + end + end diff --git a/tracks/vendor/plugins/resource_feeder/README b/tracks/vendor/plugins/resource_feeder/README new file mode 100644 index 00000000..5502be25 --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/README @@ -0,0 +1,7 @@ +ResourceFeeder +============== + +Simple feeds for resources + +NOTE: This plugin depends on the latest version of simply_helpful, available here: +http://dev.rubyonrails.org/svn/rails/plugins/simply_helpful/ diff --git a/tracks/vendor/plugins/resource_feeder/Rakefile b/tracks/vendor/plugins/resource_feeder/Rakefile new file mode 100644 index 00000000..51fce7b3 --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the resource_feed plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the resource_feed plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'ResourceFeed' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/tracks/vendor/plugins/resource_feeder/init.rb b/tracks/vendor/plugins/resource_feeder/init.rb new file mode 100644 index 00000000..7b55d76f --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/init.rb @@ -0,0 +1,2 @@ +require 'resource_feeder' +ActionController::Base.send(:include, ResourceFeeder::Rss, ResourceFeeder::Atom) \ No newline at end of file diff --git a/tracks/vendor/plugins/resource_feeder/lib/resource_feeder.rb b/tracks/vendor/plugins/resource_feeder/lib/resource_feeder.rb new file mode 100644 index 00000000..b5003419 --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/lib/resource_feeder.rb @@ -0,0 +1,2 @@ +require 'resource_feeder/rss' +require 'resource_feeder/atom' diff --git a/tracks/vendor/plugins/resource_feeder/lib/resource_feeder/atom.rb b/tracks/vendor/plugins/resource_feeder/lib/resource_feeder/atom.rb new file mode 100644 index 00000000..d3b5a63c --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/lib/resource_feeder/atom.rb @@ -0,0 +1,78 @@ +module ResourceFeeder + module Atom + extend self + + def render_atom_feed_for(resources, options = {}) + render :text => atom_feed_for(resources, options), :content_type => Mime::ATOM + end + + def atom_feed_for(resources, options = {}) + xml = Builder::XmlMarkup.new(:indent => 2) + + options[:feed] ||= {} + options[:item] ||= {} + options[:url_writer] ||= self + + if options[:class] || resources.first + klass = options[:class] || resources.first.class + new_record = klass.new + else + options[:feed] = { :title => "Empty", :link => "http://example.com" } + end + + options[:feed][:title] ||= klass.name.pluralize + options[:feed][:id] ||= "tag:#{request.host_with_port}:#{klass.name.pluralize}" + options[:feed][:link] ||= SimplyHelpful::RecordIdentifier.polymorphic_url(new_record, options[:url_writer]) + + options[:item][:title] ||= [ :title, :subject, :headline, :name ] + options[:item][:description] ||= [ :description, :body, :content ] + options[:item][:pub_date] ||= [ :updated_at, :updated_on, :created_at, :created_on ] + options[:item][:author] ||= [ :author, :creator ] + + resource_link = lambda { |r| SimplyHelpful::RecordIdentifier.polymorphic_url(r, options[:url_writer]) } + + xml.instruct! + xml.feed "xml:lang" => "en-US", "xmlns" => 'http://www.w3.org/2005/Atom' do + xml.title(options[:feed][:title]) + xml.id(options[:feed][:id]) + xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:feed][:link]) + xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:feed][:self]) if options[:feed][:self] + xml.subtitle(options[:feed][:description]) if options[:feed][:description] + + for resource in resources + published_at = call_or_read(options[:item][:pub_date], resource) + + xml.entry do + xml.title(call_or_read(options[:item][:title], resource)) + xml.content(call_or_read(options[:item][:description], resource), :type => 'html') + xml.id("tag:#{request.host_with_port},#{published_at.xmlschema}:#{call_or_read(options[:item][:guid] || options[:item][:link] || resource_link, resource)}") + xml.published(published_at.xmlschema) + xml.updated((resource.respond_to?(:updated_at) ? call_or_read(options[:item][:pub_date] || :updated_at, resource) : published_at).xmlschema) + xml.link(:rel => 'alternate', :type => 'text/html', :href => call_or_read(options[:item][:link] || options[:item][:guid] || resource_link, resource)) + + if author = call_or_read(options[:item][:author], resource) + xml.author do + xml.name() + end + end + end + end + end + end + + private + def call_or_read(procedure_or_attributes, resource) + case procedure_or_attributes + when Array + attributes = procedure_or_attributes + resource.send(attributes.select { |a| resource.respond_to?(a) }.first) + when Symbol + attribute = procedure_or_attributes + resource.send(attribute) + when Proc + procedure = procedure_or_attributes + procedure.call(resource) + end + end + end +end \ No newline at end of file diff --git a/tracks/vendor/plugins/resource_feeder/lib/resource_feeder/rss.rb b/tracks/vendor/plugins/resource_feeder/lib/resource_feeder/rss.rb new file mode 100644 index 00000000..b66ec4a8 --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/lib/resource_feeder/rss.rb @@ -0,0 +1,79 @@ +module ResourceFeeder + module Rss + extend self + + def render_rss_feed_for(resources, options = {}) + render :text => rss_feed_for(resources, options), :content_type => Mime::RSS + end + + def rss_feed_for(resources, options = {}) + xml = Builder::XmlMarkup.new(:indent => 2) + + options[:feed] ||= {} + options[:item] ||= {} + options[:url_writer] ||= self + + if options[:class] || resources.first + klass = options[:class] || resources.first.class + new_record = klass.new + else + options[:feed] = { :title => "Empty", :link => "http://example.com" } + end + use_content_encoded = options[:item].has_key?(:content_encoded) + + options[:feed][:title] ||= klass.name.pluralize + options[:feed][:link] ||= SimplyHelpful::RecordIdentifier.polymorphic_url(new_record, options[:url_writer]) + options[:feed][:language] ||= "en-us" + options[:feed][:ttl] ||= "40" + + options[:item][:title] ||= [ :title, :subject, :headline, :name ] + options[:item][:description] ||= [ :description, :body, :content ] + options[:item][:pub_date] ||= [ :updated_at, :updated_on, :created_at, :created_on ] + + resource_link = lambda { |r| SimplyHelpful::RecordIdentifier.polymorphic_url(r, options[:url_writer]) } + + rss_root_attributes = { :version => 2.0 } + rss_root_attributes.merge!("xmlns:content" => "http://purl.org/rss/1.0/modules/content/") if use_content_encoded + + xml.instruct! + + xml.rss(rss_root_attributes) do + xml.channel do + xml.title(options[:feed][:title]) + xml.link(options[:feed][:link]) + xml.description(options[:feed][:description]) if options[:feed][:description] + xml.language(options[:feed][:language]) + xml.ttl(options[:feed][:ttl]) + + for resource in resources + xml.item do + xml.title(call_or_read(options[:item][:title], resource)) + xml.description(call_or_read(options[:item][:description], resource)) + if use_content_encoded then + xml.content(:encoded) { xml.cdata!(call_or_read(options[:item][:content_encoded], resource)) } + end + xml.pubDate(call_or_read(options[:item][:pub_date], resource).to_s(:rfc822)) + xml.guid(call_or_read(options[:item][:guid] || options[:item][:link] || resource_link, resource)) + xml.link(call_or_read(options[:item][:link] || options[:item][:guid] || resource_link, resource)) + end + end + end + end + end + + private + def call_or_read(procedure_or_attributes, resource) + case procedure_or_attributes + when Array + attributes = procedure_or_attributes + resource.send(attributes.select { |a| resource.respond_to?(a) }.first) + when Symbol + attribute = procedure_or_attributes + resource.send(attribute) + when Proc + procedure = procedure_or_attributes + procedure.call(resource) + end + end + end +end diff --git a/tracks/vendor/plugins/resource_feeder/test/atom_feed_test.rb b/tracks/vendor/plugins/resource_feeder/test/atom_feed_test.rb new file mode 100644 index 00000000..3112da47 --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/test/atom_feed_test.rb @@ -0,0 +1,85 @@ +require File.dirname(__FILE__) + '/test_helper' +class AtomFeedTest < Test::Unit::TestCase + attr_reader :request + + def setup + @request = OpenStruct.new + @request.host_with_port = 'example.com' + @records = Array.new(5).fill(Post.new) + @records.each &:save + end + + def test_default_atom_feed + atom_feed_for @records + + assert_select 'feed' do + assert_select '>title', 'Posts' + assert_select '>id', "tag:#{request.host_with_port}:Posts" + assert_select '>link' do + assert_select "[rel='alternate']" + assert_select "[type='text/html']" + assert_select "[href='http://example.com/posts']" + end + assert_select 'entry', 5 do + assert_select 'title', :text => 'feed title (title)' + assert_select "content[type='html']", '<p>feed description (description)</p>' + assert_select 'id', "tag:#{request.host_with_port},#{@records.first.created_at.xmlschema}:#{'http://example.com/posts/1'}" + assert_select 'published', @records.first.created_at.xmlschema + assert_select 'updated', @records.first.created_at.xmlschema + assert_select 'link' do + assert_select "[rel='alternate']" + assert_select "[type='text/html']" + assert_select "[href='http://example.com/posts/1']" + end + end + end + end + + def test_should_allow_custom_feed_options + atom_feed_for @records, :feed => { :title => 'Custom Posts', :link => '/posts', :description => 'stuff', :self => '/posts.atom' } + + assert_select 'feed>title', 'Custom Posts' + assert_select "feed>link[href='/posts']" + assert_select 'feed>subtitle', 'stuff' + assert_select 'feed>link' do + assert_select "[rel='self']" + assert_select "[type='application/atom+xml']" + assert_select "[href='/posts.atom']" + end + end + + def test_should_allow_custom_item_attributes + atom_feed_for @records, :item => { :title => :name, :description => :body, :pub_date => :create_date, :link => :id } + + assert_select 'entry', 5 do + assert_select 'title', :text => 'feed title (name)' + assert_select "content[type='html']", '<p>feed description (body)</p>' + assert_select 'published', (@records.first.created_at - 5.minutes).xmlschema + assert_select 'updated', (@records.first.created_at - 5.minutes).xmlschema + assert_select 'id', "tag:#{request.host_with_port},#{(@records.first.created_at - 5.minutes).xmlschema}:1" + assert_select 'link' do + assert_select "[rel='alternate']" + assert_select "[type='text/html']" + assert_select "[href='1']" + end + end + end + + def test_should_allow_custom_item_attribute_blocks + atom_feed_for @records, :item => { :title => lambda { |r| r.name }, :description => lambda { |r| r.body }, :pub_date => lambda { |r| r.create_date }, + :link => lambda { |r| "/#{r.created_at.to_i}" }, :guid => lambda { |r| r.created_at.to_i } } + + assert_select 'entry', 5 do + assert_select 'title', :text => 'feed title (name)' + assert_select "content[type='html']", '<p>feed description (body)</p>' + assert_select 'published', (@records.first.created_at - 5.minutes).xmlschema + assert_select 'updated', (@records.first.created_at - 5.minutes).xmlschema + assert_select 'id', /:\d+$/ + assert_select 'link' do + assert_select "[rel='alternate']" + assert_select "[type='text/html']" + assert_select "[href=?]", /^\/\d+$/ + end + end + end +end diff --git a/tracks/vendor/plugins/resource_feeder/test/rss_feed_test.rb b/tracks/vendor/plugins/resource_feeder/test/rss_feed_test.rb new file mode 100644 index 00000000..90525baf --- /dev/null +++ b/tracks/vendor/plugins/resource_feeder/test/rss_feed_test.rb @@ -0,0 +1,86 @@ +require File.dirname(__FILE__) + '/test_helper' +class RssFeedTest < Test::Unit::TestCase + def setup + @records = Array.new(5).fill(Post.new) + @records.each &:save + end + + def test_default_rss_feed + rss_feed_for @records + + assert_select 'rss[version="2.0"]' do + assert_select 'channel' do + assert_select '>title', 'Posts' + assert_select '>link', 'http://example.com/posts' + assert_select 'language', 'en-us' + assert_select 'ttl', '40' + end + assert_select 'item', 5 do + assert_select 'title', :text => 'feed title (title)' + assert_select 'description', '<p>feed description (description)</p>' + %w(guid link).each do |node| + assert_select node, 'http://example.com/posts/1' + end + assert_select 'pubDate', @records.first.created_at.to_s(:rfc822) + end + end + end + + def test_should_allow_custom_feed_options + rss_feed_for @records, :feed => { :title => 'Custom Posts', :link => '/posts', :description => 'stuff', :language => 'en-gb', :ttl => '80' } + + assert_select 'channel>title', 'Custom Posts' + assert_select 'channel>link', '/posts' + assert_select 'channel>description', 'stuff' + assert_select 'channel>language', 'en-gb' + assert_select 'channel>ttl', '80' + end + + def test_should_allow_custom_item_attributes + rss_feed_for @records, :item => { :title => :name, :description => :body, :pub_date => :create_date, :link => :id } + + assert_select 'item', 5 do + assert_select 'title', :text => 'feed title (name)' + assert_select 'description', '<p>feed description (body)</p>' + assert_select 'pubDate', (@records.first.created_at - 5.minutes).to_s(:rfc822) + assert_select 'link', '1' + assert_select 'guid', '1' + end + end + + def test_should_allow_custom_item_attribute_blocks + rss_feed_for @records, :item => { :title => lambda { |r| r.name }, :description => lambda { |r| r.body }, :pub_date => lambda { |r| r.create_date }, + :link => lambda { |r| "/#{r.created_at.to_i}" }, :guid => lambda { |r| r.created_at.to_i } } + + assert_select 'item', 5 do + assert_select 'title', :text => 'feed title (name)' + assert_select 'description', '<p>feed description (body)</p>' + assert_select 'pubDate', (@records.first.created_at - 5.minutes).to_s(:rfc822) + end + end + + # note that assert_select isnt easily able to get elements that have xml namespaces (as it thinks they are + # invalid html psuedo children), so we do some manual testing with the response body + def test_should_allow_content_encoded_for_items + rss_feed_for @records, :item => { :content_encoded => :full_html_body } + + html_content = "Here is some full content, with out any excerpts" + assert_equal 5, @response.body.scan("").size + assert_select 'item', 5 do + assert_select 'description + *', " { :content_encoded => :full_html_body } + assert_equal %[\n], + @response.body.grep(/\n], + @response.body.grep(/feed description (#{attr_name})

    " + end + end + + def full_html_body + "Here is some full content, with out any excerpts" + end + + def create_date + @created_at - 5.minutes + end +end + +class Test::Unit::TestCase + include ResourceFeeder::Rss, ResourceFeeder::Atom + + def render_feed(xml) + @response = OpenStruct.new + @response.headers = {'Content-Type' => 'text/xml'} + @response.body = xml + end + + def rss_feed_for_with_ostruct(resources, options = {}) + render_feed rss_feed_for_without_ostruct(resources, options) + end + + def atom_feed_for_with_ostruct(resources, options = {}) + render_feed atom_feed_for_without_ostruct(resources, options) + end + + alias_method_chain :rss_feed_for, :ostruct + alias_method_chain :atom_feed_for, :ostruct + + def html_document + @html_document ||= HTML::Document.new(@response.body, false, true) + end + + def posts_url + "http://example.com/posts" + end + + def post_url(post) + "http://example.com/posts/#{post.id}" + end +end