<file_sep>module CustomFields

  module Types

    module TagSet

      class Tag

        include Mongoid::Document

        field :name,      :localize => true
        field :_slug,     :localize => false

        before_validation :set_slug

        validates_presence_of :name
        validates_uniqueness_of :name, :_slug

        def as_json(options = nil)
          super :methods => %w(_id name _slug)
        end


        def self.available_tags
          locale = Mongoid::Fields::I18n.locale.to_s

          Tag.all().asc("name.#{locale}".to_sym).to_ary
        end

        def self.find_tag_by_name(tag_name, locale = Mongoid::Fields::I18n.locale.to_s)
          Tag.where("name.#{locale}" => /^#{tag_name.strip}$/i ).first
        end

        def self.find_tag_by_slug(slug)
          Tag.where("_slug" => slug ).first
        end

        def self.class_str_to_field_name(klass)
          klass.to_s.gsub("::", "_")
        end

        def self.make_inverse_field(klass, name)
          class_str_to_field_name(klass)+"__#{name}"
        end

        def self.field_name_to_class_str(field_name)
          field_name.split("__")[0].to_s.gsub("_","::")
        end

        def self.slugify_name(tag_name)
          tag_name.parameterize('-')
        end

        def get_relation_collection(klass_id)
          klass_methods = self.methods.grep(/#{klass_id.to_s}/)
          return [] if klass_methods.empty?
          klass_methods.select! {|x| !( x.to_s.match(/_ids/) || x.to_s.match(/=/) ) }
          collection = Array.new
          klass_methods.each {|meth| collection << self.send(meth)}
          collection.flatten.uniq
        end

      protected

        # Sets the slug of the instance by using the value of the highlighted field
        # (if available). If a sibling content instance has the same permalink then a
        # unique one will be generated
        def set_slug
          self._slug = Tag.slugify_name(self.name) if self._slug.blank?

          if self._slug.present?
            self._slug = self.next_unique_slug if self.slug_already_taken?
          end
        end

        # Return the next available unique slug as a string
        def next_unique_slug
          slug        = self._slug.gsub(/-\d*$/, '')
          last_slug   = self.class.where(:_id.ne => self._id, :_slug => /^#{slug}-?\d*?$/i).order_by(:_slug).last._slug
          next_number = last_slug.scan(/-(\d)$/).flatten.first.to_i + 1
          [slug, next_number].join('-')
        end

        def slug_already_taken?
          self.class.where(:_id.ne => self._id, :_slug => self._slug).any?
        end


      end

      module Field

        extend ActiveSupport::Concern

        included do


        end

        def tag_class
          Tag
        end

        def ordered_available_tags
           available_tags
        end

        def available_tags
           Tag.available_tags
        end



      end

      module Target

        extend ActiveSupport::Concern

        module ClassMethods




        def get_localized_name(tag_hash)

          locale = Mongoid::Fields::I18n.locale.to_s

          if !tag_hash['name'].respond_to?(:merge)
            tag_hash['name']
          elsif Mongoid::Fields::I18n.fallbacks?
            tag_hash['name'][Mongoid::Fields::I18n.fallbacks[locale.to_sym].map(&:to_s).find { |loc| !tag_hash['name'][loc].nil? }]
          else
            tag_hash['name'][locale.to_s]
          end
        end

          # Adds a tags field
          #
          # @param [ Class ] klass The class to modify
          # @param [ Hash ] rule It contains the name of the field and if it is required or not
          #
          def apply_tag_set_custom_field(klass, rule)
            name, base_collection_name = rule['name'], "#{rule['name']}_available_tags".to_sym
            inverse_name =Tag.make_inverse_field(klass,name)
            raw_name = "raw_#{name}"


            Tag.has_and_belongs_to_many inverse_name, :class_name => klass.to_s, :inverse_of => raw_name

            klass.has_and_belongs_to_many raw_name, :class_name => "CustomFields::Types::TagSet::Tag", :inverse_of =>  inverse_name

            # other methods
            klass.send(:define_method, name.to_sym) { _get_tags(name) }
            klass.send(:define_method, :"#{name}=") { |value| _set_tags(name, value) }
            klass.class_eval("alias :#{name.singularize}_ids :raw_#{name.singularize}_ids")
            klass.class_eval("alias :#{name.singularize}_ids= :raw_#{name.singularize}_ids=")


            klass.class_eval <<-EOV

              def self.#{base_collection_name}
                self._available_tags('#{name}')
              end

              def self.tag_inverse_relation_#{name}
                "#{inverse_name}"
              end

            EOV

            if rule['required']
              klass.validates_length_of name, :minimum => 1
            end
          end

          # Returns a list of documents groups by select values defined in the custom fields recipe
          #
          # @param [ Class ] klass The class to modify
          # @return [ Array ] An array of hashes (keys: select option and related documents)
          #
          def group_by_tag(name, order_by = nil)
            ids_field = "raw_#{name.to_s.singularize}_ids"
            groups = self.only(ids_field.to_sym).group

            Tag.available_tags.map do |tag|
              group = groups.select { |g| g[ids_field].include?(tag['_id']) }
              list  = group ? group.collect{|g| g['group'][0]} : []

               groups.delete(group) if group

               { :name => get_localized_name(tag), :entries => self._order_tagged_entries(list, order_by) }.with_indifferent_access
            end.tap do |array|
              empty_group = groups.select { |g| g[ids_field].empty? }

              if not empty_group.empty? # orphan entries ?
                empty = { :name => nil, :entries => [] }.with_indifferent_access
                 list  = empty_group.collect{|g| g['group'][0]}
                 empty[:entries] = self._order_tagged_entries(list, order_by)
                 array << empty
               end
            end
          end

          def _available_tags(name)
            Tag.available_tags.map do |tag|
              tag_name = get_localized_name(tag)
              { '_id' => tag['_id'], 'name' => tag_name }
            end.sort_by{ |x| x['name']}
          end

          def _order_tagged_entries(list, order_by = nil)
            return list if order_by.nil?

            column, direction = order_by.flatten

            list = list.sort { |a, b| (a.send(column) && b.send(column)) ? (a.send(column) || 0) <=> (b.send(column) || 0) : 0 }

            direction == 'asc' ? list : list.reverse

            list
          end

        end

        def _tags_ids(name)
          self.send(:"raw_#{name.singularize}_ids")
        end

        #finds tags based on their names, or create it if it doesn't exist
        def _find_tags(names)
          names.collect{|tag_name| Tag.find_or_create_by(name: tag_name)}
        end

        #gets an array of tag names based on the how the object is tagged
        def _get_tags(field_name)
          _tags_ids(field_name).collect { |id| Tag.where(:_id => id).first.name}

        end

        #sets the tags (and makes new ones!) based on the value given.. ?
        def _set_tags(field_name, value)
          if(value.blank?)
            tag_name_array = []
          else
            tag_name_array = value.kind_of?(Array) ? value : value.split(",")
            tag_name_array.map!(&:strip).reject!(&:blank?)
          end
          self.send(:"raw_#{field_name}").clear
          locale = Mongoid::Fields::I18n.locale.to_s

          tag_name_array.each do |tag_name|
            tag = Tag.find_tag_by_name(tag_name, locale)
            self.send(:"raw_#{field_name}") << (tag.nil? ? Tag.create(:name => tag_name) : tag)
          end
        end


      end

    end

  end

end<file_sep>require 'spec_helper'


describe CustomFields::Types::TagSet do

  before(:each) do
    @blog = create_blog
  end

  describe 'a new post' do

    before(:each) do
      @post = @blog.posts.build :title => '<NAME>', :body => 'Lorem ipsum...'
    end

    it 'sets the tags' do
      @post.topics = 'locomotive'
      @post.save
      @post.topic_ids.should include(@locomotive_tag._id)
    end

    it 'adds a new tag to the list of used tags' do
      @post.topics = "cms, ruby"
      @post.topics.should include("ruby","cms")
    end

    it 'removes extra white space from tags' do
      @post.topics = "cms, ruby,   rails   ,  a b c"
      @post.topics.should include("ruby","cms", "rails", "a b c")
    end

    it 'can have no tags' do
      @post.topics = ""
      @post.topics.should == []
    end

    it 'can be set via an array' do
      @post.topics = ['hello', ' world', '<NAME>']
      @post.topics.should include("hello", "<NAME>", "world")
    end

    it 'returns the name of the tag' do
      @post.topics = ""
      @post.topic_ids.append(@locomotive_tag._id)
      @post.save
      @post.topics.should include('locomotive')
    end

    it 'ignores the case of tags' do
      @post.topics = 'LocomOtive'
      @post.topic_ids.should include(@locomotive_tag._id)
    end

    it 'can have a tag field that is not named topics' do
      @post.stuff = 'hello, world'
      @post.stuff.length.should ==  2
    end

    it 'ignores empty tags' do
      @post.topics = 'this,is,,not,blank'
      @post.topic_ids.length.should == 4
    end

  end

  describe 'an existing post' do

    before(:each) do
      @post = @blog.posts.create :title => '<NAME>', :body => 'Lorem ipsum...', :raw_topic_ids => [@beach_tag._id, @castle_tag._id]
      @post = Post.find(@post._id)
    end

    it 'returns the tags' do
      @post.topics.should include("beach", "castle")
    end

    it 'assigns a new tag' do
      @post.topic_ids.append(@lego_tag._id)
      @post.save
      @post = Post.find(@post._id)
      @post.topics.should include("lego")
    end

    it 'create a new tag and assigns it' do
      tag = @blog.posts_custom_fields.detect {|f| f[:label] == "Topics"}.tag_class.create :name => 'new_tag'
      @blog.save
      @post = Post.find(@post._id)
      @post.topics = 'new_tag'
      @post.attributes['raw_topic_ids'].should include(tag._id)
      @post.save
      @post = Post.find(@post._id)
      @post.topics.should include( 'new_tag')
    end

  end

  describe 'an saved post' do

    before(:each) do
      post = @blog.posts.create :title => 'Hello world', :body => 'Lorem ipsum...'
      post.topics = "topic1, topic2"
      post.save
      @post = Post.find(post._id)
    end

    it 'has the correct tags' do
      @post.topics.should include("topic2", "topic1")
      @post.topics.length.should == 2
      @post.class.topics_available_tags.find_all{|x| x['name'] == 'topic1' || x['name'] == 'topic2'}.length.should == 2

    end

  end

  describe 'slugs and inverse' do

    before(:each) do
      post = @blog.posts.create :title => 'Hello world', :body => 'Lorem ipsum...'
      post.topics = "locomotive, Hello World!"
      post.save
      @post = Post.find(post._id)
    end

    it 'gives slugs to tags' do
      @post.raw_topics[0]._slug.should == "locomotive"
      @post.raw_topics[1]._slug.should == "hello-world"

    end

    it 'can find a tag by a slug' do
      CustomFields::Types::TagSet::Tag.find_tag_by_slug("locomotive").should == @post.raw_topics[0]
    end

    it 'can find the class after making the inverse relation' do
      CustomFields::Types::TagSet::Tag.field_name_to_class_str(@post.class.tag_inverse_relation_topics).should == @post.class.to_s
    end

    it 'inverse relations work with Namespaced classes' do
      namespaced_class_str = "HelloWorld::This:Is:A::NamespacedModule"
      CustomFields::Types::TagSet::Tag.field_name_to_class_str(CustomFields::Types::TagSet::Tag.class_str_to_field_name(namespaced_class_str)).should == namespaced_class_str
    end

    it 'can give a collection based on klass id' do
      my_tag = CustomFields::Types::TagSet::Tag.find_tag_by_slug('hello-world')
      my_tag.get_relation_collection(@blog.id).size.should == 1
    end

  end



  describe 'group_by' do

    before(:each) do
      p1 = @blog.posts.create :title => 'Hello world 1(sun, beach)', :body => 'Lorem ipsum...'
      p2 = @blog.posts.create :title => 'Hello world 2(castle, lego)', :body => 'Lorem ipsum...'
      p3 = @blog.posts.create :title => 'Hello world 3 (locomotive)', :body => 'Lorem ipsum...'
      p4 = @blog.posts.create :title => 'Hello world 4 (beach, castle, locomotive)', :body => 'Lorem ipsum...'
      p5 = @blog.posts.create :title => 'Hello world 5 (castle)', :body => 'Lorem ipsum...'
      p6 = @blog.posts.create :title => 'Hello world (_none_)', :body => 'Lorem ipsum...'

      p1.raw_topics.concat([@sun_tag, @beach_tag])
      p1.save

      p2.raw_topics.concat([@castle_tag, @lego_tag])
      p2.save

      p3.raw_topics << @locomotive_tag
      p3.save

      p4.raw_topics.concat([@locomotive_tag, @beach_tag, @castle_tag])
      p4.save

      p5.raw_topics.concat([@castle_tag])
      p5.save

      klass = @blog.klass_with_custom_fields(:posts)
      @groups = klass.group_by_tag(:topics)
    end

    it 'is an non empty array' do
      @groups.class.should == Array
      @groups.size.should == 6
    end

    it 'is an array of hashes composed of a name' do
      @groups.map { |g| g[:name].to_s }.should == ["beach", "castle", "lego", "locomotive", "sun", ""]
    end

    it 'is an array of hashes composed of a list of documents' do
      @groups[0][:entries].size.should == 2
      @groups[1][:entries].size.should == 3
      @groups[2][:entries].size.should == 1
      @groups[3][:entries].size.should == 2
      @groups[4][:entries].size.should == 1
      @groups[5][:entries].size.should == 1
    end

    it 'can be accessed from the parent document' do
      blog = Blog.find(@blog._id)
      blog.posts.group_by_tag(:topics).class.should == Array
    end

  end


  def create_blog
    CustomFields::Types::TagSet::Tag.delete_all
    Blog.new(:name => 'My personal blog').tap do |blog|
      blog.posts_custom_fields.build :label => 'stuff', :type => 'tag_set'
      field = blog.posts_custom_fields.build :label => 'Topics', :type => 'tag_set'

      Mongoid::Fields::I18n.locale = :en

      @sun_tag          = field.tag_class.create(name: 'sun')
      @beach_tag        = field.tag_class.create(name: 'beach')
      @lego_tag         = field.tag_class.create(name: 'lego')
      @locomotive_tag   = field.tag_class.create(name: 'locomotive' )
      @castle_tag       = field.tag_class.create(name: 'castle')

      blog.save & blog.reload
    end
  end

end<file_sep>require 'spec_helper'

describe CustomFields::Types::TagSet do

  before(:each) do
    @blog = build_blog
    @post = @blog.posts.build :title => 'Hello world', :body => 'Lorem ipsum...'
  end

  it 'is not considered as a relationship field type' do
    @blog.posts_custom_fields.first.is_relationship?.should be_false
  end

  it 'stores the list of tags' do
    @field.respond_to?(:available_tags).should be_true
  end

  it 'sets a value' do
    @post.topics = 'Test'
    @post.topics.should == ['Test']
  end

  describe 'validation' do

    [nil, ''].each do |value|
      it "should not be valid if the value is #{value.inspect}" do
        @post.topics = value
        @post.valid?.should be_false
        @post.errors[:topics].should_not be_blank
      end
    end

  end


  context '#localize' do

    before(:each) do
      field = @blog.posts_custom_fields.build :label => 'Taxonomy', :type => 'tag_set', :localized => true
      Mongoid::Fields::I18n.locale = :en
      @option_1 = field.tag_class.create :name => 'Item #1 in English'
      @option_2 = field.tag_class.create :name => 'Item #2 in English'
      Mongoid::Fields::I18n.locale = :fr
      @option_1.name = 'Item #1 in French'
      @option_2.name = 'Item #2 in French'
      @option_1.save
      @option_2.save

      field.valid?
      Mongoid::Fields::I18n.locale = :en
      @blog.bump_custom_fields_version(:posts)
    end

    it 'serializes / deserializes' do
      post = @blog.posts.build :taxonomy => 'Item #1 in English'
      post.taxonomy.should == ['Item #1 in English']
    end

    it 'serializes / deserializes in a different locale' do
      post = @blog.posts.build :taxonomy => 'Item #1 in English'
      Mongoid::Fields::I18n.locale = :fr
      post.taxonomy = 'Item #2 in French'
      post.taxonomy_ids.should include(@option_2._id)
      post.taxonomy_ids.length.should == 1
      post.taxonomy.should == [@option_2.name]
    end

  end

  def build_blog
    Blog.new(:name => 'My personal blog').tap do |blog|
      @field = blog.posts_custom_fields.build :label => 'Topics', :type => 'tag_set', :required => true
      @field.tag_class.create :name => 'Test'
      @field.valid?
    end
  end

end<file_sep>require 'spec_helper'

describe 'Performance' do

  occurences = 1000

  available_tags = %w(some tags picked at random)

  context 'with custom fields' do

    before(:each) do
      @blog = create_blog
      create_posts(@blog, occurences)
      @blog = Blog.find(@blog._id)
    end

    it "retrieves #{occurences} posts" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        @blog.posts.all.collect(&:title)
      end
    end

    it "retrieves #{occurences} posts and trigger methods" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        @blog.posts.all.each do |entry|
          entry.main_author = 'john'
          entry.location = 'chicago'
        end
      end
    end

  end

  context 'without custom fields' do

    before(:each) do
      @blog = create_blog(false)
      create_posts(@blog, occurences)
      @blog = Blog.find(@blog._id)
    end

    it "retrieves #{occurences} posts" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        @blog.posts.all.collect(&:title)
      end
    end

    it "retrieves #{occurences} posts and trigger methods" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        @blog.posts.all.each do |entry|
          entry.title = 'yeaah'
          entry.body = 'a test'
        end
      end
    end

  end

  context 'with custom fields including tags' do

    before(:each) do
      @blog = create_blog_with_tags
      create_posts_with_tags(@blog, occurences, available_tags)
      @blog = Blog.find(@blog._id)
    end

    it "retrieves #{occurences} posts" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        @blog.posts.all.collect(&:title)
      end
    end

    it "retrieves #{occurences} posts and trigger methods" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        @blog.posts.all.each do |entry|
          entry.main_author = 'john'
          entry.location = 'chicago'
        end
      end
    end

    it "retrieves #{occurences} posts via tags" do
      MyBenchmark.measure("retrieving #{occurences} posts") do
        groups = @blog.posts.group_by_tag(:topics)
        groups.class.should == Array
      end
    end


  end


  def create_blog(custom_fields = true)
    Blog.new(:name => 'My personal blog').tap do |blog|
      if custom_fields
        blog.posts_custom_fields.build :label => 'Main Author', :type => 'string'
        blog.posts_custom_fields.build :label => 'Location',    :type => 'string'
        blog.posts_custom_fields.build :label => 'Posted at',   :type => 'date'
        blog.posts_custom_fields.build :label => 'Published',   :type => 'boolean'
      end
      blog.save
    end
  end

  def create_posts(blog, n)
    n.times do |i|
      blog.posts.create :title => "Hello world #{i}", :body => 'Lorem ipsum...'
    end
  end

  def create_blog_with_tags(custom_fields = true)
    Blog.new(:name => 'My personal blog').tap do |blog|
      if custom_fields
        blog.posts_custom_fields.build :label => 'Main Author', :type => 'string'
        blog.posts_custom_fields.build :label => 'Location',    :type => 'string'
        blog.posts_custom_fields.build :label => 'Posted at',   :type => 'date'
        blog.posts_custom_fields.build :label => 'Published',   :type => 'boolean'
        blog.posts_custom_fields.build :label => 'Topics',   :type => 'tag_set'
      end
      blog.save
    end
  end

  def create_posts_with_tags(blog, n, available_tags)
    number_available_tags = available_tags.length;

    n.times do |i|
       number_of_tags_to_use = rand(number_available_tags)
       instance_tag_list = []

       for i in 0..number_of_tags_to_use
         instance_tag_list << available_tags[rand(number_available_tags)]
       end
       blog.posts.create :title => "Hello world #{i}", :body => 'Lorem ipsum...', :topics => instance_tag_list
    end
  end

end