I am trying to implement a rails tagging model as outlined in Ryan Bate's railscast #167. http://railscasts.com/episodes/167-more-on-virtual-attributes
This is a great system to use. However, I cannot get the form to submit the tag_names to the controller. The definition for tag_names is :
def tag_names
#tag_names || tags.map(&:name).join(' ')
end
Unfortunately, #tag_names never gets assigned on form submission in my case. I cannot figure out why. SO it always defaults to tags.map(&:name).join(' '). This means that I can't create Articles because their tag_names are not there, and I also can't edit these tags on existing ones. Anyone can help?
In short, your class is missing a setter (or in Ruby lingo, an attribute writer). There are two ways in which you can define a setter and handle converting the string of space-separated tag names into Tag objects and persist them in the database.
Solution 1 (Ryan's solution)
In your class, define your setter using Ruby's attr_writer method and convert the string of tag names (e.g. "tag1 tag2 tag3") to Tag objects and save them in the database in an after save callback. You will also need a getter that converts the array of Tag object for the article into a string representation in which tags are separated by spaces:
class Article << ActiveRecord::Base
# here we are delcaring the setter
attr_writer :tag_names
# here we are asking rails to run the assign_tags method after
# we save the Article
after_save :assign_tags
def tag_names
#tag_names || tags.map(&:name).join(' ')
end
private
def assign_tags
if #tag_names
self.tags = #tag_names.split(/\s+/).map do |name|
Tag.find_or_create_by_name(name)
end
end
end
end
Solution 2: Converting the string of tag names to Tag objects in the setter
class Article << ActiveRecord::Base
# notice that we are no longer using the after save callback
# instead, using :autosave => true, we are asking Rails to save
# the tags for this article when we save the article
has_many :tags, :through => :taggings, :autosave => true
# notice that we are no longer using attr_writer
# and instead we are providing our own setter
def tag_names=(names)
self.tags.clear
names.split(/\s+/).each do |name|
self.tags.build(:name => name)
end
end
def tag_names
tags.map(&:name).join(' ')
end
end
Related
Right now, I have some legacy classes with differently-named columns that I've aliased to a new, common name via Rails' alias_attribute, as below:
class User < ActiveRecord::Base
alias_attribute :id, :UserId
...
end
class Car < ActiveRecord::Base
alias_attribute :id, :CarId
...
end
For some logging purposes, I need to access the old column names (eg. CarId and UserId). Is there a general way to access the old name from alias_attribute via its alias? Renaming the old columns is not ideal, since many other parts of the app are still using the old column names.
alias_attribute is a very simple method. All it does is, well, define aliases.
def alias_attribute(new_name, old_name)
# The following reader methods use an explicit `self` receiver in order to
# support aliases that start with an uppercase letter. Otherwise, they would
# be resolved as constants instead.
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
STR
end
So no, there's no way to retrieve the original column name.
How does attr_accessor works in ActiveResource?
class User < ActiveResource::Base
attr_accessor :name
end
How its different from attr_accessor in ActiveRecord?
attr_accessor is built into Ruby, not rails. You may be confusing it with attr_accessible, which is part of ActiveRecord. Here's the difference:
attr_accessor
Take a class:
class Dog
attr_accessor :first_name, :last_name
def initialize(first_name, last_name)
self.first_name = first_name
self.last_name = last_name
end
end
attr_accessor creates a property and creates methods that allow it to be readable and writeable. Therefore, the above class would allow you to do this:
my_dog = Dog.new('Rex', 'Thomas')
puts my_dog.first_name #=> "Rex"
my_dog.first_name = "Baxter"
puts my_dog.first_name #=> "Baxter"
It creates two methods, one for setting the value and one for reading it. If you only want to read or write, then you can use attr_reader and attr_writer respectively.
attr_accessible
This is an ActiveRecord specific thing that looks similar to attr_accessor. However, it behaves very differently. It specifies which fields are allowed to be mass-assigned. For example:
class User
attr_accessible :name, :email
end
Mass assignment comes from passing the hash of POST parameters into the new or create action of a Rails controller. The values of the hash are then assigned to the user being created, e.g.:
def create
# params[:user] contains { name: "Example", email: "..."}
User.create(params[:user])
#...
end
For the sake of security, attr_accessible has to be used to specify which fields are allowed to be mass-assigned. Otherwise, if the user had an admin flag, someone could just post admin: true as data to your app, and make themselves an admin.
In summary
attr_accessor is a helper method for Ruby classes, whereas attr_accessible is an ActiveRecord thing for rails, to tighten up security.
You don't need to have attr_accessor to work with ActiveResource.
The base model (ActiveResource::Base) contains the #attributes hash in which you can 'dump' properties as you wish. (you should be careful though on what params you allow)
The way it does this, is by handling the method_missing? method.
You can take a look here
If you define attr_accessor, what ruby does is that it creates a setter and a getter method, so it will break the method_missing functionality since it will never get to execute that code.
If you still want to use attr_accessor, you should create a Concern something like this:
module Attributes
extend ActiveSupport::Concern
module ClassMethods
def attr_accessor(*attribs)
attribs.each do |a|
define_method(a) do
#attributes[a]
end
define_method("#{a}=") do |val|
#attributes[a] = val
end
end
end
end
end
Due to the nature of my use of 'updated_at' (specifically for use in atom feeds), I need to avoid updating the updated_at field when a record is saved without any changes. To accomplish that I read up and ended up with the following:
module ActiveRecord
class Base
before_validation :clear_empty_strings
# Do not actually save the model if no changes have occurred.
# Specifically this prevents updated_at from being changed
# when the user saves the item without actually doing anything.
# This especially helps when synchronizing models between apps.
def save
if changed?
super
else
class << self
def record_timestamps; false; end
end
super
class << self
remove_method :record_timestamps
end
end
end
# Strips and nils strings when necessary
def clear_empty_strings
attributes.each do |column, value|
if self[column].is_a?(String)
self[column].strip.present? || self[column] = nil
end
end
end
end
end
This works fine on all my models except for my Email model. An Email can have many Outboxes. An outbox is basically a two-column model that holds a subscriber (email To:) and an email (email to send to subscriber). When I update the attributes of an outbox and then save Email, I get the (arguments 1 for 0) error on save (it points to the 'super' call in the save method).
Email.rb
has_many :outboxes, :order => "subscriber_id", :autosave => true
Outbox.rb
belongs_to :email, :inverse_of => :outboxes
belongs_to :subscriber, :inverse_of => :outboxes
validates_presence_of :subscriber_id, :email_id
attr_accessible :subscriber_id, :email_id
UPDATE: I also noticed that the 'changed' array isn't being populated when I change the associated models.
#email.outboxes.each do |out|
logger.info "Was: #{ out.paused }, now: #{ !free }"
out.paused = !free
end unless #email.outboxes.empty?
#email.save # Upon saving, the changed? method returns false...it should be true
...sigh. After spending countless hours trying to find a solution I came across this. Has I known that the 'save' method actually takes an argument I would have figured this out sooner. Apparently looking at the source didn't help in that regard. All I had to do was add an args={} parameter in the save method and pass it to 'super' and everything is working now. Unmodified records are saved without updating the timestamp, modified records are saved with the timestamp and associations are saved without error.
module ActiveRecord
class Base
before_validation :clear_empty_strings
# Do not actually save the model if no changes have occurred.
# Specifically this prevents updated_at from being changed
# when the user saves the item without actually doing anything.
# This especially helps when synchronizing models between apps.
def save(args={})
if changed?
super args
else
class << self
def record_timestamps; false; end
end
super args
class << self
remove_method :record_timestamps
end
end
end
# Strips and nils strings when necessary
def clear_empty_strings
attributes.each do |column, value|
if self[column].is_a?(String)
self[column].strip.present? || self[column] = nil
end
end
end
end
I'm relatively new to Rails and a bit surprised this isn't a configurable behavior...at least not one I've been able to find yet?!? I would have thought that 99% of forms would benefit from whitespace being trimmed from all string & text fields?!? Guess I'm wrong...
Regardless, I'm looking for a DRY way to strip all whitespace from form fields (of type :string & :text) in a Rails 3 app.
The Views have Helpers that are automatically referenced (included?) and available to each view...but Models don't seem to have such a thing?!? Or do they?
So currently I doing the following which first requires and then includes the whitespace_helper (aka WhitespaceHelper). but this still doesn't seem very DRY to me but it works...
ClassName.rb:
require 'whitespace_helper'
class ClassName < ActiveRecord::Base
include WhitespaceHelper
before_validation :strip_blanks
...
protected
def strip_blanks
self.attributeA.strip!
self.attributeB.strip!
...
end
lib/whitespace_helper.rb:
module WhitespaceHelper
def strip_whitespace
self.attributes.each_pair do |key, value|
self[key] = value.strip if value.respond_to?('strip')
end
end
I guess I'm looking for a single (D.R.Y.) method (class?) to put somewhere (lib/ ?) that would take a list of params (or attributes) and remove the whitespace (.strip! ?) from each attribute w/out being named specifically.
Create a before_validation helper as seen here
module Trimmer
def trimmed_fields *field_list
before_validation do |model|
field_list.each do |n|
model[n] = model[n].strip if model[n].respond_to?('strip')
end
end
end
end
require 'trimmer'
class ClassName < ActiveRecord::Base
extend Trimmer
trimmed_fields :attributeA, :attributeB
end
Use the AutoStripAttributes gem for Rails. it'll help you to easily and cleanly accomplish the task.
class User < ActiveRecord::Base
# Normal usage where " aaa bbb\t " changes to "aaa bbb"
auto_strip_attributes :nick, :comment
# Squeezes spaces inside the string: "James Bond " => "James Bond"
auto_strip_attributes :name, :squish => true
# Won't set to null even if string is blank. " " => ""
auto_strip_attributes :email, :nullify => false
end
Note I haven't tried this and it might be a crazy idea, but you could create a class like this:
MyActiveRecordBase < ActiveRecord::Base
require 'whitespace_helper'
include WhitespaceHelper
end
... and then have your models inherit from that instead of AR::Base:
MyModel < MyActiveRecordBase
# stuff
end
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.