From 58d8bc56d1e394db9e83dc80bf450e2288241663 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Fri, 28 Oct 2011 19:33:51 +0200 Subject: [PATCH] fix #827. You can now select todos with tags using OR and AND /todos/tag/tagA,tagB?and=tagC will select all todos with (tagA or tagB) AND tagC --- app/controllers/todos_controller.rb | 113 ++++++++++---------- app/models/todo.rb | 10 +- app/views/todos/mobile_tag.rhtml | 12 +-- app/views/todos/tag.html.erb | 14 +-- config/locales/en.yml | 4 +- config/routes.rb | 1 + test/functional/todos_controller_test.rb | 64 ++++++++++++ test/functional/todos_tagging_test.rb | 62 ----------- test/unit/todo_test.rb | 127 ++++++++++++++++++++--- 9 files changed, 255 insertions(+), 152 deletions(-) delete mode 100644 test/functional/todos_tagging_test.rb diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 6294a82b..fdf12454 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -2,8 +2,8 @@ class TodosController < ApplicationController helper :todos - skip_before_filter :login_required, :only => [:index, :calendar] - prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar] + skip_before_filter :login_required, :only => [:index, :calendar, :tag] + prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar, :tag] append_before_filter :find_and_activate_ready, :only => [:index, :list_deferred] # TODO: replace :except with :only @@ -584,50 +584,11 @@ class TodosController < ApplicationController redirect_to project_todos_path(project, :format => 'm') end - def get_ids_from_tag_expr(tag_expr) - ids = [] - tag_expr.each do |tag_list| - id_list = [] - tag_list.each do |tag| - tag = Tag.find_by_name(tag) - id_list << tag.id if tag - end - ids << id_list - end - return ids - end - - def get_params_for_tag_view - # use sanitize to prevent XSS attacks - - @tag_expr = [] - @tag_expr << sanitize(params[:name]).split(',') - @tag_expr << sanitize(params[:and]).split(',') if params[:and] - - i = 1 - while params['and'+i.to_s] - @tag_expr << sanitize(params['and'+i.to_s]).split(',') - i=i+1 - end - - @single_tag = @tag_expr.size == 1 && @tag_expr[0].size == 1 - @tag_name = @tag_expr[0][0] # if @single_tag - end - - def find_todos_with_tag_ids(tag_ids) - todos = current_user.todos - tag_ids.each do |ids| - todos = todos.with_tags(ids) unless ids.nil? || ids.empty? - end - return todos - end - # /todos/tag/[tag_name] shows all the actions tagged with tag_name def tag - @page_title = t('todos.tagged_page_title', :tag_name => @tag_name) - @source_view = params['_source_view'] || 'tag' - get_params_for_tag_view + @page_title = t('todos.tagged_page_title', :tag_name => @tag_title) + @source_view = params['_source_view'] || 'tag' if mobile? # mobile tags are routed with :name ending on .m. So we need to chomp it @@ -636,11 +597,7 @@ class TodosController < ApplicationController init_data_for_sidebar end - @tag = Tag.find_by_name(@tag_name) - @tag = Tag.new(:name => @tag_name) if @tag.nil? - - @tag_ids = get_ids_from_tag_expr(@tag_expr) - todos_with_tag_ids = find_todos_with_tag_ids(@tag_ids) + todos_with_tag_ids = find_todos_with_tag_expr(@tag_expr) @not_done_todos = todos_with_tag_ids.active.not_hidden.find(:all, :order => 'todos.due IS NULL, todos.due ASC, todos.created_at ASC', :include => Todo::DEFAULT_INCLUDES) @@ -648,17 +605,15 @@ class TodosController < ApplicationController :include => Todo::DEFAULT_INCLUDES, :order => 'todos.completed_at DESC, todos.created_at DESC') @deferred = todos_with_tag_ids.deferred.find(:all, - :order => 'show_from ASC, todos.created_at DESC', :include => Todo::DEFAULT_INCLUDES) + :order => 'todos.show_from ASC, todos.created_at DESC', :include => Todo::DEFAULT_INCLUDES) @pending = todos_with_tag_ids.blocked.find(:all, - :order => 'show_from ASC, todos.created_at DESC', :include => Todo::DEFAULT_INCLUDES) + :order => 'todos.show_from ASC, todos.created_at DESC', :include => Todo::DEFAULT_INCLUDES) # If you've set no_completed to zero, the completed items box isn't shown on # the tag page - max_completed = current_user.prefs.show_number_completed - @done = current_user.todos.with_tag(@tag).completed.find(:all, - :limit => max_completed, - :order => 'todos.completed_at DESC', - :include => Todo::DEFAULT_INCLUDES) + @done = todos_with_tag_ids.completed.find(:all, + :limit => current_user.prefs.show_number_completed, + :order => 'todos.completed_at DESC', :include => Todo::DEFAULT_INCLUDES) @projects = current_user.projects @contexts = current_user.contexts @@ -680,6 +635,9 @@ class TodosController < ApplicationController cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} render :action => "mobile_tag" } + format.text { + render :action => 'index', :layout => false, :content_type => Mime::TEXT + } end end @@ -1033,6 +991,51 @@ class TodosController < ApplicationController :include => [ :project, :context, :tags ]) end + def tag_title(tag_expr) + and_list = tag_expr.inject([]) { |s,tag_list| s << tag_list.join(',') } + return and_list.join(' AND ') + end + + def get_params_for_tag_view + # use sanitize to prevent XSS attacks + + @tag_expr = [] + @tag_expr << sanitize(params[:name]).split(',') + @tag_expr << sanitize(params[:and]).split(',') if params[:and] + + i = 1 + while params['and'+i.to_s] + @tag_expr << sanitize(params['and'+i.to_s]).split(',') + i=i+1 + end + + @single_tag = @tag_expr.size == 1 && @tag_expr[0].size == 1 + @tag_name = @tag_expr[0][0] + @tag_title = @single_tag ? @tag_name : tag_title(@tag_expr) + end + + def get_ids_from_tag_expr(tag_expr) + ids = [] + tag_expr.each do |tag_list| + id_list = [] + tag_list.each do |tag| + tag = Tag.find_by_name(tag) + id_list << tag.id if tag + end + ids << id_list + end + return ids + end + + def find_todos_with_tag_expr(tag_expr) + tag_ids = get_ids_from_tag_expr(tag_expr) + todos = current_user.todos + tag_ids.each do |ids| + todos = todos.with_tags(ids) unless ids.nil? || ids.empty? + end + return todos + end + def determine_down_count source_view do |from| from.todo do diff --git a/app/models/todo.rb b/app/models/todo.rb index 80481546..45253ab3 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -28,18 +28,18 @@ class Todo < ActiveRecord::Base named_scope :deferred_or_blocked, :conditions => ["(todos.completed_at IS NULL AND NOT(todos.show_from IS NULL)) OR (todos.state = ?)", "pending"] named_scope :not_deferred_or_blocked, :conditions => ["todos.completed_at IS NULL AND todos.show_from IS NULL AND NOT(todos.state = ?)", "pending"] named_scope :hidden, - :joins => :context, - :conditions => ["todos.state = ? OR (contexts.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?))", + :joins => "INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id", + :conditions => ["todos.state = ? OR (c_hidden.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?))", 'project_hidden', true, 'active', 'deferred', 'pending'] named_scope :not_hidden, - :joins => [:context], - :conditions => ['NOT(todos.state = ? OR (contexts.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?)))', + :joins => "INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id", + :conditions => ['NOT(todos.state = ? OR (c_hidden.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?)))', 'project_hidden', true, 'active', 'deferred', 'pending'] # other scopes named_scope :are_due, :conditions => ['NOT (todos.due IS NULL)'] named_scope :with_tag, lambda { |tag| {:joins => :taggings, :conditions => ["taggings.tag_id = ? ", tag.id] } } - named_scope :with_tags, lambda { |tag_ids| {:joins => :taggings, :conditions => ["taggings.tag_id IN (?) ", tag_ids] } } + named_scope :with_tags, lambda { |tag_ids| {:conditions => ["EXISTS(SELECT * from taggings t WHERE t.tag_id IN (?) AND t.taggable_id=todos.id AND t.taggable_type='Todo')", tag_ids] } } named_scope :of_user, lambda { |user_id| {:conditions => ["todos.user_id = ? ", user_id] } } named_scope :completed_after, lambda { |date| {:conditions => ["todos.completed_at > ? ", date] } } named_scope :completed_before, lambda { |date| {:conditions => ["todos.completed_at < ? ", date] } } diff --git a/app/views/todos/mobile_tag.rhtml b/app/views/todos/mobile_tag.rhtml index 5dd8efd3..4dde5250 100644 --- a/app/views/todos/mobile_tag.rhtml +++ b/app/views/todos/mobile_tag.rhtml @@ -2,24 +2,24 @@ <% if @not_done_todos.empty? -%>

