mirror of
https://github.com/TracksApp/tracks.git
synced 2025-09-22 05:50:47 +02:00
Merge pull request #2255 from ZeiP/feature/#1955_user_tags
#1955: Migrate tags to belong to named users for enhanced privacy.
This commit is contained in:
commit
3ac8702a5e
9 changed files with 130 additions and 24 deletions
|
@ -712,9 +712,8 @@ class TodosController < ApplicationController
|
||||||
|
|
||||||
|
|
||||||
def tags
|
def tags
|
||||||
# TODO: limit to current_user
|
tags_beginning = current_user.tags.where(Tag.arel_table[:name].matches("#{params[:term]}%"))
|
||||||
tags_beginning = Tag.where(Tag.arel_table[:name].matches("#{params[:term]}%"))
|
tags_all = current_user.tags.where(Tag.arel_table[:name].matches("%#{params[:term]}%"))
|
||||||
tags_all = Tag.where(Tag.arel_table[:name].matches("%#{params[:term]}%"))
|
|
||||||
tags_all = tags_all - tags_beginning
|
tags_all = tags_all - tags_beginning
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
@ -3,13 +3,15 @@ class Tag < ApplicationRecord
|
||||||
has_many :taggings
|
has_many :taggings
|
||||||
has_many :taggable, :through => :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 <tt>validates_format_of parameters</tt> if you change this.
|
DELIMITER = ",".freeze # Controls how to split and join tagnames from strings. You may need to change the <tt>validates_format_of parameters</tt> if you change this.
|
||||||
JOIN_DELIMITER = ", ".freeze
|
JOIN_DELIMITER = ", ".freeze
|
||||||
|
|
||||||
# If database speed becomes an issue, you could remove these validations and
|
# If database speed becomes an issue, you could remove these validations and
|
||||||
# rescue the ActiveRecord database constraint errors instead.
|
# rescue the ActiveRecord database constraint errors instead.
|
||||||
validates_presence_of :name
|
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
|
before_create :before_create
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
has_many :tags, dependent: :delete_all
|
||||||
has_many :notes, -> { order "created_at DESC" }, dependent: :delete_all
|
has_many :notes, -> { order "created_at DESC" }, dependent: :delete_all
|
||||||
has_one :preference, dependent: :destroy
|
has_one :preference, dependent: :destroy
|
||||||
has_many :attachments, through: :todos
|
has_many :attachments, through: :todos
|
||||||
|
|
88
db/migrate/20190618202817_add_user_id_to_tag.rb
Normal file
88
db/migrate/20190618202817_add_user_id_to_tag.rb
Normal file
|
@ -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
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table "attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
|
||||||
t.integer "todo_id"
|
t.integer "todo_id"
|
||||||
|
@ -159,6 +159,7 @@ ActiveRecord::Schema.define(version: 2016_01_31_233303) do
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
|
t.integer "user_id"
|
||||||
t.index ["name"], name: "index_tags_on_name"
|
t.index ["name"], name: "index_tags_on_name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,27 @@
|
||||||
## Version 2.4
|
## Version 2.4
|
||||||
|
### New features
|
||||||
* Removed support for deprecated password-hashing algorithm. This
|
* Removed support for deprecated password-hashing algorithm. This
|
||||||
eliminates config.salt. Note the addition of a pre-upgrade step to
|
eliminates config.salt. Note the addition of a pre-upgrade step to
|
||||||
check for obsolete passwords.
|
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
|
* Rails 4.2
|
||||||
* Thin replaces WEBrick as the included web server
|
* Thin replaces WEBrick as the included web server
|
||||||
* Tracks is tested on Ruby 1.9.3, 2.0.0, 2.1, and 2.2.
|
* 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
|
* The MessageGateway will save the received email as an attachement to the todo
|
||||||
* Add a configuration option for serving static assets from Rails
|
* 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
|
## Version 2.3.0
|
||||||
|
|
||||||
### New and changed features
|
### New and changed features
|
||||||
|
|
|
@ -15,43 +15,43 @@ module IsTaggable
|
||||||
self.to_a.reject{|tag| tag.name == Todo::STARRED_TAG_NAME}
|
self.to_a.reject{|tag| tag.name == Todo::STARRED_TAG_NAME}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_list
|
def tag_list
|
||||||
tags.reload
|
tags.reload
|
||||||
tags.to_s
|
tags.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_list=(value)
|
def tag_list=(value)
|
||||||
tag_with(value)
|
tag_with(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace the existing tags on <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
|
# Replace the existing tags on <tt>self</tt>. 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)
|
list = tag_cast_to_string(list)
|
||||||
|
|
||||||
# Transactions may not be ideal for you here; be aware.
|
# Transactions may not be ideal for you here; be aware.
|
||||||
Tag.transaction do
|
Tag.transaction do
|
||||||
current = tags.to_a.map(&:name)
|
current = tags.to_a.map(&:name)
|
||||||
_add_tags(list - current)
|
_add_tags(list - current)
|
||||||
_remove_tags(current - list)
|
_remove_tags(current - list)
|
||||||
end
|
end
|
||||||
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_tag?(tag_name)
|
def has_tag?(tag_name)
|
||||||
return tags.any? {|tag| tag.name == tag_name}
|
return tags.any? {|tag| tag.name == tag_name}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add tags to <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
|
# Add tags to <tt>self</tt>. 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.
|
# 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|
|
tag_cast_to_string(incoming).each do |tag_name|
|
||||||
# added following check to prevent empty tags from being saved (which will fail)
|
# added following check to prevent empty tags from being saved (which will fail)
|
||||||
if tag_name.present?
|
if tag_name.present?
|
||||||
begin
|
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?
|
raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record?
|
||||||
tags << tag
|
tags << tag
|
||||||
rescue ActiveRecord::StatementInvalid => e
|
rescue ActiveRecord::StatementInvalid => e
|
||||||
|
@ -62,11 +62,11 @@ module IsTaggable
|
||||||
end
|
end
|
||||||
|
|
||||||
# Removes tags from <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, or an array of Tags.
|
# Removes tags from <tt>self</tt>. 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)
|
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
|
end
|
||||||
|
|
||||||
def get_tag_name_from_item(item)
|
def get_tag_name_from_item(item)
|
||||||
case item
|
case item
|
||||||
# removed next line as it prevents using numbers as tags
|
# 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"
|
raise "Invalid object of class #{obj.class} as tagging method parameter"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
5
test/fixtures/tags.yml
vendored
5
test/fixtures/tags.yml
vendored
|
@ -10,18 +10,21 @@ end
|
||||||
foo:
|
foo:
|
||||||
id: 1
|
id: 1
|
||||||
name: foo
|
name: foo
|
||||||
|
user_id: 1
|
||||||
created_at: <%= today %>
|
created_at: <%= today %>
|
||||||
updated_at: <%= today %>
|
updated_at: <%= today %>
|
||||||
|
|
||||||
bar:
|
bar:
|
||||||
id: 2
|
id: 2
|
||||||
name: bar
|
name: bar
|
||||||
|
user_id: 1
|
||||||
created_at: <%= today %>
|
created_at: <%= today %>
|
||||||
updated_at: <%= today %>
|
updated_at: <%= today %>
|
||||||
|
|
||||||
baz:
|
baz:
|
||||||
id: 3
|
id: 3
|
||||||
name: baz
|
name: baz
|
||||||
|
user_id: 1
|
||||||
created_at: <%= today %>
|
created_at: <%= today %>
|
||||||
updated_at: <%= today %>
|
updated_at: <%= today %>
|
||||||
|
|
||||||
|
|
|
@ -25,20 +25,20 @@ class TagTest < ActiveSupport::TestCase
|
||||||
tag = Tag.where(:name => "8.1.2").first_or_create
|
tag = Tag.where(:name => "8.1.2").first_or_create
|
||||||
assert !tag.new_record?
|
assert !tag.new_record?
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tag_name_always_lowercase
|
def test_tag_name_always_lowercase
|
||||||
tag = Tag.where(:name => "UPPER").first_or_create
|
tag = Tag.where(:name => "UPPER").first_or_create
|
||||||
assert !tag.new_record?
|
assert !tag.new_record?
|
||||||
|
|
||||||
upper = Tag.where(:name => "upper").first
|
upper = Tag.where(:name => "upper").first
|
||||||
assert_not_nil upper
|
assert_not_nil upper
|
||||||
assert upper.name == "upper"
|
assert upper.name == "upper"
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tag_name_stripped_of_spaces
|
def test_tag_name_stripped_of_spaces
|
||||||
tag = Tag.where(:name => " strip spaces ").first_or_create
|
tag = Tag.where(:name => " strip spaces ").first_or_create
|
||||||
assert !tag.new_record?
|
assert !tag.new_record?
|
||||||
|
|
||||||
assert tag.name == "strip spaces"
|
assert tag.name == "strip spaces"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue