Continuing on from [Part 2]({{< relref "/dev/rails-blog-part-2.md" >}}) of our Rails Blog tutorial, now we're going to be adding in a tag system so users can tag posts and check out what posts are available under a particular tag. To start with, run `rails g model Tag name:string` to generate a model for the tags then run `rails g migration posts_tags` and find the migration this generated (typically db/migrate/YYYYMMDDHHMMSS_create_tags.rb with a different time stamp) and add the following inside the `change` method: create_table :posts_tags, :id => false do |t| t.integer :post_id t.integer :tag_id end This will create a new table without a primary key (`:id => false`) with two attributes, both being foreign keys and leading to the post and tag tables respectively. Run `rake db:migrate` to add these changes to the database. Next you'll need to open up app/models/tag.rb and add: has_and_belongs_to_many :posts Then open up app/models/post.rb and add: has_and_belongs_to_many :tags This creates the relationship that we want between the tags and posts; a many-to-many relationship where each tag can have multiple posts related to it and each post can contain multiple tags. Now with a lot of blogging platforms, you enter a list of comma-separated values, each value becoming a tag. We're going to try and implement something similar in this project. Open up app/views/posts/new.html.erb and before the hidden field we added with the user ID, add the following code:
Tags: <% @post.tags.each do |t| %> <%= link_to t.name, t %><% if @post.tags.last != t %>, <% end %> <% end %>
Now open up app/controllers/posts_controller.rb and after the `post_params` function, we'll add a couple more private functions to help reduce the amount of code we need to duplicate afterwards: def tag_params params.require(:post).permit(:tag_ids) end def ready_tags tags = post_params[:tag_ids].split(/,\s*/) tags_ready = [] tags.each do |tag| temp = Tag.find_by name: tag if temp == nil temp = Tag.create(name: tag) end tags_ready.push(temp) end tags_ready end def destroy_orphaned_tags(tags, limit) tags.each do |tag| if (tag.posts.count <= limit) tag.destroy end end end `tag_params` allows us to use the `:tag_ids` variable passed through from app/views/posts/_form.html.erb. The second function (`ready_tags`) readies the tag_ids from app/views/posts/_form.html.erb to be used by the post in the `create` and `update` methods while `destroy_orphaned_tags` deletes tags that have are not attached to any post (makes more sense in context of the controller). Next we'll need to alter the `create`, `update` and `destroy` methods to look like below: # POST /posts def create @post = Post.new(post_params) @post.tags = ready_tags if @post.save redirect_to @post, notice: 'Post was successfully created.' else destroy_orphaned_tags(@post.tags, 0) render :new end end # PATCH/PUT /posts/1 def update destroy_orphaned_tags(@post.tags, 1) @post.tags = ready_tags if author_exists = User.where(:id => @post.user_id).first if current_user == author_exists || current_user.try(:admin?) if @post.update(post_params) redirect_to @post, notice: 'Post was successfully updated.' else destroy_orphaned_tags(@post.tags, 0) render :edit end else render :show end else if current_user.try(:admin?) if @post.update(post_params) redirect_to @post, notice: 'Post was successfully updated.' else destroy_orphaned_tags(@post.tags, 0) render :edit end else render :show end end end # DELETE /posts/1 def destroy if author_exists = User.where(:id => @post.user_id).first if current_user == author_exists || current_user.try(:admin?) destroy_orphaned_tags(@post.tags, 1) @post.destroy redirect_to posts_url, notice: 'Post was successfully destroyed.' else render :show end else if current_user.try(:admin?) destroy_orphaned_tags(@post.tags, 1) @post.destroy redirect_to posts_url, notice: 'Post was successfully destroyed.' else render :show end end end As you can see, we utilise the new methods available to add tags to post, update tags on existing posts and delete unneeded tags when posts are deleted. Next we'll edit some more views, firstly adding the following code near the bottom of app/views/posts/index.html.erb<%= link_to 'Tags', tags_path %>
Then create a file called app/views/tags/index.html.erb and add the following code:Tag | ||
---|---|---|
<%= tag.name %> | <%= link_to 'Show', tag %> | <% if current_user.try(:admin?) %><%= link_to 'Destroy', tag, method: :delete, data: { confirm: 'Are you sure?' } %> | <% end %>
<%= link_to 'Index', posts_path %>
Next create app/views/tags/show.html.erb and use the following code:<%= link_to 'Destroy', @tag, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>Post | ||
---|---|---|
<%= post.title %> | <%= link_to 'Show', post %> | <% if (current_user == post.user && post.user != nil) || current_user.try(:admin?) %><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %> | <% end %>
<%= link_to 'Users List', users_path %>
These files create a way for users to see all tags, see tags connected to posts and delete either tags or particular posts connected to a tag (with the right authorisation levels). Next run `rails g controller tags` and open up the new controller file located at app/controllers/tags_controller.rb and add the following code inside the class: before_filter :authenticate_user!, :only => [:destroy] # GET /tags def index @tags = Tag.all end # GET /tags/1 def show @tag = Tag.find(params[:id]) end # DELETE /tags/1 def destroy @tag = Tag.find(params[:id]) if current_user.try(:admin?) @tag.destroy redirect_to tags_url, notice: 'Tag was successfully destroyed.' else redirect_to tags_url, notice: 'Tag unsuccessfully destroyed.' end end This creates the necessary framework to show the index of tags, show a particular tag or delete a particular tag as well as requiring authentication before tags can be destroyed.. To make all these resources accessible, add `resources :tags` inside your config/routes.rb file.