Posts tagged with acts-as-taggable-on

Avatar-eric-london
Created by Eric.London on 2012-06-27
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
I recently exported a few data sources to CSV and thought it would interesting to migrate the data into a Ruby on Rails model and use Active Admin to create a slick admin interface. Here is the skeletal code I used.

Part 1, Rails setup


# create directory for Rails project
$ mkdir csvimport

# create new rvm gemset
$ echo "rvm use --create ruby-1.9.2@csvimport" > csvimport/.rvmrc

# enter directory
$ cd csvimport

# add rails gem
$ gem install rails

# create new Rails app
$ rails new .

# git integration
# I generally commit after every action, but I'll leave that out per sake of clutter
$ git init
$ git add .
$ git commit -am "new rails project"

# setup database
$ rake db:migrate


Part 2, Model creation


# add acts as taggable on gem, for tagging (clearly optional)
# edit file: Gemfile
+gem 'acts-as-taggable-on'

# execute bundle to install gems
$ bundle

# create Post model
$ rails generate model Post title:string content:text

# add taggable property to Post model & make mass-assignable
# file: app/models/post.rb
 class Post < ActiveRecord::Base
-  attr_accessible :content, :title
+  attr_accessible :content, :title, :tag_list
+  acts_as_taggable_on :tags
 end

# run acts as taggable migration
$ rails generate acts_as_taggable_on:migration

# setup database
$ rake db:migrate


Part 3, Active Admin integration


# add active admin gem
# edit file: Gemfile
+# active admin
+gem 'activeadmin'
+gem 'sass-rails', '~> 3.2.3'
+gem 'meta_search'
+
 # Gems used only for assets and not required
 # in production environments by default.
 group :assets do
-  gem 'sass-rails',   '~> 3.2.3'
+  #gem 'sass-rails',   '~> 3.2.3'

# execute bundle to install gems
$ bundle

# run active admin generator, and setup database
$ rails generate active_admin:install
$ rake db:migrate

# register Post model with active admin
$ rails generate active_admin:resource Post


Part 4, Active Admin + acts-as-taggable-on integration

In this section, I (optionally) show how you might alter the active admin post file to integrate with acts-as-taggable-on for tagging.


# edit file: app/admin/post.rb:
ActiveAdmin.register Post do

  # filters
  filter :title
  filter :content
  filter :created_at
  filter :updated_at
  filter :taggings_tag_name, :as => :check_boxes, :collection => proc { Post.tag_counts.map{|t| t.name} }

  # column list
  index do
    column :id
    column :title
    column :content
    column :tag_list
    column :created_at
    column :updated_at
    default_actions
  end

  # form
  form do |f|
    f.inputs "Post" do
      f.input :title
      f.input :content
      f.input :tag_list
    end
    f.buttons
  end

end


Next, I executed some code to set the root (home) route to the active admin dashboard, removed the default public index.html file, and updated the admin user.


# set root route to active admin dashboard
# file: config/routes.rb
+  root :to => 'admin/dashboard#index'

# clean up, remove public facing index.html file
$ rm public/index.html

# update default/admin user, or create a new user via console
$ rails c
> u = AdminUser.find(1)
> u.email = 'youremailaddress'
> u.password = 'yourpassword'
> u.save
> exit


Part 5, Rake task to import CSV data


# create a rake task to import the CSV data
# new file: lib/tasks/import_csv_data.rake

namespace :csvimport do

  desc "Import CSV Data."
  task :import_csv_data => :environment do

    require 'csv'

    csv_file_path = '/path/to/your/file.csv'

    CSV.foreach(csv_file_path) do |row|

      p = Post.create!({
          :title => row[0],
          :content => row[1],
          :tag_list => row[2].split('|'),
        }
      )

    end

  end

end


Lastly, I ran the rake task to import the CSV data into the Rails model. And then started the WEBrick server.


# run import task
rake csvimport:import_csv_data

# start server
rails s


Here is a screenshot of the CSV data file I imported, and below, the active admin interface that was created:

Test-data-csv

Active admin csv
Avatar-eric-london
Created by Eric.London on 2012-04-10
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
In this article I'll show how to setup a Rails project with faceted solr searching integration. This code uses the following: sunspot gem for Solr integration, and acts-as-taggable-on for tagging and search facets.

RVM/Rails Setup

$ mkdir solrfacets

# create rvm gemset
$ echo "rvm use --create ruby-1.9.2@solrfacets" > solrfacets/.rvmrc

$ cd solrfacets

# install rails
$ gem install rails

# create new rails project
$ rails new .

# version control
$ git init
$ git add .
$ git commit -am "new rails project"


Add gems

# file: Gemfile, added:
gem 'acts-as-taggable-on'
gem 'sunspot_rails'
gem 'sunspot_solr', :groups => [:development, :test]

# installing gems
$ bundle


Create default scaffolding for a Post model

$ rails generate scaffold Post title:string content:text


Add tags property to Post model

# file: app/models/post.rb

 class Post < ActiveRecord::Base
   attr_accessible :content, :title
+  acts_as_taggable_on :tags
 end


Run acts-as-taggable-on migration

$ rails generate acts_as_taggable_on:migration


Setup/create database

$ rake db:migrate


Part 2, Random Data

The model is now setup to create Posts with a title, content, and array of tags. For demonstration purposes, I decided to create a rake task to populate the content attribute with lorem ipsum text, and the tags with random words from /usr/share/dict/words.

Modified the Post model to enable :tag_list as mass assignable

# file: app/models/post.rb

 class Post < ActiveRecord::Base
