diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 3f616b86..37c884ee 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,5 +1,4 @@
module TodosHelper
-
def remote_star_icon(todo=@todo)
link_to( image_tag_for_star(todo),
diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb
index 634b242b..f3ef3363 100644
--- a/app/models/recurring_todo.rb
+++ b/app/models/recurring_todo.rb
@@ -6,6 +6,8 @@ class RecurringTodo < ActiveRecord::Base
has_many :todos
+ include IsTaggable
+
named_scope :active, :conditions => { :state => 'active'}
named_scope :completed, :conditions => { :state => 'completed'}
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 0d6d24e5..bb39407a 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1,9 +1,8 @@
-
-# The Tag model. This model is automatically generated and added to your app if
-# you run the tagging generator included with has_many_polymorphs.
-
class Tag < ActiveRecord::Base
-
+
+ has_many :taggings
+ has_many :taggable, :through => :taggings
+
DELIMITER = "," # Controls how to split and join tagnames from strings. You may need to change the validates_format_of parameters if you change this.
JOIN_DELIMITER = ", "
@@ -12,26 +11,6 @@ class Tag < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :case_sensitive => false
- # Change this validation if you need more complex tag names.
- # validates_format_of :name, :with => /^[a-zA-Z0-9\_\-]+$/, :message => "can not contain special characters"
-
- # Set up the polymorphic relationship.
- has_many_polymorphs :taggables,
- :from => [:todos, :recurring_todos],
- :through => :taggings,
- :dependent => :destroy,
- :skip_duplicates => false,
- :parent_extend => proc {
- # Defined on the taggable models, not on Tag itself. Return the tagnames
- # associated with this record as a string.
- def to_s
- self.map(&:name).sort.join(Tag::JOIN_DELIMITER)
- end
- def all_except_starred
- self.reject{|tag| tag.name == Todo::STARRED_TAG_NAME}
- end
- }
-
# Callback to strip extra spaces from the tagname before saving it. If you
# allow tags to be renamed later, you might want to use the
# before_save callback instead.
@@ -43,9 +22,4 @@ class Tag < ActiveRecord::Base
taggings.create :taggable => taggable, :user => user
end
- # Tag::Error class. Raised by ActiveRecord::Base::TaggingExtensions if
- # something goes wrong.
- class Error < StandardError
- end
-
end
diff --git a/app/models/tagging.rb b/app/models/tagging.rb
index 4d16acd2..d00b1406 100644
--- a/app/models/tagging.rb
+++ b/app/models/tagging.rb
@@ -1,16 +1,13 @@
# The Tagging join model. This model is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
-class Tagging < ActiveRecord::Base
+class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
-
- # If you also need to use acts_as_list, you will have to manage the tagging positions manually by creating decorated join records when you associate Tags with taggables.
- # acts_as_list :scope => :taggable
-
+
# This callback makes sure that an orphaned Tag is deleted if it no longer tags anything.
def after_destroy
tag.destroy_without_callbacks if tag and tag.taggings.count == 0
- end
+ end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index c056ff58..201234c2 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -2,12 +2,16 @@ class Todo < ActiveRecord::Base
after_save :save_predecessors
- # relations
+ # associations
belongs_to :context
belongs_to :project
belongs_to :user
belongs_to :recurring_todo
+ # Tag association
+ include IsTaggable
+
+ # Dependencies associations
has_many :predecessor_dependencies, :foreign_key => 'predecessor_id', :class_name => 'Dependency', :dependent => :destroy
has_many :successor_dependencies, :foreign_key => 'successor_id', :class_name => 'Dependency', :dependent => :destroy
has_many :predecessors, :through => :successor_dependencies
@@ -16,7 +20,7 @@ class Todo < ActiveRecord::Base
:source => :predecessor, :conditions => ['NOT (todos.state = ?)', 'completed']
has_many :pending_successors, :through => :predecessor_dependencies,
:source => :successor, :conditions => ['todos.state = ?', 'pending']
-
+
# scopes for states of this todo
named_scope :active, :conditions => { :state => 'active' }
named_scope :active_or_hidden, :conditions => ["todos.state = ? OR todos.state = ?", 'active', 'project_hidden']
diff --git a/config/environment.rb b/config/environment.rb
index 390f585b..fcb6fed8 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -78,7 +78,6 @@ end
require 'name_part_finder'
require 'tracks/todo_list'
require 'tracks/config'
-require 'tagging_extensions' # Needed for tagging-specific extensions
require 'digest/sha1' #Needed to support 'rake db:fixtures:load' on some ruby installs: http://dev.rousette.org.uk/ticket/557
if ( SITE_CONFIG['authentication_schemes'].include? 'ldap')
diff --git a/lib/is_taggable.rb b/lib/is_taggable.rb
new file mode 100644
index 00000000..2308a0b6
--- /dev/null
+++ b/lib/is_taggable.rb
@@ -0,0 +1,91 @@
+# These methods are adapted from has_many_polymorphs' tagging_extensions
+
+module IsTaggable
+
+ def self.included(klass)
+ klass.class_eval do
+
+ # Add tags associations
+ has_many :taggings, :as => :taggable
+ has_many :tags, :through => :taggings do
+ def to_s
+ self.map(&:name).sort.join(Tag::JOIN_DELIMITER)
+ end
+ def all_except_starred
+ self.reject{|tag| tag.name == Todo::STARRED_TAG_NAME}
+ end
+ end
+
+ def tag_list
+ tags.reload
+ tags.to_s
+ end
+
+ def tag_list=(value)
+ tag_with(value)
+ end
+
+ # Replace the existing tags on self. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
+ def tag_with list
+ list = tag_cast_to_string(list)
+
+ # Transactions may not be ideal for you here; be aware.
+ Tag.transaction do
+ current = tags.map(&:name)
+ _add_tags(list - current)
+ _remove_tags(current - list)
+ end
+
+ self
+ end
+
+ # Add tags to self. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
+ #
+ # We need to avoid name conflicts with the built-in ActiveRecord association methods, thus the underscores.
+ def _add_tags incoming
+ tag_cast_to_string(incoming).each do |tag_name|
+ # added following check to prevent empty tags from being saved (which will fail)
+ unless tag_name.blank?
+ begin
+ tag = Tag.find_or_create_by_name(tag_name)
+ raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record?
+ tags << tag
+ rescue ActiveRecord::StatementInvalid => e
+ raise unless e.to_s =~ /duplicate/i
+ end
+ end
+ end
+ end
+
+ # Removes tags from self. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
+ def _remove_tags outgoing
+ outgoing = tag_cast_to_string(outgoing)
+ tags.delete(*(tags.select{|tag| outgoing.include? tag.name}))
+ end
+
+ def tag_cast_to_string obj
+ case obj
+ when Array
+ obj.map! do |item|
+ case item
+ # removed next line as it prevents using numbers as tags
+ # when /^\d+$/, Fixnum then Tag.find(item).name # This will be slow if you use ids a lot.
+ when Tag then item.name
+ when String then item
+ else
+ raise "Invalid type"
+ end
+ end
+ when String
+ obj = obj.split(Tag::DELIMITER).map do |tag_name|
+ tag_name.strip.squeeze(" ")
+ end
+ else
+ raise "Invalid object of class #{obj.class} as tagging method parameter"
+ end.flatten.compact.map(&:downcase).uniq
+ end
+
+ end
+ end
+
+end
diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb
index fcdc20bc..cfb9cdec 100644
--- a/test/functional/todos_controller_test.rb
+++ b/test/functional/todos_controller_test.rb
@@ -261,7 +261,7 @@ class TodosControllerTest < ActionController::TestCase
def test_find_tagged_with
login_as(:admin_user)
@user = User.find(@request.session['user_id'])
- tag = Tag.find_by_name('foo').todos
+ tag = Tag.find_by_name('foo').taggings
@tagged = tag.count
get :tag, :name => 'foo'
assert_response :success