From d74da02194783e8b442f2464dba864916b9cfec6 Mon Sep 17 00:00:00 2001 From: kgaikwad Date: Wed, 14 Dec 2016 04:27:17 -0500 Subject: [PATCH] fixes #6150 - users need taxonomy added on "all Users" With this commit, if "All users" is ticked then locations and organizations are added to users. On edit user page, you can not edit this taxonomy as they are disabled with tooltip "Select all option enabled for this taxonomy". --- app/assets/javascripts/jquery.multi-select.js | 56 ++++++++++--------- app/assets/javascripts/taxonomy_edit.js | 6 ++ app/helpers/form_helper.rb | 22 +++----- app/models/concerns/taxonomix.rb | 18 +++++- app/models/taxonomy.rb | 47 +++++++++------- app/views/taxonomies/_loc_org_tabs.html.erb | 19 ++++--- ...mies_with_selected_ignore_types_objects.rb | 9 +++ lib/tasks/taxonomy.rake | 18 ++++++ test/models/taxonomy_test.rb | 20 +++++++ 9 files changed, 146 insertions(+), 69 deletions(-) create mode 100644 db/migrate/20170922082617_update_taxonomies_with_selected_ignore_types_objects.rb create mode 100644 lib/tasks/taxonomy.rake diff --git a/app/assets/javascripts/jquery.multi-select.js b/app/assets/javascripts/jquery.multi-select.js index 057a7a1cf4d1..0792e1f4f6ae 100644 --- a/app/assets/javascripts/jquery.multi-select.js +++ b/app/assets/javascripts/jquery.multi-select.js @@ -20,44 +20,48 @@ function multiSelectOnLoad(){ function multiSelectToolTips(){ $('select[multiple]').each(function(i,item){ - var mismatches = $(item).attr('data-mismatches'); - var inheriteds = $(item).attr('data-inheriteds'); - var descendants = $(item).attr('data-descendants'); - var useds = $(item).attr('data-useds'); - var msid = '#ms-'+item.id; + var mismatches = $(item).attr('data-mismatches'), + inheriteds = $(item).attr('data-inheriteds'), + descendants = $(item).attr('data-descendants'), + useds = $(item).attr('data-useds'), + used_all = $(item).attr('data-used-all'), + msid = '#ms-'+item.id; // it an
  • items match multiple tooltips, then only the first tooltip will show + if (!(used_all == null || used_all == 'undefined')) { + addTooltipForElements(msid, used_all, + [{class_to_find_li: 'selection', clname: 'selected_taxonomy', label: "Select all option enabled for this taxonomy", position: 'right'}]); + } if (!(mismatches == null || mismatches == 'undefined')) { - var missing_ids = $.parseJSON(mismatches); - $.each(missing_ids, function(index,missing_id){ - opt_id = sanitize(missing_id+''); - $(msid).find('li#'+opt_id+'-selectable').addClass('delete').tooltip({container: 'body', title: __("Select this since it belongs to a host"), placement: "left"}); - }) + addTooltipForElements(msid, mismatches, + [{class_to_find_li: 'selectable', clname: 'delete', label: "Select this since it belongs to a host", position: 'left'}]); } if (!(useds == null || descendants == 'useds')) { - var used_ids = $.parseJSON(useds); - $.each(used_ids, function(index,used_id){ - opt_id = sanitize(used_id+''); - $(msid).find('li#'+opt_id+'-selection').addClass('used_by_hosts').tooltip({container: 'body', title: __("This is used by a host"), placement: "right"}); - }) + addTooltipForElements(msid, useds, + [{class_to_find_li: 'selection', clname: 'used_by_hosts', label: "This is used by a host", position: 'right'}]); } if (!(inheriteds == null || inheriteds == 'undefined')) { - var inherited_ids = $.parseJSON(inheriteds); - $.each(inherited_ids, function(index,inherited_id){ - opt_id = sanitize(inherited_id+''); - $(msid).find('li#'+opt_id+'-selection').addClass('inherited').tooltip({container: 'body', title: __("This is inherited from parent"), placement: "right"}); - }) + addTooltipForElements(msid, inheriteds, + [{class_to_find_li: 'selection', clname: 'inherited', label: "This is inherited from parent", position: 'right'}]); } if (!(descendants == null || descendants == 'undefined')) { - var descendant_ids = $.parseJSON(descendants); - $.each(descendant_ids, function(index,descendant_id){ - opt_id = sanitize(descendant_id+''); - $(msid).find('li#'+opt_id+'-selection').addClass('descendants').tooltip({container: 'body', title: __("Parent is already selected"), placement: "right"}); - $(msid).find('li#'+opt_id+'-selectable').addClass('descendants').tooltip({container: 'body', title: __("Parent is already selected"), placement: "left"}); - }) + addTooltipForElements(msid, descendants, + [{class_to_find_li: 'selection', clname: 'descendants', label: 'Parent is already selected', position: 'right'}, + {class_to_find_li: 'selectable', clname: 'descendants', label: 'Parent is already selected', position: 'left'}]); } }) } +function addTooltipForElements(msid, json_ids, tooltips_with_options) { + var ids = $.parseJSON(json_ids); + $.each(ids, function(index, id){ + opt_id = sanitize(id+''); + $.each(tooltips_with_options, function(tooltip_index, tooltip_opts) { + $(msid).find('li#'+opt_id+'-' + tooltip_opts.class_to_find_li).addClass(tooltip_opts.clname).tooltip( + {container: 'body', title: __(tooltip_opts.label), placement: tooltip_opts.position}); + }); + }); +} + // function below is copy/paste from source of multi-select-rails gem // it takes the option value and returns a value used in css id function sanitize(value){ diff --git a/app/assets/javascripts/taxonomy_edit.js b/app/assets/javascripts/taxonomy_edit.js index cb6e82596d99..fc66edcfbfd6 100644 --- a/app/assets/javascripts/taxonomy_edit.js +++ b/app/assets/javascripts/taxonomy_edit.js @@ -2,6 +2,11 @@ $(function() { $(':checkbox').each(function(i, item) { ignore_checked(item); }); + + $('form').on('submit', function() { + $('select.without_select2').prop('disabled', false); + $('.ms-selection li.ms-elem-selection').removeClass('disabled'); + }); }); function ignore_checked(item) { @@ -11,6 +16,7 @@ function ignore_checked(item) { if ($(item).is(':checked')) { current_select.attr('disabled', 'disabled'); + current_select.find("option").prop('selected', true); } else { current_select.removeAttr('disabled'); } diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index eeb9cee413f2..94142fbe8423 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -57,13 +57,9 @@ def multiple_checkboxes(f, attr, klass, associations, options = {}, html_options # add hidden field for options[:disabled] def multiple_selects(f, attr, associations, selected_ids, options = {}, html_options = {}) options[:size] = "col-md-10" - case attr - when :organizations - klass = Organization - when :locations - klass = Location - else - klass = nil + klass = nil + if [:organizations, :locations].include?(attr) + klass = attr.to_s.classify.constantize end authorized = AssociationAuthorizer.authorized_associations(associations.reorder(nil), klass).all @@ -78,12 +74,12 @@ def multiple_selects(f, attr, associations, selected_ids, options = {}, html_opt attr_ids = attr.to_s attr_ids = (attr_ids.singularize + '_ids').to_sym unless attr_ids.end_with?('_ids') hidden_fields = '' - html_options["data-useds"] ||= "[]" - JSON.parse(html_options["data-useds"]).each do |disabled_value| - hidden_fields += f.hidden_field(attr_ids, :multiple => true, :value => disabled_value, :id => '') - end - unauthorized.each do |unauthorized_value| - hidden_fields += f.hidden_field(attr_ids, :multiple => true, :value => unauthorized_value, :id => '') + html_options["data-useds"] ||= '[]' + html_options["data-used-all"] ||= '[]' + [JSON.parse(html_options["data-useds"]), JSON.parse(html_options["data-used-all"]), unauthorized].each do |optns| + optns.each do |value| + hidden_fields += f.hidden_field(attr_ids, :multiple => true, :value => value, :id = '') + end end hidden_fields + f.collection_select(attr_ids, authorized.sort_by { |a| a.to_s }, :id, options.delete(:object_label_method) || :to_label, options.merge(:selected => selected_ids), diff --git a/app/models/concerns/taxonomix.rb b/app/models/concerns/taxonomix.rb index 5221afe94805..2adbb12c0ae1 100644 --- a/app/models/concerns/taxonomix.rb +++ b/app/models/concerns/taxonomix.rb @@ -12,6 +12,7 @@ module Taxonomix :through => TAXONOMY_JOIN_TABLE, :source => :taxonomy, :validate => false after_initialize :set_current_taxonomy + before_save :add_taxonomies_with_ignore_type_enabled scoped_search :relation => :locations, :on => :name, :rename => :location, :complete_value => true, :only_explicit => true scoped_search :relation => :locations, :on => :id, :rename => :location_id, :complete_enabled => false, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER @@ -83,7 +84,6 @@ def taxable_ids(loc = which_location, org = which_organization, inner_method = w # Passing a nil or [] value as taxonomy equates to "Any context". # Any other value will be understood as 'IDs available in this taxonomy'. def inner_ids(taxonomy, taxonomy_class, inner_method) - return unscoped.pluck("#{table_name}.id") if taxonomy_class.ignore?(to_s) return inner_select(taxonomy, inner_method) if taxonomy.present? return [] unless User.current.present? # Any available taxonomy to the current user @@ -211,6 +211,22 @@ def ensure_taxonomies_not_escalated end end + def add_taxonomies_with_ignore_type_enabled + Taxonomy.enabled_taxonomies.each do |taxonomy_pluralize_str| + taxonomy_class_name = taxonomy_pluralize_str.classify + obj_ids = taxonomy_class_name.constantize.taxonomy_ids_by_ignore_type(self.class.to_s) + assign_taxonomy_ids(taxonomy_class_name, obj_ids) if obj_ids.present? + end + end + + def assign_taxonomy_ids(taxonomy_class_name, taxonomy_ids_by_ignore_type) + taxonomy_ids_meth = taxonomy_class_name.underscore + '_ids' + existing_obj_ids = self.send(taxonomy_ids_meth) + return if (taxonomy_ids_by_ignore_type - existing_obj_ids).blank? + taxonomy_ids_by_ignore_type.concat(existing_obj_ids).uniq! + self.send("#{taxonomy_ids_meth}=", taxonomy_ids_by_ignore_type) + end + protected def taxonomy_foreign_key_conditions diff --git a/app/models/taxonomy.rb b/app/models/taxonomy.rb index 99220429d4dc..5c858103d634 100644 --- a/app/models/taxonomy.rb +++ b/app/models/taxonomy.rb @@ -34,6 +34,8 @@ class Taxonomy < ApplicationRecord validate :parent_id_does_not_escalate, :if => Proc.new { |t| t.ancestry_changed? && t.parent_id != 0 && t.parent.present? } validates :name, :presence => true, :uniqueness => {:scope => [:ancestry, :type], :case_sensitive => false} + before_save :add_references_for_selected_ignore_types + def self.inherited(child) child.instance_eval do scoped_search :on => :description, :complete_enabled => :false, :only_explicit => true @@ -56,6 +58,8 @@ def self.inherited(child) end } + scope :taxonomies_by_ignore_type, ->(taxable_type) { where("ignore_types LIKE ?", "%#{taxable_type}%") } + def self.no_taxonomy_scope as_taxonomy nil, nil do yield if block_given? @@ -86,6 +90,10 @@ def self.ignore?(taxable_type) end end + def self.taxonomy_ids_by_ignore_type(ignore_type_str) + taxonomies_by_ignore_type(ignore_type_str).pluck(:id) + end + # if taxonomy e.g. organization was not set by current context (e.g. Any organization) # then we have to compute what this context mean for current user (what organizations # they are assigned to) @@ -134,25 +142,6 @@ def dup new end - # overwrite *_ids since need to check if ignored? - don't overwrite location_ids and organization_ids since these aren't ignored - (TaxHost::HASH_KEYS - [:location_ids, :organization_ids]).each do |key| - # def domain_ids - # if ignore?("Domain") - # Domain.pluck(:id) - # else - # super() # self.domain_ids - # end - define_method(key) do - klass = hash_key_to_class(key) - if ignore?(klass) - return User.unscoped.except_admin.except_hidden.map(&:id) if klass == "User" - return klass.constantize.pluck(:id) - else - super() - end - end - end - def expire_topbar_cache (users + User.only_admin).each { |u| u.expire_topbar_cache } end @@ -214,8 +203,13 @@ def destroy_taxable_taxonomies :to => :tax_host def assign_default_templates - Template.where(:default => true).group_by { |t| t.class.to_s.underscore.pluralize }.each do |association, templates| - self.send("#{association}=", self.send(association) + templates.select(&:valid?)) + # Template.where(:default => true).group_by { |t| t.class.to_s.underscore.pluralize }.each do |association, templates| + # self.send("#{association}=", self.send(association) + templates.select(&:valid?)) + Template.where(:default => true).select(&:valid?).find_each do |template| + classify_template_str = template.class.to_s + unless self.ignore_types.include?(classify_template_str) + self.send(classify_template_str.underscore.pluralize.to_s) << template + end end end @@ -243,4 +237,15 @@ def parent_id_does_not_escalate false end end + + def add_references_for_selected_ignore_types + ignore_types = self.ignore_types + ignore_types.each do |klass| + klass_obj_ids = klass.constantize.unscoped.pluck(:id) + klass_ids_meth = klass.underscore + '_ids' + existing_ids = self.send(klass_ids_meth) + next if (klass_obj_ids - existing_ids).blank? + self.send("#{klass_ids_meth}=", klass_obj_ids) + end + end end diff --git a/app/views/taxonomies/_loc_org_tabs.html.erb b/app/views/taxonomies/_loc_org_tabs.html.erb index 2f61f924fd67..4b31179f7601 100644 --- a/app/views/taxonomies/_loc_org_tabs.html.erb +++ b/app/views/taxonomies/_loc_org_tabs.html.erb @@ -5,12 +5,14 @@ <% if show_location_tab? %>
    + <% location_ids_with_select_all_for_ignore_type = Location.taxonomy_ids_by_ignore_type(obj.class.to_s) %> <%= location_selects f, obj.used_or_selected_location_ids, - {:disabled => obj.used_location_ids }.merge!(select_options), + {:disabled => obj.used_location_ids.concat(location_ids_with_select_all_for_ignore_type)}.merge!(select_options), {'data-descendants' => obj.children_of_selected_location_ids.to_json, - 'data-useds' => obj.used_location_ids.to_json }.merge!(html_options[:location]) %> + 'data-useds' => obj.used_location_ids.to_json, + 'data-used-all' => location_ids_with_select_all_for_ignore_type.to_json }.merge!(html_options[:location]) %> <% if obj.kind_of? User %> - <%= select_f f, :default_location_id, (obj.admin? ? Location.all : obj.my_locations), :id, :title, + <%= select_f f, :default_location_id, (obj.admin? ? Location.all : obj.my_locations), :id, :title, { :include_blank => true }, { :label => _('Default on login') } %> <% end %>
    @@ -18,14 +20,15 @@ <% if show_organization_tab? %>
    + <% organization_ids_with_select_all_for_ignore_type = Organization.taxonomy_ids_by_ignore_type(obj.class.to_s) %> <%= organization_selects f, obj.used_or_selected_organization_ids, - {:disabled => obj.used_organization_ids }.merge!(select_options), + {:disabled => obj.used_organization_ids.concat(organization_ids_with_select_all_for_ignore_type)}.merge!(select_options), {'data-descendants' => obj.children_of_selected_organization_ids.to_json, - 'data-useds' => obj.used_organization_ids.to_json }.merge!(html_options[:organization]) %> + 'data-useds' => obj.used_organization_ids.to_json, + 'data-used-all' => organization_ids_with_select_all_for_ignore_type.to_json }.merge!(html_options[:organization]) %> <% if obj.kind_of? User %> <%= select_f f, :default_organization_id,(obj.admin? ? Organization.all : obj.my_organizations), :id, :title, - { :include_blank => true }, { :label => _('Default on login') } %> + { :include_blank => true }, { :label => _('Default on login') } %> <% end %>
    -<% end %> - +<% end %> \ No newline at end of file diff --git a/db/migrate/20170922082617_update_taxonomies_with_selected_ignore_types_objects.rb b/db/migrate/20170922082617_update_taxonomies_with_selected_ignore_types_objects.rb new file mode 100644 index 000000000000..34d809b2ad7a --- /dev/null +++ b/db/migrate/20170922082617_update_taxonomies_with_selected_ignore_types_objects.rb @@ -0,0 +1,9 @@ +class UpdateTaxonomiesWithSelectedIgnoreTypesObjects < ActiveRecord::Migration[4.2] + def up + say "This Migration will take time for updating organization & location records with all objects of selected ignore_types." + Rake::Task['taxonomy:update_taxonomy'].invoke + end + + def down + end +end diff --git a/lib/tasks/taxonomy.rake b/lib/tasks/taxonomy.rake new file mode 100644 index 000000000000..aa4af86e5b5e --- /dev/null +++ b/lib/tasks/taxonomy.rake @@ -0,0 +1,18 @@ +namespace :taxonomy do + desc <<-END_DESC + This task will update organization & location records with ignore_types. + END_DESC + task :update_taxonomy => :environment do + User.as_anonymous_admin do + Organization.unscoped.each do |org| + success = org.save + puts "Failed to save Organization #{org.id}- #{org.errors.full_messages.inspect}" unless success + end + + Location.unscoped.each do |loc| + success = loc.save + puts "Failed to save Location #{loc.id}- #{loc.errors.full_messages.inspect}" unless success + end + end + end +end diff --git a/test/models/taxonomy_test.rb b/test/models/taxonomy_test.rb index 27174e62ead9..141e337ca153 100644 --- a/test/models/taxonomy_test.rb +++ b/test/models/taxonomy_test.rb @@ -73,4 +73,24 @@ class TaxonomyTest < ActiveSupport::TestCase location.save assert_match /expecting organizations/, location.errors.messages[:organizations].first end + + test "#taxonomy_ids_by_ignore_type should return ids of taxonomy_type for which 'Select All' option is checked for a resource." do + FactoryBot.create(:organization, :ignore_types => [ 'User' ]) + user = FactoryBot.create(:user, :id => 20) + organization_ids_with_select_all = Organization.taxonomy_ids_by_ignore_type(user.class.to_s) + assert_not organization_ids_with_select_all.empty? + assert_equal organization_ids_with_select_all.count, 1 + end + + test "#add_references_for_selected_ignore_types assigns all objects of selected ignore_types to taxonomy" do + user = FactoryBot.create(:user, :login => 'foo_test_u1') + org1 = FactoryBot.create(:organization, :ignore_types => ['User']) + assert_includes org1.user_ids, user.id + end + + test "taxonomy created with ignore_type Domain, this taxonomy will get assign to all new domains created after it" do + org1 = FactoryBot.create(:organization, :ignore_types => ['Domain'], :name => "Test_OrgA") + domain1 = FactoryBot.create(:domain, :name => 'test-domain.com') + assert_includes domain1.organization_ids, org1.id + end end