diff --git a/app/models/container_label_tag_mapping.rb b/app/models/container_label_tag_mapping.rb index 3676220ea36..6da843ab30d 100644 --- a/app/models/container_label_tag_mapping.rb +++ b/app/models/container_label_tag_mapping.rb @@ -21,6 +21,9 @@ class ContainerLabelTagMapping < ApplicationRecord belongs_to :tag + scope :any_value, -> { where(:label_value => nil) } + scope :specific_value, -> { where.not(:label_value => nil) } + require_nested :Mapper # Return ContainerLabelTagMapping::Mapper instance that holds all current mappings, @@ -31,24 +34,21 @@ def self.mapper # Assigning/unassigning should be possible without Mapper instance, perhaps in another process. - # Checks whether a Tag record is under mapping control. - # TODO: expensive. + # Checks whether a Tag record is under mapping control. TODO: Remove? Only used by tests. def self.controls_tag?(tag) - return false unless tag.classification.try(:read_only) # never touch user-assignable tags. - tag_ids = [tag.id, tag.category.tag_id].uniq - where(:tag_id => tag_ids).any? + Tag.controlled_by_mapping.where(:id => tag.id).exists? end # Assign/unassign mapping-controlled tags, preserving user-assigned tags. # All tag references must have been resolved first by Mapper#find_or_create_tags. def self.retag_entity(entity, tag_references) mapped_tags = Mapper.references_to_tags(tag_references) - existing_tags = entity.tags + existing_tags = entity.tags.controlled_by_mapping Tagging.transaction do (mapped_tags - existing_tags).each do |tag| Tagging.create!(:taggable => entity, :tag => tag) end - (existing_tags - mapped_tags).select { |tag| controls_tag?(tag) }.tap do |tags| + (existing_tags - mapped_tags).tap do |tags| Tagging.where(:taggable => entity, :tag => tags.collect(&:id)).destroy_all end end diff --git a/app/models/tag.rb b/app/models/tag.rb index 5cb8abd8d3d..fa44d4991a1 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -4,10 +4,15 @@ class Tag < ApplicationRecord virtual_has_one :category, :class_name => "Classification" virtual_has_one :categorization, :class_name => "Hash" - has_many :container_label_tag_mappings + has_many :container_label_tag_mappings # see also controlled_by_mapping scope before_destroy :remove_from_managed_filters + # Note those scopes exclude Tags that don't have a Classification. + scope :visible, -> { joins(:classification).merge(Classification.visible) } + scope :read_only, -> { joins(:classification).merge(Classification.read_only) } + scope :writable, -> { joins(:classification).merge(Classification.writable) } + def self.list(object, options = {}) ns = get_namespace(options) if ns[0..7] == "/virtual" @@ -152,6 +157,24 @@ def categorization end end + # @return [ActiveRecord::Relation] Scope for tags controlled by ContainerLabelTagMapping. + # May include not only "entry" tags but also some parent "category" tags. + def self.controlled_by_mapping + # TODO: complex query, can we simply select by prefixes e.g. '/managed/kubernetes:%'? + # User can create categories with such prefix, but they won't be read_only. + + # Entry tags from specific value->tag mappings. + + mapped_specific_tags = read_only.where(:id => ContainerLabelTagMapping.specific_value.pluck(:tag_id)) + + # Entry tags for name->category mappings. + mapped_categories = Classification.where(:tag => ContainerLabelTagMapping.any_value.pluck(:tag_id)) + mapped_entries = Classification.where(:parent => mapped_categories) + mapped_child_tags = read_only.where(:id => mapped_entries.select(:tag_id)) + + mapped_specific_tags.or(mapped_child_tags) + end + private def remove_from_managed_filters