diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb
index 7a3fa8b4..a23419b8 100644
--- a/app/controllers/todos_controller.rb
+++ b/app/controllers/todos_controller.rb
@@ -712,9 +712,8 @@ class TodosController < ApplicationController
def tags
- # TODO: limit to current_user
- tags_beginning = Tag.where(Tag.arel_table[:name].matches("#{params[:term]}%"))
- tags_all = Tag.where(Tag.arel_table[:name].matches("%#{params[:term]}%"))
+ tags_beginning = current_user.tags.where(Tag.arel_table[:name].matches("#{params[:term]}%"))
+ tags_all = current_user.tags.where(Tag.arel_table[:name].matches("%#{params[:term]}%"))
tags_all = tags_all - tags_beginning
respond_to do |format|
diff --git a/app/models/tag.rb b/app/models/tag.rb
index aa193c20..2c710dad 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -3,13 +3,15 @@ class Tag < ApplicationRecord
has_many :taggings
has_many :taggable, :through => :taggings
+ belongs_to :user
+
DELIMITER = ",".freeze # 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 = ", ".freeze
# If database speed becomes an issue, you could remove these validations and
# rescue the ActiveRecord database constraint errors instead.
validates_presence_of :name
- validates_uniqueness_of :name, :case_sensitive => false
+ validates_uniqueness_of :name, :scope => "user_id", :case_sensitive => false
before_create :before_create
diff --git a/app/models/user.rb b/app/models/user.rb
index 99a54528..736db95e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -94,6 +94,7 @@ class User < ApplicationRecord
end
end
+ has_many :tags, dependent: :delete_all
has_many :notes, -> { order "created_at DESC" }, dependent: :delete_all
has_one :preference, dependent: :destroy
has_many :attachments, through: :todos
diff --git a/db/migrate/20190618202817_add_user_id_to_tag.rb b/db/migrate/20190618202817_add_user_id_to_tag.rb
new file mode 100644
index 00000000..9483338c
--- /dev/null
+++ b/db/migrate/20190618202817_add_user_id_to_tag.rb
@@ -0,0 +1,88 @@
+class AddUserIdToTag < ActiveRecord::Migration[5.2]
+ def self.up
+ add_column :tags, :user_id, :integer
+
+ # Find uses of each tag for both Todos and RecurringTodos to
+ # figure out which users use which tags.
+ @tags = execute <<-EOQ
+ SELECT t.id AS tid, tds.user_id AS todo_uid, rt.user_id AS rtodo_uid
+ FROM tags t
+ JOIN taggings tgs ON tgs.tag_id = t.id
+ LEFT OUTER JOIN todos tds
+ ON tgs.taggable_type = "Todo" AND tds.id = tgs.taggable_id
+ LEFT OUTER JOIN recurring_todos rt
+ ON tgs.taggable_type = "RecurringTodo" AND rt.id = tgs.taggable_id
+ WHERE rt.id IS NOT NULL OR tds.id IS NOT NULL
+ GROUP BY t.id, tds.user_id, rt.user_id
+ EOQ
+
+ # Map each tag to the users using it.
+ @tag_users = {}
+ @tags.each do |row|
+ uid = (row[1] ? row[1] : row[2])
+ if not @tag_users[row[0]]
+ @tag_users[row[0]] = [uid]
+ elsif not @tag_users[row[0]].include? uid
+ @tag_users[row[0]] << uid
+ end
+ end
+
+ # Go through the tags assigning users and duplicating as necessary.
+ @tag_users.each do |tid, uids|
+ tag = Tag.find(tid)
+
+ # One of the users will get the original tag instance, but first
+ # duplicate their own copy to all the others.
+ extras = uids.length - 1
+ extras.times do |n|
+ uid = uids[n+1]
+
+ # Create a duplicate of the tag assigned to the user.
+ new_tag = tag.dup
+ new_tag.user_id = uid
+ new_tag.save!
+
+ # Move all the user's regular todos to the new tag.
+ execute <<-EOQ
+ UPDATE taggings ta
+ JOIN todos t
+ ON ta.taggable_type = "Todo" AND t.id = ta.taggable_id
+ SET ta.tag_id = #{new_tag.id}
+ WHERE t.user_id = #{uid} AND ta.tag_id = #{tid}
+ EOQ
+
+ # Move all the user's recurring todos to the new tag.
+ execute <<-EOQ
+ UPDATE taggings ta
+ JOIN recurring_todos t
+ ON ta.taggable_type = "RecurringTodo" AND t.id = ta.taggable_id
+ SET ta.tag_id = #{new_tag.id}
+ WHERE t.user_id = #{uid} AND ta.tag_id = #{tid}
+ EOQ
+ end
+
+ tag.user_id = uids[0]
+ tag.save!
+ end
+
+ # Set all unowned tags to the only user, if there's only one. Otherwise
+ # remove them since there's no way of knowing who they belong to.
+ if User.all.count == 1
+ uid = User.first.id
+ execute <<-EOQ
+ UPDATE tags
+ SET user_id = #{uid}
+ WHERE user_id IS NULL
+ EOQ
+ else
+ execute <<-EOQ
+ DELETE FROM tags
+ WHERE user_id IS NULL
+ EOQ
+ end
+ end
+ def self.down
+ remove_column :tags, :user_id
+ end
+end
+
diff --git a/db/schema.rb b/db/schema.rb
index 93a6c96f..d961d407 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2016_01_31_233303) do
+ActiveRecord::Schema.define(version: 2019_06_18_202817) do
create_table "attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.integer "todo_id"
@@ -159,6 +159,7 @@ ActiveRecord::Schema.define(version: 2016_01_31_233303) do
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "user_id"
t.index ["name"], name: "index_tags_on_name"
end
diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
index 038ba777..e3a0f8b0 100644
--- a/doc/CHANGELOG.md
+++ b/doc/CHANGELOG.md
@@ -1,13 +1,27 @@
## Version 2.4
+### New features
* Removed support for deprecated password-hashing algorithm. This
eliminates config.salt. Note the addition of a pre-upgrade step to
check for obsolete passwords.
+* All tags now belong to a user. Existing tags are migrated to users based on
+ the taggings and duplicated as necessary. If there's only one user, all unused tags are
+ assigned to them, otherwise unused tags are removed.
+* All REST APIs now also accept user token as password.
+* The stats view now uses Charts.js instead of the Flash-based chart library.
+* A Docker environment is used unless the .skip-docker file exists.
* Rails 4.2
* Thin replaces WEBrick as the included web server
* Tracks is tested on Ruby 1.9.3, 2.0.0, 2.1, and 2.2.
* The MessageGateway will save the received email as an attachement to the todo
* Add a configuration option for serving static assets from Rails
+### Removed features
+* Ruby versions below 2.4 are no longer supported.
+
+### Bug fixes
+* Multiple fixes to REST APIs.
+* Several UI bugs fixed.
+
## Version 2.3.0
### New and changed features
diff --git a/lib/is_taggable.rb b/lib/is_taggable.rb
index 122f0409..59944811 100644
--- a/lib/is_taggable.rb
+++ b/lib/is_taggable.rb
@@ -15,43 +15,43 @@ module IsTaggable
self.to_a.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
+ 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.to_a.map(&:name)
_add_tags(list - current)
_remove_tags(current - list)
end
-
+
self
end
def has_tag?(tag_name)
return tags.any? {|tag| tag.name == tag_name}
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
+ 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)
if tag_name.present?
begin
- tag = Tag.where(:name => tag_name).first_or_create
+ tag = self.user.tags.where(:name => tag_name).first_or_create
raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record?
tags << tag
rescue ActiveRecord::StatementInvalid => e
@@ -62,11 +62,11 @@ module IsTaggable
end
# Removes tags from self. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
- def _remove_tags outgoing
+ def _remove_tags(outgoing)
outgoing = tag_cast_to_string(outgoing)
- tags.destroy(*(tags.select{|tag| outgoing.include? tag.name}))
+ tags.destroy(*(self.user.tags.select{|tag| outgoing.include? tag.name}))
end
-
+
def get_tag_name_from_item(item)
case item
# removed next line as it prevents using numbers as tags
@@ -94,8 +94,6 @@ module IsTaggable
raise "Invalid object of class #{obj.class} as tagging method parameter"
end
end
-
end
end
-
end
diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml
index 6251df50..c746e7f3 100644
--- a/test/fixtures/tags.yml
+++ b/test/fixtures/tags.yml
@@ -10,18 +10,21 @@ end
foo:
id: 1
name: foo
+ user_id: 1
created_at: <%= today %>
updated_at: <%= today %>
bar:
id: 2
name: bar
+ user_id: 1
created_at: <%= today %>
updated_at: <%= today %>
baz:
id: 3
name: baz
+ user_id: 1
created_at: <%= today %>
updated_at: <%= today %>
-
+
diff --git a/test/models/tag_test.rb b/test/models/tag_test.rb
index b070554c..b0c9b2f1 100644
--- a/test/models/tag_test.rb
+++ b/test/models/tag_test.rb
@@ -25,20 +25,20 @@ class TagTest < ActiveSupport::TestCase
tag = Tag.where(:name => "8.1.2").first_or_create
assert !tag.new_record?
end
-
+
def test_tag_name_always_lowercase
tag = Tag.where(:name => "UPPER").first_or_create
assert !tag.new_record?
-
+
upper = Tag.where(:name => "upper").first
assert_not_nil upper
assert upper.name == "upper"
end
-
+
def test_tag_name_stripped_of_spaces
tag = Tag.where(:name => " strip spaces ").first_or_create
assert !tag.new_record?
-
+
assert tag.name == "strip spaces"
end