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