Using slugs in polymorphic_path - ruby-on-rails

Is there a way I can use parameters in a polymorphic_path, to pass in a slug?
For instance, I have the following routes
routes.rb
MyApp::Application.routes.draw do
match "movies/:slug" => 'movies#show', :as=>:movie
match "series/:slug" => 'series#show', :as=>:series
end
And I have the following models:
Movie.rb
class Movie < ActiveRecord::Base
has_many :cast_members, :as=>:media_item
end
Series.rb
class Series < ActiveRecord::Base
has_many :cast_members, :as=>:media_item
end
CastMember.rb
class CastMember < ActiveRecord::Base
belongs_to :media_item, :polymorphic=>true
end
This works great, and I can reference my movie from the cast member, and vice-versa, just like a normal has_many/belongs_to relationship.
I can also do this from within my cast_member view:
*cast_members/show.html.erb*
link_to (#cast_member.movie.title, movie_path(#cast_member.movie.slug))
which returns "movie/movie-title"
and I can do
*cast_members/show.html.erb*
link_to (#cast_member.movie.title, polymorphic_path(#cast_member.media_item))
but this returns "/movies/24"
I've tried passing a slug as an item to polymorphic_path in different ways, like
link_to (#cast_member.movie.title, polymorphic_path(#cast_member.media_item, #cast_member.media_item.slug))
link_to (#cast_member.movie.title, polymorphic_path(#cast_member.media_item, :slug=>#cast_member.media_item.slug))
link_to ([#cast_member.movie.title, polymorphic_path(#cast_member.media_item, #cast_member.media_item.slug]))
but these all return errors or the path with the id.
How can I make the polymorphic_path use the movie.slug instead of the id?

I switched over to using friendly_id to generate slugs. It magically handles all the slug<->id conversions magically in the background, and sosolves the issue.
I do think that rails should have a baked-in way to do this, the same way you can pass a slug into the default *_path methods.

I solved this by using Rails' built-in path helpers instead of polymorphic_path. I really wanted to use that method, since it required use of the model's ID, I couldn't.
In my app, I have a lot of models that are "slugable", so it made sense to include a #to_path method in the slugable mixin.
# app/models/concerns/slugable.rb
module Slugable
extend ActiveSupport::Concern
included do
validates :slug, presence: true, uniqueness: {case_sensitive: false}
end
def to_path
path_method = "#{ActiveModel::Naming.singular_route_key(self)}_path"
Rails.application.routes.url_helpers.send(path_method, slug)
end
def slug=(value)
self[:slug] = value.blank? ? nil : value.parameterize
end
end
then in the templates:
<%= link_to my_slugable_model.name, my_slugable_model.to_path %>
If you have nested resources in your routes, then you'll need to adjust the code for that resource.
something like this (untested):
def to path(my_has_many_model_instance)
class_path = self.class.to_s.underscore
has_many_class_path = my_has_many_model_instance.class.to_s.underscore
path_method = "#{self_path}_#{has_many_class_path}_path"
Rails.application.routes.url_helpers.send(path_method, slug, my_has_many_model)
end
Good luck!

Related

Rails 4 Not Updating Nested Attributes Via JSON

I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.
Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.
Here's the json in the AngularJS front-end for the candidate:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}
Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}
The candidate_controller.rb contains a simple update:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def update
respond_with Candidate.update(params[:id], candidate_params)
end
private
def candidate_params
params.require(:candidate).permit(:nickname,
works_attributes: [:id, :title, :description])
end
end
The candidate.rb model includes the following code defining the has_many relationship with the works model:
class Candidate < ActiveRecord::Base
## Model Relationships
belongs_to :users
has_many :works, :dependent => :destroy
## Nested model attributes
accepts_nested_attributes_for :works, allow_destroy: true
## Validations
validates_presence_of :nickname
validates_uniqueness_of :user_id
end
And finally, the works.rb model defines the other side of the has_many relationship:
class Work < ActiveRecord::Base
belongs_to :candidate
end
I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.
Thanks!
I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:
class CandidatesController < ApplicationController
respond_to :json
nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
format: :json
# ...
end
Refer to this issue in Rails to see if/when they fix this problem.
Update 10/17
Pending a PR merge here: rails/rails#19254.
I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:
"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."
Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
wrap_parameters include: [:id, :nickname, :works_attributes]
...
Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails
You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:
module ActionController
module ParamsWrapper
Options.class_eval do
def include
return super if #include_set
m = model
synchronize do
return super if #include_set
#include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
self.include = m.attribute_names + nested_attributes_names_array_of(m)
end
end
end
end
private
# added method. by default code was equivalent to this equaling to []
def nested_attributes_names_array_of model
model.nested_attributes_options.keys.map { |nested_attribute_name|
nested_attribute_name.to_s + '_attributes'
}
end
end
end
end

how to index associated models using thinkingtank and indextank

We are using thinkingtank gem and having trouble indexing model associations, even simple ones. For example, a profile belongs to an institution, which has a name – we would like to do something like:
class Profile < ActiveRecord::Base
#model associations
define_index do
indexes institution(:name), :as => :institution_name
end
end
but that doesn't work. This must be very simple – what am I doing wrong?
a possible solution to this issue would be adding a method returning the element to index. For the profile.institution.name case:
# profile.rb
# ...
belongs_to :institution
# ...
define_index do
indexes institution_name
end
def institution_name
self.institution.name
end
# ...
Also the ", :as => ..." syntax is not supported on thinkingtank.
I would also recommend giving a try to Tanker: https://github.com/kidpollo/tanker
Regards.
Adrian

Creating url slugs for tags with acts_as_taggable_on

I would like to create url slugs for tags managed by the acts_as_taggable_on gem. For instance instead of urls like http://myapp.com/tags/5, I would like to have http://myapp.com/tags/my-tag (where 'my tag' is the tag's unique name).
In models that I create myself I usually do this by overriding the model's to_param method, and creating a "slug" field in the model to save the result of the new to_param method. I tried doing this with the Tag model of ActsAsTaggableOn, but it is not working.
I can otherwise override things in the tag.rb class of ActsAsTaggableOn as follows:
# Overwrite tag class
ActsAsTaggableOn::Tag.class_eval do
def name
n = read_attribute(:name).split
n.each {|word| word.capitalize!}.join(" ")
end
end
However, if I try to override the to_param method in that same block with a method definition like:
def to_param
name.parameterize
end
Rails still generates and responds to routes with integer IDs rather than the parameterized name. In fact in the console if I try something like
ActsAsTaggableOn::Tag.find(1).to_param
The integer ID is returned, rather than the result of the overridden to_param method.
I'd rather not fork the gem and customize it if there is any way I can do it with my own application code. Thanks.
I'm using the friendly_id ( https://github.com/norman/friendly_id ) gem to manage slugs. My method to create slugs for my tags is similar to yours, but a lit bit simpler.
I've just created the initializer act_as_taggable_on.rb with the following code:
# act_as_taggable_on.rb
ActsAsTaggableOn::Tag.class_eval do
has_friendly_id :name,
:use_slug => true,
:approximate_ascii => true,
:reserved_words => ['show', 'edit', 'create', 'update', 'destroy']
end
And then:
#user = User.new :name => "Jamie Forrest"
#user.tag_list = "This is awesome!, I'm a ruby programmer"
#user.save
And voilá:
ActsAsTaggableOn::Tag.find('this-is-awesome') #=> #<Tag id: 1, name: "This is awesome!">
ActsAsTaggableOn::Tag.find('im-a-ruby-programmer') #=> #<Tag id: 2, name: "I'm a ruby programmer">
Hope this help...
#vitork's code is a good start but doesn't work for newer versions of friendly_id and acts_as_taggable. Here's an updated initializer:
ActsAsTaggableOn::Tag.class_eval do
extend FriendlyId
friendly_id :name,
:use => :slugged,
:slug_column => :permalink,
:reserved_words => ['show', 'edit', 'create', 'update', 'destroy']
end
My db column is called permalink, you can use slugged if you prefer. Btw, I'm using the following:
friendly_id (4.0.5)
acts-as-taggable-on (2.2.2)
Thanks Vitork for the initial code!
To make this work with latest version (Rails 4.x, friendly_id 5.x) here are the steps you should follow:
Create migration to add slug to tags table
# rails generate migration add_slugs_to_tags
class AddSlugToTags < ActiveRecord::Migration
def change
add_column :tags, :slug, :string
add_index :tags, :slug
end
end
You can rename the :slug column - you should specify the column name in the initializer if you change it. Don't forget to run the migration rake db:migrate.
Create an initializer for ActsAsTaggableOn
# config/initializers/acts_as_taggable_on.rb
ActsAsTaggableOn::Tag.class_eval do
extend FriendlyId
friendly_id :name, use: :slugged
end
When searching for tags you have to use ActsAsTaggableOn::Tag.friendly.find 'tag-name' or add :finders to friendly_id :use call to use find directly on the model. Read more in friendly_id guides.
Actually the answer is much simplier and you dont need to use friendly_id or any other unnecessary extension.
сonfig/initializers/act_as_taggable_on.rb
ActsAsTaggableOn::Tag.class_eval do
before_save { |tag| tag.slug = name.parameterize if name_changed? }
def to_param
slug
end
end
Add a slug column if you need to, otherwise skip before_save callback.
Then in the view, instead of iterating like
article.tag_list.each do |tag|..
you'll iterate like this
article.tags.each
because tag_list gives you only strings, whereas with tags u have real tags instances.
And at least in the controller
if params[:tag]
tag = ActsAsTaggableOn::Tag.find_by_slug(params[:tag])
#articles = Article.moderated.includes(:user).tagged_with(tag)
end
There is another way.
Create a controller for the tags with single action:
rails g controller tags index
In routes.rb change the generated route to:
get 'tags/:tag' => 'tags#index', as: :tag
In tags_controller.rb add this code:
def index
#tag = params[:tag]
#entries = Entry.tagged_with(#tag)
end
where Entry is a model name.
Now you able to get all entries with nice urls like example.com/tags/animals
Usage in views:
- #entry.tags.each do |tag|
= link_to tag, tag_path(tag.name)

Rails 3 - polymorphic_path - How to Create One given a table

i have table AuditLog with fields including: audited_id | audited_type
That results in data like:
108 | Photo
303 | Comment
What I want to do is create a link to the item, so for the example above:
here is the photo
I'm trying to use a polymorphic_path but am getting an error: "undefined method `model_name' for Fixnum:Class"
When using:
<%= link_to 'Here she is', polymorphic_path([audited_id, audited_type]) %>
Ideas? Thanks
Updating with code based on the answer by Luke below:
class NewsFeed < ActiveRecord::Base
default_scope :order => 'news_feeds.created_at DESC'
belongs_to :feeded, :polymorphic => true
end
class Note < ActiveRecord::Base
has_many :newsfeed, :as => :feeded
end
In the partial which is being passed the local storyitem:
<%= link_to 'Here she is', polymorphic_path(storyitem.feeded) %>
The DB migration file, contains the following line for CreateNewsFeeds
t.references :feeded, :polymorphic => true
You should have a method #auditable (or whatever your polymorphic association is called) on AuditLog objects. If you pass the result of that method to polymorphic_path it will return the correct path for you.
Update:
Assuming you have the following associations (or are using acts_as_auditable or something that sets up the relationships for you):
class AuditLog
belongs_to :auditable, :polymorphic => true
end
class AuditedObject
has_many :audits, :as => :auditable
end
You'll be able to call auditable on any instance of AuditLog, and it will return the associated audited object. So you can call
<%= link_to 'Here she is', polymorphic_path(audit_log.auditable) %>
to get a link to the audited object.
So, anywhere that you have a polymorphic association in a class, there is an instance method setup with the name of that association that will return the associated object.
Gosh, I'm hoping that makes sense. Let me know if you need me to clarify it further.
The problem with polymorphic_path it needs an object, so you first need to fetch the object from the database.
Depending on your use case this can be a big performance problem.
In case of a log viewer, where you have a list of for example 100 entries,
and just want to show links to the entires, you will fetch 100 objects, just to get their path.
I had a similar problem, my solution was to hack a method to construct the path from the class name and id:
class AuditLog
...
def audited_path
"/#{self.audited_type.tableize}/#{self.audited_id}"
end
The method will return for example "/photos/302". But of course it will work only for quite simple routings.

Update owner tags via form

I would like to uniquely use owner tags in my app. My problem is that when I create / update a post via a form I only have f.text_field :tag_list which only updates the tags for the post but has no owner. If I use f.text_field :all_tags_list it doesn't know the attribute on create / update. I could add in my controller:
User.find(:first).tag( #post, :with => params[:post][:tag_list], :on => :tags )
but then I have duplicate tags, for post and for the owner tags. How can I just work with owner tags?
The answer proposed by customersure (tsdbrown on SO) on https://github.com/mbleigh/acts-as-taggable-on/issues/111 works for me
# In a taggable model:
before_save :set_tag_owner
def set_tag_owner
# Set the owner of some tags based on the current tag_list
set_owner_tag_list_on(account, :tags, self.tag_list)
# Clear the list so we don't get duplicate taggings
self.tag_list = nil
end
# In the view:
<%= f.text_field :tag_list, :value => #obj.all_tags_list %>
I used an observer to solve this. Something like:
in /app/models/tagging_observer.rb
class TaggingObserver < ActiveRecord::Observer
observe ActsAsTaggableOn::Tagging
def before_save(tagging)
tagging.tagger = tagging.taggable.user if (tagging.taggable.respond_to?(:user) and tagging.tagger != tagging.taggable.user)
end
end
Don't forget to declare your observer in application.rb
config.active_record.observers = :tagging_observer
Late to the party, but I found guillaume06's solution worked well, and I added some additional functionality to it:
What this will enable: You will be able to specify the tag owner by the name of the relationship between the tagged model and the tag owner model.
How: write a module and include in your lib on initialization (require 'lib/path/to/tagger'):
module Giga::Tagger
extend ActiveSupport::Concern
included do
def self.tagger owner
before_save :set_tag_owner
def set_tag_owner
self.tag_types.each do |tag|
tag_type = tag.to_s
# Set the owner of some tags based on the current tag_list
set_owner_tag_list_on(owner, :"#{tag_type}", self.send(:"#{tag_type.chop}_list"))
# Clear the list so we don't get duplicate taggings
self.send(:"#{tag_type.chop}_list=",nil)
end
end
end
end
end
Usage Instructions:
Given: A model, Post, that is taggable
A model, User, that is the tag owner
A post is owned by the user through a relationship called :owner
Then add to Post.rb:
include Tagger
acts_as_taggable_on :skills, :interests, :tags
tagger :owner
Make sure Post.rb already has called acts_as_taggable_on, and that User.rb has acts_as_tagger
Note: This supports multiple tag contexts, not just tags (eg skills, interests)..
the set_tag_owner before_save worked for me. But as bcb mentioned, I had to add a condition (tag_list_changed?) to prevent the tags from being deleted on update:
def set_tag_owner
if tag_list_changed?
set_owner_tag_list_on(account, :tags, tag_list)
self.tag_list = nil
end
end
When working with ownership the taggable model gets its tags a little different. Without ownership it can get its tags like so:
#photo.tag_list << 'a tag' # adds a tag to the existing list
#photo.tag_list = 'a tag' # sets 'a tag' to be the tag of the #post
However, both of these opperations create taggins, whose tagger_id and tagger_type are nil.
In order to have these fields set, you have to use this method:
#user.tag(#photo, on: :tags, with: 'a tag')
Suppose you add this line to the create/update actions of your PhotosController:
#user.tag(#photo, on: :tags, with: params[:photo][:tag_list])
This will create two taggings (one with and one without tagger_id/_type), because params[:photo][:tag_list] is already included in photo_params. So in order to avoid that, just do not whitelist :tag_list.
For Rails 3 - remove :tag_list from attr_accessible.
For Rails 4 - remove :tag_list from params.require(:photo).permit(:tag_list).
At the end your create action might look like this:
def create
#photo = Photo.new(photo_params) # at this point #photo will not have any tags, because :tag_list is not whitelisted
current_user.tag(#photo, on: :tags, with: params[:photo][:tag_list])
if #photo.save
redirect_to #photo
else
render :new
end
end
Also note that when tagging objects this way you cannot use the usual tag_list method to retrieve the tags of a photo, because it searches for taggings, where tagger_id IS NULL. You have to use instead
#photo.tags_from(#user)
In case your taggable object belongs_to a single user you can also user all_tags_list.
Try using delegation:
class User < ActiveRecord::Base
acts_as_taggable_on
end
class Post < ActiveRecord::Base
delegate :tag_list, :tag_list=, :to => :user
end
So when you save your posts it sets the tag on the user object directly.
I ended up creating a virtual attribute that runs the User.tag statement:
In my thing.rb Model:
attr_accessible :tags
belongs_to :user
acts_as_taggable
def tags
self.all_tags_list
end
def tags=(tags)
user = User.find(self.user_id)
user.tag(self, :with => tags, :on => :tags, :skip_save => true)
end
The only thing you have to do is then change your views and controllers to update the tag_list to tags and make sure you set the user_id of the thing before the tags of the thing.

Resources