-  attr_accessible :content, :title
+  attr_accessible :content, :title, :tag_list
   acts_as_taggable_on :tags
 end


Added lorem gem

# file: Gemfile
gem 'lorem', :groups => [:development]

# installing
$ bundle


Created a ruby rake script to create 20 Posts with 20 random tag words

# file: lib/tasks/create_random_posts_and_tags.rake

namespace :db do
  desc "Create random posts and tags."
  task :create_random_posts_and_tags => :environment do
    
    # count the number of lines in the dictionary
    dict_word_count = `wc -l /usr/share/dict/words | awk '{print $1}'`.to_i
    
    # get 100 random words for the facets
    facet_words = 100.times.map{ `sed $(echo #{Random.rand(dict_word_count)})"q;d" /usr/share/dict/words`.strip! }
    
    # create 20 random posts
    (1..20).each do |i|

      post = Post.create!({
        :title => "Post #{i}",
        :content => Lorem::Base.new('paragraphs', 1).output,
        :tag_list => 20.times.map{ facet_words[rand(facet_words.size)] },
      })
      
    end
    
  end
end


Executed rake task to create posts

$ rake db:create_random_posts_and_tags


Part 3, Solr Sunspot

Generate default configuration

$ rails generate sunspot_rails:install


Add code to index Post data. In this code, I added ":stored => true" to each property to: 1. avoid querying Active Record on the search results page; and 2. to enable matches highlighting.

# file: app/models/post.rb

 class Post < ActiveRecord::Base
   attr_accessible :content, :title, :tag_list
   acts_as_taggable_on :tags
+
+  searchable :auto_index => true, :auto_remove => true do
+    string :title, :stored => true
+    text :content, :stored => true
+    string :tag_list, :multiple => true, :stored => true
+  end
+
 end


Setup Solr development server via Jetty

# start solr
$ rake sunspot:solr:start 

# index data
$ rake sunspot:solr:reindex


At this point, you should be able to browse and query the solr search results and verify the structure of the indexed data. Example URL: http://localhost:8982/solr/select/?q=*:*
Querying solr directly

Add a new Search controller

$ rails generate controller Search search


Revised search controller to be named route

# file: config/routes.rb

-  get "search/search" 
+  get 'search' => 'search#search', :as => 'search'


Define the search controller method. I set the controller to pass 2 instance variables to the view: @search and @hits. @hits contains the stored values, allowing us to query solr directly, instead of Active Record.

# file: app/controllers/search_controller.rb

class SearchController < ApplicationController
  def search

    # only search if keyword has been entered
    if params[:keywords].nil? || params[:keywords].empty?
      @hits = []
    else
      @search = Post.search do
        fulltext params[:keywords] do
          highlight :content
        end
        facet :tag_list
        paginate :per_page => 10
        
        # tags, AND'd        
        if params[:tag].present?
          all_of do
            params[:tag].each do |tag|
              with(:tag_list, tag)
            end
          end
        end
        
      end
      @hits = @search.hits
      
    end    
  end
end


Define the search view. This code contains the following sections: search form, search results (@hits with matches highlighting), and facets generation. I set the facets as an array, to allow the user to select multiple.

# file: app/views/search/search.html.erb

<h1>Search#search</h1>

<!-- FORM: -->
<%= form_tag search_path, :method => :get do %>
  <%= text_field_tag :keywords, params[:keywords] %>
  <%= submit_tag "Search", :name => nil %>
<% end %>

<!-- SEARCH RESULTS: -->
<% if @hits.any? %>
  <h2>Search Results</h2>
  <ul>
    <% @hits.each do |hit| %>
      <li>
        <%= link_to hit.stored(:title), post_path(hit.primary_key) %><br/>
        <% hit.highlights(:content).each do |highlight| %>          
          <%= highlight.format { |word| "*#{word}*" } %>
        <% end %>
      </li>
    <% end %>  
  </ul>
<% end %>

<!-- FACETS HTML: -->
<%
facets_html = ''
if not @search.nil?
  
  # check for existing tags in query string
  existing_tag_facets = []
  if params[:tag].present?
    existing_tag_facets = params[:tag]
  end

  facet_links_off = ''
  facet_links_on = ''

  @search.facet(:tag_list).rows.each_with_index do |facet, index|
    break if index == 10;
    
    # check if facet is selected
    if (params[:tag].kind_of?(Array) and params[:tag].include? facet.value)
      tag_facets = existing_tag_facets - [facet.value]      
      facet_links_on << "<li>#{link_to facet.value, :keywords => params[:keywords], :tag => tag_facets} (-)</li>"
    elsif @hits.size > 1
      tag_facets = existing_tag_facets + [facet.value]
      facet_links_off << "<li>#{link_to facet.value, :keywords => params[:keywords], :tag => tag_facets} (#{facet.count})</li>"
    end

  end

  facets_html << "<strong>Filter by tags</strong>"
  if facet_links_on.size > 0
    facets_html << "<ul class='search_facets_on'>#{facet_links_on}</ul>"
  end
  if facet_links_off.size > 0
    facets_html << "<ul class='search_facets_off'>#{facet_links_off}</ul>"
  end

end
%>
<%= raw facets_html %>


Browsing to http://localhost:3000/search now shows the search form. I entered "lorem" to get the following result. Note the asterisks around keyword "lorem" in the results. The tag facets are shown below with their associated result count.
solr search results with facets

By clicking on two tags, the facet counts and associated results decrease. The facet links can also be unselected. Great.
solr search results with facets selected