<%= t('todos.no_actions_found_title') %>

-
<%= t('todos.no_actions_with', :tag_name => @tag_name) %>
+
<%= t('todos.no_actions_with', :tag_name => @tag_title) %>
<% end -%> <%= render :partial => "contexts/mobile_context", :collection => @contexts_to_show -%> -

<%= t('todos.deferred_actions_with', :tag_name=> @tag_name) %>

+

<%= t('todos.deferred_actions_with', :tag_name=> @tag_title) %>

<% unless (@deferred.nil? or @deferred.size == 0) -%> <%= render :partial => "todos/mobile_todo", :collection => @deferred, :locals => { :parent_container_type => "tag" } -%> -
+ <% else -%> -<%= t('todos.no_deferred_actions_with', :tag_name => @tag_name) %> +<%= t('todos.no_deferred_actions_with', :tag_name => @tag_title) %> <% end -%> -

<%= t('todos.completed_actions_with', :tag_name => @tag_name) %>

+

<%= t('todos.completed_actions_with', :tag_name => @tag_title) %>

<% unless (@done.nil? or @done.size == 0) -%> <%= render :partial => "todos/mobile_todo", :collection => @done, :locals => { :parent_container_type => "tag" } %>
<% else -%> -<%= t('todos.no_completed_actions_with', :tag_name => @tag_name) %> +<%= t('todos.no_completed_actions_with', :tag_name => @tag_title) %> <% end -%> \ No newline at end of file diff --git a/app/views/todos/tag.html.erb b/app/views/todos/tag.html.erb index 646f153c..64506aa1 100644 --- a/app/views/todos/tag.html.erb +++ b/app/views/todos/tag.html.erb @@ -8,22 +8,22 @@ :locals => { :collapsible => true } %> <% unless @deferred.nil? -%> - <%= render :partial => "todos/deferred", :locals => { - :deferred => @deferred, + <%= render :partial => "todos/deferred", :locals => { + :deferred => @deferred, :pending => @pending, - :collapsible => true, - :append_descriptor => t('todos.tagged_with', :tag_name => @tag_name), - :parent_container_type => 'tag' + :collapsible => true, + :append_descriptor => t('todos.tagged_with', :tag_name => @tag_title), + :parent_container_type => 'tag' } %> <% end -%> <% unless @hidden_todos.nil? -%> - <%= render :partial => "todos/hidden", :object => @hidden_todos, :locals => { :collapsible => true, :append_descriptor => t('todos.tagged_with', :tag_name => @tag_name) } %> + <%= render :partial => "todos/hidden", :object => @hidden_todos, :locals => { :collapsible => true, :append_descriptor => t('todos.tagged_with', :tag_name => @tag_title) } %> <% end -%> <% unless @done.nil? -%> <%= render :partial => "todos/completed", :object => @done, - :locals => { :collapsible => true, :append_descriptor => t('todos.tagged_with', :tag_name => @tag_name) } %> + :locals => { :collapsible => true, :append_descriptor => t('todos.tagged_with', :tag_name => @tag_title) } %> <% end -%> diff --git a/config/locales/en.yml b/config/locales/en.yml index 338d9531..9feff005 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -391,10 +391,10 @@ en: all_completed: All completed actions feed_title_in_context: in context '%{context}' older_than_days: "" - completed_tagged_page_title: TRACKS::Completed tasks with tag %{tag_name} + completed_tagged_page_title: TRACKS::Completed tasks with tag '%{tag_name}' edit: Edit pending: Pending - completed_actions_with: Completed actions with the tag %{tag_name} + completed_actions_with: Completed actions with the tag '%{tag_name}' deleted_success: The action was deleted succesfully. completed_tasks_title: TRACKS::Completed tasks feed_title_in_project: in project '%{project}' diff --git a/config/routes.rb b/config/routes.rb index 60f777cd..533ca270 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,6 +40,7 @@ ActionController::Routing::Routes.draw do |map| # UPDATE: added support for mobile view. All tags ending on .m will be # routed to mobile view of tags. todos.mobile_tag 'todos/tag/:name.m', :action => "tag", :format => 'm' + todos.mobile_tag 'todos/tag/:name.txt', :action => "tag", :format => 'txt' todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/ todos.done_tag 'todos/done/tag/:name', :action => "done_tag" todos.all_done_tag 'todos/all_done/tag/:name', :action => "all_done_tag" diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index 1abdfadc..20b3c7a5 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -644,4 +644,68 @@ class TodosControllerTest < ActionController::TestCase assert_select("div#notes_todo_#{todo.id} a", 'link me to onenote') assert_select("div#notes_todo_#{todo.id} a[href=onenote:///E:\\OneNote\\dir\\notes.one#PAGE&section-id={FD597D3A-3793-495F-8345-23D34A00DD3B}&page-id={1C95A1C7-6408-4804-B3B5-96C28426022B}&end]", 'link me to onenote') end + + def test_get_boolean_expression_from_parameters_of_tag_view_single_tag + login_as(:admin_user) + get :tag, :name => "single" + assert_equal true, assigns['single_tag'], "should recognize it is a single tag name" + assert_equal "single", assigns['tag_expr'][0][0], "should store the single tag" + end + + def test_get_boolean_expression_from_parameters_of_tag_view_multiple_tags + login_as(:admin_user) + get :tag, :name => "multiple", :and => "tags", :and1 => "present", :and2 => "here" + assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" + assert_equal 4, assigns['tag_expr'].size, "should have 4 AND expressions" + end + + def test_get_boolean_expression_from_parameters_of_tag_view_multiple_tags_without_digitless_and + login_as(:admin_user) + get :tag, :name => "multiple", :and1 => "tags", :and2 => "present", :and3 => "here" + assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" + assert_equal 4, assigns['tag_expr'].size, "should have 4 AND expressions" + end + + def test_get_boolean_expression_from_parameters_of_tag_view_multiple_ORs + login_as(:admin_user) + get :tag, :name => "multiple,tags,present" + assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" + assert_equal 1, assigns['tag_expr'].size, "should have 1 expressions" + assert_equal 3, assigns['tag_expr'][0].size, "should have 3 ORs in 1st expression" + end + + def test_get_boolean_expression_from_parameters_of_tag_view_multiple_ORs_and_ANDS + login_as(:admin_user) + get :tag, :name => "multiple,tags,present", :and => "here,is,two", :and1=>"and,three" + assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" + assert_equal 3, assigns['tag_expr'].size, "should have 3 expressions" + assert_equal 3, assigns['tag_expr'][0].size, "should have 3 ORs in 1st expression" + assert_equal 3, assigns['tag_expr'][1].size, "should have 3 ORs in 2nd expression" + assert_equal 2, assigns['tag_expr'][2].size, "should have 2 ORs in 3rd expression" + end + + def test_set_right_title + login_as(:admin_user) + + get :tag, :name => "foo" + assert_equal "foo", assigns['tag_title'] + get :tag, :name => "foo,bar", :and => "baz" + assert_equal "foo,bar AND baz", assigns['tag_title'] + end + + def test_set_default_tag + login_as(:admin_user) + + get :tag, :name => "foo" + assert_equal "foo", assigns['initial_tags'] + get :tag, :name => "foo,bar", :and => "baz" + assert_equal "foo", assigns['initial_tags'] + end + + def test_tag_text_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :tag, {:name => "foo", :format => "txt" } + assert_response 401 + end + end diff --git a/test/functional/todos_tagging_test.rb b/test/functional/todos_tagging_test.rb deleted file mode 100644 index 46dd1627..00000000 --- a/test/functional/todos_tagging_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -require File.dirname(__FILE__) + '/../test_helper' -require 'todos_controller' - -# Re-raise errors caught by the controller. -class TodosController; def rescue_action(e) raise e end; end - -class TodosControllerTest < ActionController::TestCase - fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos - - def test_get_boolean_expression_from_parameters_of_tag_view_single_tag - login_as(:admin_user) - get :tag, :name => "single" - assert_equal true, assigns['single_tag'], "should recognize it is a single tag name" - assert_equal "single", assigns['tag_expr'][0][0], "should store the single tag" - end - - def test_get_boolean_expression_from_parameters_of_tag_view_multiple_tags - login_as(:admin_user) - get :tag, :name => "multiple", :and => "tags", :and1 => "present", :and2 => "here" - assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" - assert_equal 4, assigns['tag_expr'].size, "should have 4 AND expressions" - end - - def test_get_boolean_expression_from_parameters_of_tag_view_multiple_tags_without_digitless_and - login_as(:admin_user) - get :tag, :name => "multiple", :and1 => "tags", :and2 => "present", :and3 => "here" - assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" - assert_equal 4, assigns['tag_expr'].size, "should have 4 AND expressions" - end - - def test_get_boolean_expression_from_parameters_of_tag_view_multiple_ORs - login_as(:admin_user) - get :tag, :name => "multiple,tags,present" - assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" - assert_equal 1, assigns['tag_expr'].size, "should have 1 expressions" - assert_equal 3, assigns['tag_expr'][0].size, "should have 3 ORs in 1st expression" - end - - def test_get_boolean_expression_from_parameters_of_tag_view_multiple_ORs_and_ANDS - login_as(:admin_user) - get :tag, :name => "multiple,tags,present", :and => "here,is,two", :and1=>"and,three" - assert_equal false, assigns['single_tag'], "should recognize it has multiple tags" - assert_equal 3, assigns['tag_expr'].size, "should have 3 expressions" - assert_equal 3, assigns['tag_expr'][0].size, "should have 3 ORs in 1st expression" - assert_equal 3, assigns['tag_expr'][1].size, "should have 3 ORs in 2nd expression" - assert_equal 2, assigns['tag_expr'][2].size, "should have 2 ORs in 3rd expression" - end - - def test_get_ids_from_tag_expr - login_as(:admin_user) - - # make sure the tags exits - # "multiple,tags,present,here,is,two,and,three".split(',').each { |tag| Tag.find_or_create_by_name(:name=>tag)} - - get :tag, :name => "foo,bar", :and => "baz" - - assert_equal 1, assigns['tag_ids'][0][0], "first id should be 1 for foo" - assert_equal 2, assigns['tag_ids'][0][1], "second id should be 2 for bar" - assert_equal 3, assigns['tag_ids'][1][0], "third id should be 3 for baz" - end - -end \ No newline at end of file diff --git a/test/unit/todo_test.rb b/test/unit/todo_test.rb index 00460139..eb677699 100644 --- a/test/unit/todo_test.rb +++ b/test/unit/todo_test.rb @@ -9,7 +9,7 @@ class TodoTest < ActiveSupport::TestCase @not_completed2 = Todo.find(2).reload @completed = Todo.find(8).reload end - + # Test loading a todo item def test_load assert_kind_of Todo, @not_completed1 @@ -24,13 +24,13 @@ class TodoTest < ActiveSupport::TestCase assert_nil @not_completed1.completed_at assert_equal 1, @not_completed1.user_id end - + def test_completed assert_kind_of Todo, @completed assert @completed.completed? assert_not_nil @completed.completed_at end - + def test_completed_at_cleared_after_toggle_to_active assert_kind_of Todo, @completed assert @completed.completed? @@ -38,8 +38,8 @@ class TodoTest < ActiveSupport::TestCase assert @completed.active? assert_nil @completed.completed_at end - - + + # Validation tests # def test_validate_presence_of_description @@ -49,7 +49,7 @@ class TodoTest < ActiveSupport::TestCase assert_equal 1, @not_completed2.errors.count assert_equal "can't be blank", @not_completed2.errors.on(:description) end - + def test_validate_length_of_description assert_equal "Call dinosaur exterminator", @not_completed2.description @not_completed2.description = generate_random_string(101) @@ -57,7 +57,7 @@ class TodoTest < ActiveSupport::TestCase assert_equal 1, @not_completed2.errors.count assert_equal "is too long (maximum is 100 characters)", @not_completed2.errors.on(:description) end - + def test_validate_length_of_notes assert_equal "Ask him if I need to hire a skip for the corpses.", @not_completed2.notes @not_completed2.notes = generate_random_string(60001) @@ -74,7 +74,7 @@ class TodoTest < ActiveSupport::TestCase assert_equal 1, t.errors.count assert_equal "must be a date in the future", t.errors.on(:show_from) end - + def test_defer_an_existing_todo @not_completed2 assert_equal :active, @not_completed2.aasm_current_state @@ -82,20 +82,20 @@ class TodoTest < ActiveSupport::TestCase assert @not_completed2.save, "should have saved successfully" + @not_completed2.errors.to_xml assert_equal :deferred, @not_completed2.aasm_current_state end - + def test_create_a_new_deferred_todo user = users(:other_user) todo = user.todos.build todo.show_from = next_week todo.context_id = 1 - todo.description = 'foo' + todo.description = 'foo' assert todo.save, "should have saved successfully" + todo.errors.to_xml assert_equal :deferred, todo.aasm_current_state end def test_create_a_new_deferred_todo_by_passing_attributes user = users(:other_user) - todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo') + todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo') assert todo.save, "should have saved successfully" + todo.errors.to_xml assert_equal :deferred, todo.aasm_current_state end @@ -167,15 +167,15 @@ class TodoTest < ActiveSupport::TestCase t.reload assert_equal :deferred, t.aasm_current_state end - + def test_todo_is_not_starred assert !@not_completed1.starred? end - + def test_todo_2_is_not_starred assert !Todo.find(2).starred? end - + def test_todo_is_starred_after_starred_tag_is_added @not_completed1._add_tags('starred') assert @not_completed1.starred? @@ -185,7 +185,7 @@ class TodoTest < ActiveSupport::TestCase @not_completed1.toggle_star! assert @not_completed1.starred? end - + def test_todo_is_not_starred_after_toggle_starred_twice @not_completed1.toggle_star! @not_completed1.toggle_star! @@ -239,4 +239,101 @@ class TodoTest < ActiveSupport::TestCase assert_equal 2, @predecessor_array.size end + def test_finding_todos_with_a_tag + todo = @not_completed1 + todo.tag_list = "a, b, c" + todo.save! + + tag_a = Tag.find_by_name("a") + tag_b = Tag.find_by_name("b") + tag_c = Tag.find_by_name("c") + + todos_with_a = Todo.with_tag(tag_a) + assert 1, todos_with_a.count + assert_equal todo.description, todos_with_a.first.description + + todos_with_b = Todo.with_tag(tag_b) + assert 1, todos_with_b.count + assert_equal todo.id, todos_with_b.first.id + + todo2 = @not_completed2 + todo2.tag_list = "a, c, d" + todo2.save! + + tag_d = Tag.find_by_name("d") + + todos_with_a = Todo.with_tag(tag_a) + assert 2, todos_with_a.count + + todos_with_d = Todo.with_tag(tag_d) + assert 1, todos_with_a.count + end + + def test_finding_todos_with_more_tags_using_OR + todo1 = @not_completed1 + todo1.tag_list = "a, b, c" + todo1.save! + + todo2 = @not_completed2 + todo2.tag_list = "a, c, d" + todo2.save! + + tag_a = Tag.find_by_name("a") + tag_b = Tag.find_by_name("b") + tag_c = Tag.find_by_name("c") + tag_d = Tag.find_by_name("d") + + # overlapping tags + tag_ids = [tag_a.id, tag_c.id] + todos_with_a_or_c = Todo.with_tags(tag_ids) + assert 2, todos_with_a_or_c.count + + # non-overlapping tags + tag_ids = [tag_b.id, tag_d.id] + todos_with_b_or_d = Todo.with_tags(tag_ids) + assert 2, todos_with_b_or_d.count + end + + def test_finding_todos_with_more_tags_using_AND + todo1 = @not_completed1 + todo1.tag_list = "a, b, c" + todo1.save! + + todo2 = @not_completed2 + todo2.tag_list = "a, c, d" + todo2.save! + + tag_a_id = Tag.find_by_name("a").id + tag_b_id = Tag.find_by_name("b").id + + todos_with_a_and_b = Todo.with_tags([tag_a_id]).with_tags([tag_b_id]) + assert 1, todos_with_a_and_b.count + assert todo1.id, todos_with_a_and_b.first.id + end + + def test_finding_todos_with_more_tags_using_AND_and_OR + todo1 = @not_completed1 + todo1.tag_list = "a, b, c" + todo1.save! + + todo2 = @not_completed2 + todo2.tag_list = "a, c, d" + todo2.save! + + tag_a_id = Tag.find_by_name("a").id + tag_b_id = Tag.find_by_name("b").id + tag_c_id = Tag.find_by_name("c").id + + todos_with_aORc_and_b = Todo.with_tags([tag_a_id, tag_c_id]).with_tags([tag_b_id]) + assert 1, todos_with_aORc_and_b.count + assert todo1.id, todos_with_aORc_and_b.first.id + + # let todo2 fit the expression + todo2.tag_list = "a, b, r" + todo2.save! + todos_with_aORc_and_b = Todo.with_tags([tag_a_id, tag_c_id]).with_tags([tag_b_id]) + assert 2, todos_with_aORc_and_b.count + end + + end