Custom Typecaster with ActiveAttr Rails Gem - ruby-on-rails

I would like to create a custom Typecaster for the ActiveAttr Gem.
I have a Package class:
class Package
include ActiveAttr::Model
attribute :quantity, :type => Integer
attribute :detail
attribute :type
attribute :order
def shipping
end
end
and I have Order class
class Order
include ActiveAttr::Model
attribute :id, :type => Integer
def test
end
end
In the Package class I want to use attribute :order, :type => OrderTypecaster because when I create a new package (p = Package.new(params['package']) I would like to set the Order id attribute automatically.
Is this possible?
I'm using Rails 3.2.13
Tks!

I found a way to solve my problem without create a custom Typecaster.
In Package class I wrote two simple methods:
def order=(value)
#order = Order.new(value)
end
def order
#order ||= Order.new
end
Now when I call Package.new(params['package']) the order id is automatically setted.
I don't know if this is the best solution but works well, any better solution is welcome. :)
Tks guys!

Related

How to return all attributes of an object with Rails Serializer?

I have a simple question. I have a seriaizer that looks like this:
class GroupSerializer < ActiveModel::Serializer
attributes :id, :name, :about, :city
end
The problem is that, whenever I change my model, I have to add/remove attributes from this serializer. I just want to get the whole object by default as in the default rails json respond:
render json: #group
How can I do that?
At least on 0.8.2 of ActiveModelSerializers you can use the following:
class GroupSerializer < ActiveModel::Serializer
def attributes
object.attributes.symbolize_keys
end
end
Be carful with this though as it will add every attribute that your object has attached to it. You probably will want to put in some filtering logic on your serializer to prevent sensitive information from being shown (i.e., encrypted passwords, etc...)
This does not address associations, although with a little digging around you could probably implement something similar.
============================================================
UPDATE: 01/12/2016
On 0.10.x version of ActiveModelSerializers, attributes receives two arguments by default. I added *args to avoid exception:
class GroupSerializer < ActiveModel::Serializer
def attributes(*args)
object.attributes.symbolize_keys
end
end
Just to add to #kevin's answer. I was looking also to how to add filters on the returned attributes. I looked to the the documentation active_model_serializers 0.9 and it does support filters that looks like this:
def attributes
object.attributes.symbolize_keys
end
def filter(keys)
keys - [:author, :id]
end
I tried it, but it did not work. I assumed that's because the attributes are not specified explicitly. I had to do it the same way specified in the rails cast to work:
##except=[:author, :id]
def attributes
data = object.attributes.symbolize_keys
##except.each { |e| data.delete e }
data
end
Try the following to get all the attribute keys for the Group class:
Group.new.attributes.keys
For example, I get the following for users on one app:
> User.new.attributes.keys
=> ["id", "password_digest", "auth_token", "password_reset_token", "password_reset_requested_at", "created_at", "updated_at"]
On 0.10.x version of ActiveModelSerializers, attributes receives two arguments by default. I added *args to avoid exception:
class GroupSerializer < ActiveModel::Serializer
def attributes(*args)
object.attributes.symbolize_keys
end
end
I want get all attributes + few more.
base on answer above, this work:
class NotificationSerializer < ActiveModel::Serializer
def actor
'asdasd'
end
def attributes(*args)
keys = object.attributes
keys[:actor] = actor() # add attribute here
keys.symbolize_keys
end
end

Mongoid: add some dynamic (non-DB) fields in Model before passing it to Controller

Let's say I have a Model Item which uses Mongoid
class Item
include Mongoid::Document
field :title, type: String
...
...
end
I want to add some dynamic fields to Item right in Model before passing data to a Controller - because Item is being used by several Controllers.
For example I want to add thumb field which I will generate by adding /path/to + filename.
I tried some solutions with attr_accessor:
class Item
include Mongoid::Document
field :title, type: String
...
...
attr_accessor :thumb
def prepare_data
#thumb = "/path/to/thumb"
end
end
...And later in some Controller:
#items_all = Item.all
#thumbs = []
#items_all.each do |i]
i.prepare_data
#thumbs.push(i[:thumb])
end
# #thumbs >>> (empty)
So it seems that I'm missing some point here because it doesn't work.
Also can I avoid calling prepare_data each time manually? May be with help of after_initialize? (which didn't work for me also).
I found my mistake. First I forgot to add after_initialize :do_something and then I found that I can user #attributes.merge!
after_initialize :do_after_initialize
def do_after_initialize
#attributes.merge!({
:title => self.get_title,
:type => self.get_type,
:thumb => ImageUploader::thumb(self[:pictures][0]["filename"])
:price_f => ActionController::Base.helpers.number_to_currency(self[:price], {:precision=>0})
})
end

How to create an object of a STI subclass using ActiveAdmin

Given the following setup(which is not working currently)
class Employee < ActiveRecord::Base
end
class Manager < Employee
end
ActiveAdmin.register Employee do
form do |f|
f.input :name
f.input :joining_date
f.input :salary
f.input :type, as: select, collection: Employee.descendants.map(&:name)
end
end
I would like to have a single "new" form for all employees and be able to select the STI type of the employee in the form.
I am able to see the select box for "type" as intended but when I hit the "Create" button, I get the following error:
ActiveModel::MassAssignmentSecurity::Error in Admin::EmployeesController#create
Can't mass-assign protected attributes: type
Now, I am aware of the way protected attributes work in Rails and I have a couple of workarounds such as defining Employee.attributes_protected_by_default but that is lowering the security and too hack-y.
I want to be able to do this using some feature in ActiveAdmin but I can't find one. I do not want to have to create a custom controller action as the example I showed is highly simplified and contrived.
I wish that somehow the controller generated by ActiveAdmin would identify type and do Manager.create instead of Employee.create
Does anyone know a workaround?
You can customize the controller yourself. Read ActiveAdmin Doc on Customizing Controllers. Here is a quick example:
controller do
alias_method :create_user, :create
def create
# do what you need to here
# then call create_user alias
# which calls the original create
create_user
# or do the create yourself and don't
# call create_user
end
end
Newer versions of the inherited_resources gem have a BaseHelpers module. You can override its methods to change how the model is altered, while still maintaining all of the surrounding controller code. It's a little cleaner than alias_method, and they have hooks for all the standard REST actions:
controller do
# overrides InheritedResources::BaseHelpers#create_resource
def create_resource(object)
object.do_some_cool_stuff_and_save
end
# overrides InheritedResources::BaseHelpers#destroy_resource
def destroy_resource(object)
object.soft_delete
end
end

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)

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