mysociety/alaveteli

View on GitHub
lib/has_tag_string/has_tag_string.rb

Summary

Maintainability
A
45 mins
Test Coverage
# lib/has_tag_string.rb:
# Lets a model have tags, represented as space separate strings in a public
# interface, but stored in the database as keys. Each tag can have a value
# followed by a colon - e.g. url:http://www.flourish.org
#
# Copyright (c) 2010 UK Citizens Online Democracy. All rights reserved.
# Email: hello@mysociety.org; WWW: http://www.mysociety.org/

module HasTagString
  # Represents one tag of one model.
  # The migration to make this is currently only in WDTK code.
  class HasTagStringTag < ActiveRecord::Base
    # TODO: strip_attributes

    belongs_to :model, polymorphic: true

    validates_presence_of :name

    def self.from_string(tag)
      name, value = split_tag_into_name_value(tag)
      new(name: name, value: value)
    end

    # Return instance of the model that this tag tags
    def tagged_model
      model_type.constantize.find(model_id)
    end

    # For display purposes, returns the name and value as a:b, or
    # if there is no value just the name a
    def name_and_value
      ret = name
      ret += ':' + value unless value.nil?
      ret
    end

    # Parses a text version of one single tag, such as 'a:b' and returns
    # the name and value, with nil for value if there isn't one.
    def self.split_tag_into_name_value(tag)
      sections = tag.split(/:/)
      name = sections[0]
      if sections[1]
        value = sections[1, sections.size].join(':')
      else
        value = nil
      end
      [name, value]
    end
  end

  # Methods which are added to the model instances being tagged
  module InstanceMethods
    # Given an input string of tags, sets all tags to that string.
    # TODO: This immediately saves the new tags.
    def tag_string=(tag_string)
      tag_string = '' if tag_string.nil?

      tag_string = tag_string.strip
      # split tags apart
      tags = tag_string.split(/\s+/).uniq

      ActiveRecord::Base.transaction do
        self.tags.each(&:destroy)
        self.tags = []
        tags.each do |tag|
          # see if is a machine tags (i.e. a tag which has a value)
          name, value = HasTagStringTag.split_tag_into_name_value(tag)

          tag = HasTagStringTag.new(
            model_type: self.class.base_class.to_s,
            model_id: id,
            name: name, value: value
          )
          self.tags << tag
        end
      end
    end

    # Returns the tags the model has, as a space separated string
    def tag_string
      tags.map(&:name_and_value).join(' ')
    end

    # Returns the tags the model has, as an array of pairs of key/value
    # (this can't be a dictionary as you can have multiple instances of a
    # key with different values)
    def tag_array
      tags.map { |t| [t.name, t.value] }
    end

    # Returns a list of all the strings someone might want to search for.
    # So that is the key by itself, or the key and value.
    # e.g. if a request was tagged openlylocal_id:12345, they might
    # want to search for 'openlylocal_id' or for 'openlylocal_id:12345' to find
    # it.
    def tag_array_for_search
      ret = {}
      tags.each do |tag|
        ret[tag.name] = 1
        ret[tag.name_and_value] = 1
      end

      ret.keys.sort
    end

    # Test to see if class is tagged with the given tag
    def has_tag?(tag_as_string) # rubocop:disable Naming::PredicateName
      tags.each do |tag|
        return true if tag.name == tag_as_string
      end
      false
    end

    alias tagged? has_tag?

    class TagNotFound < StandardError
    end

    # If the tag is a machine tag, returns array of its values
    def get_tag_values(tag_as_string)
      found = false
      results = []
      tags.each do |tag|
        if tag.name == tag_as_string
          found = true
          results << tag.value unless tag.value.nil?
        end
      end
      raise TagNotFound unless found

      results
    end

    # Adds a new tag to the model, if it isn't already there
    def add_tag_if_not_already_present(tag_as_string)
      return tag_string if has_tag?(tag_as_string)

      self.tag_string = tag_string + ' ' + tag_as_string
    end

    # Removes tag to the model
    def remove_tag(tag_as_string)
      return tag_string unless has_tag?(tag_as_string)

      self.tag_string = tag_string.sub(/\b#{tag_as_string}\b/, '').strip
    end
  end

  # Methods which are added to the model class being tagged
  module ClassMethods
    # Find all public bodies with a particular tag
    def find_by_tag(tag_as_string)
      join_sql = <<-EOF.strip_heredoc.squish
      LEFT JOIN has_tag_string_tags
      ON has_tag_string_tags.model_type = '#{ self }'
      AND has_tag_string_tags.model_id = #{ table_name }.id
      EOF

      search =
        joins(join_sql).
        where(has_tag_string_tags: { name: tag_as_string, model_type: to_s }).
        references(:has_tag_string_tags)

      ordered =
        if respond_to?(:translated_locales)
          search.
            includes(:translations).
            references(:translations).
            merge(translation_class.order(:name))
        else
          search.order(:name)
        end

      ordered.distinct
    end
  end

  ######################################################################
  # Main entry point, add has_tag_string to your model.
  module HasMethods
    def has_tag_string # rubocop:disable Naming/PredicateName
      klass = to_s
      has_many :tags, -> { where(model_type: klass) },
               foreign_key: "model_id",
               class_name: 'HasTagString::HasTagStringTag'

      include InstanceMethods
      self.class.send :include, ClassMethods
    end
  end
end

ActiveRecord::Base.extend HasTagString::HasMethods