Passing attributes to Mongoid update_attributes() - ruby-on-rails

I want to use this function from mongoid:
person.update_attributes(first_name: "Jean", last_name: "Zorg")
But I want to pass in all the attributes from another variable. How do I do that?
Edit: Thanks everyone for your reply. I'm new to ruby so at first I thought I just made a silly mistake with this. The bug was in a completely different place, the correct code, for your enjoyment:
def twitter
# Scenarios:
# 1. Player is already signed in with his fb account:
# we link the accounts and update the information.
# 2. Player is new: we create the account.
# 3. Player is old: we update the player's information.
# login with a safe write.
puts "twitter"
twitter_details = {
twitter_name: env["omniauth.auth"]['user_info']['name'],
twitter_nick: env["omniauth.auth"]['user_info']['nickname'],
twitter_uid: env["omniauth.auth"]['uid']
}
if player_signed_in?
#player = Player.find(current_player['_id'])
else
#player = Player.first(conditions: {twitter_uid: env['omniauth.auth']['uid']})
end
if #player.nil?
#player = Player.create!(twitter_details)
else
#player.update_attributes(twitter_details)
end
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Twitter"
sign_in_and_redirect #player, :event => :authentication
end

The update_attributes method takes a Hash argument so if you have a Hash, h, with just :first_name and :last_name keys then:
person.update_attributes(h)
If your Hash has more keys then you can use slice to pull out just the ones you want:
person.update_attributes(h.slice(:first_name, :last_name))

if you look at the source code of Mongoid, you'll see the definition of update_attributes in the file
.rvm/gems/ruby-1.9.2-p0/gems/mongoid-2.3.1/lib/mongoid/persistence.rb
# Update the document attributes in the datbase.
#
# #example Update the document's attributes
# document.update_attributes(:title => "Sir")
#
# #param [ Hash ] attributes The attributes to update.
#
# #return [ true, false ] True if validation passed, false if not.
def update_attributes(attributes = {})
write_attributes(attributes); save
end
It takes a Hash -- that means you can use a Hash as the variable that's passed in.
e.g.
my_attrs = {first_name: "Jean", last_name: "Zorg"}
person.update_attributes( my_attrs )

What's happening in the update_attributes method and, indeed, across the Rails platform is variables get put into a hash internally, when necessary.
So the following are equivalent:
person.update_attributes(first_name: "Jean", last_name: "Zorg")
person.update_attributes({first_name: "Jean", last_name: "Zorg"})
person.update_attributes(name_hash)
Where name_hash is:
name_hash = {first_name: "Jean", last_name: "Zorg"}

Related

update hash by method to save in db Rails

I am trying to update a hash that is being made when a csv is uploaded by a user, so that it saves the added key/value pair to the db.
How can I update the hash being made in the create_by_location method with the method check_enable_recordings
user model
before_save :check_enable_recordings
def check_enable_recordings
x = tenant.enable_recording_extensions
Rails.logger.debug("check if true #{x}" )
if x
user = User.new(
recorded: "1"
)
end
end
def self.create_by_location(location,hash)
user = User.new(
first_name: hash[:firstname],
last_name: hash[:lastname],
email: hash[:email],
)
end
Perhaps you're looking for something like:
before_save :check_enable_recordings
def check_enable_recordings
self.recorded = 1 if tenant.enable_recording_extensions
end
def self.create_by_location(location,hash)
user = User.new(
first_name: hash[:firstname],
last_name: hash[:lastname],
email: hash[:email],
)
end
BTW, you don't seem to use the location argument anywhere. Maybe you're not showing us all the code.
Also, if you have control over the construction of the hash argument, you should probably change firstname to first_name and lastname to last_name so you can just do:
def self.create_by_location(location,hash)
user = User.new(hash)
end

Rails4: How to permit a hash with dynamic keys in params?

I make a http put request with following parameters:
{"post"=>{"files"=>{"file1"=>"file_content_1",
"file2"=>"file_content_2"}}, "id"=>"4"}
and i need to permit hash array in my code.
based on manuals I've tried like these:
> params.require(:post).permit(:files) # does not work
> params.require(:post).permit(:files => {}) # does not work, empty hash as result
> params.require(:post).permit! # works, but all params are enabled
How to make it correctly?
UPD1: file1, file2 - are dynamic keys
Rails 5.1+
params.require(:post).permit(:files => {})
Rails 5
params.require(:post).tap do |whitelisted|
whitelisted[:files] = params[:post][:files].permit!
end
Rails 4 and below
params.require(:post).tap do |whitelisted|
whitelisted[:files] = params[:post][:files]
end
In rails 5.1.2, this works now:
params.require(:post).permit(:files => {})
See https://github.com/rails/rails/commit/e86524c0c5a26ceec92895c830d1355ae47a7034
I understand that this is an old post. However, a Google search brought me to this result, and I wanted to share my findings:
Here is an alternative solution that I have found that works (Rails 4):
params = ActionController::Parameters.new({"post"=>{"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}, "id"=>"4"})
params.require(:post).permit(files: params[:post][:files].keys)
# Returns: {"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}
The difference between this answer and the accepted answer, is that this solution restricts the parameter to only 1 level of dynamic keys. The accepted answer permits multiple depths.
[Edit] Useful tip from comment
"Oh, and you need to verify that params[:post][.files] exists otherwise keys will fail"
Orlando's answer works, but the resulting parameter set returns false from the permitted? method. Also it's not clear how you would proceed if you were to later have other parameters in the post hash that you want included in the result.
Here's another way
permitted_params = params.require(:post).permit(:other, :parameters)
permitted_params.merge(params[:post][:files])
Here's what we had to do in Rails 5.0.0, hope this helps someone.
files = params[:post].delete(:files) if params[:post][:files]
params.require(:post).permit(:id).tap do |whitelisted|
whitelisted[:files] = files.permit!
end
In my case, there was just one attribute which had dynamic keys,
def post_params
marking_keys = Set.new
params[:post][:marking].keys.collect {|ii| marking_keys.add(ii)}
params.require(:post).permit(:name, marking: marking_keys.to_a)
end
Here is another way to get around this:
def post_params
permit_key_params(params[:post]) do
params.require(:post)
end
end
def permit_key_params(hash)
permitted_params = yield
dynamic_keys = hash.keys
dynamic_keys.each do |key|
values = hash.delete(key)
permitted_params[key] = values if values
end
permitted_params
end
This should work for post: { something: {...}, something_else: {...} }
You can use a temporary variable to build your permitted list like so:
permitted = params.require(:post).permit(:id)
permitted[:post][:files] = params[:post][:files].permit!
Here's a simple way to do it (works for rails 5):
def my_params
data_params = preset_data_params
params.require(:my_stuff).permit(
:some,
:stuff,
data: data_params
)
end
def preset_data_params
return {} unless params[:my_stuff]
return {} unless params[:my_stuff][:data]
params[:my_stuff][:data].keys
end
Send params as array type like name=date[]**strong text**
def user_post
dates = params[:date]
#render json: { 'response' => params }
i = 0
dates.each do |date|
locations = params['location_'+"#{i}"]
user_names = params['user_'+"#{i}"]
currency_rates = params['currency_'+"#{i}"]
flags = params['flag_'+"#{i}"]
j = 0
locations.each do |location|
User.new(user_name: user_names[j], currency_name: flags[j],
currency_rate: currency_rates[j], currency_flag: flags[j], location: location).save
j =+ 1
end
i =+ 1
end
def
I could not get any of the many proposed answers to work (Rails 5) without either:
knowing all the hash keys in advance, or
virtually negating the value of strong parameters by allowing arbitrary params.
I'm using this solution.
It uses the standard strong parameters rig to clean up most of the params,
and the Hash attribute is added back in explicitly.
# Assuming:
class MyObject < ApplicationRecord
serialize :hash_attr as: Hash
#...
end
# MyObjectsController method to filter params:
def my_object_params
# capture the hashed attribute value, as a Hash
hash_attr = params[:my_object] && params[:my_object][:hash_attr] ?
params[my_object][:hash_attr].to_unsafe_h : {}
# clean up the params
safe_params = params.require(:my_object).permit(:attr1, :attr2) # ... etc
# and add the hashed value back in
safe_params.to_unsafe_h.merge hash_attr: hash_attr
end
Let's use a more complicated subset of data:
task: {
code: "Some Task",
enabled: '1',
subtask_attributes: {
'1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
'2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }}
}
}
So we send it to Strong Parameters for processing:
params = ActionController::Parameters.new({
task: {
code: "Some Task",
enabled: '1',
subtask_attributes: {
'1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
'2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }}
}
}
})
We will not be able to specify :rules in Strong Params in Rails 4 because it is a hash of data:
permitted = params.require(:task).permit(:code, :enabled, subtask_attributes: [:field, :rules])
Unpermitted parameter: rules
Unpermitted parameter: rules
So what if you want to whitelist specific attributes AND a COLLECTION of hashes of data. The accepted answer does not whitelist specified attributes. You have to do this:
params.require(:task).permit(
:code, :enabled,
subtask_attributes: [:field, :rules],
)
# whitelist the validation rules hash
params.require(:task).tap do |whitelisted|
params[:task][:subtask_attributes].each do |k,v|
whitelisted[:subtask_attributes][k] = params[:task][:subtask_attributes][k]
whitelisted.permit!
end
end
After trying several of the solutions here, none worked. Only aboved worked for nested attributes in a has_many association which contains arbitrary hash data.
I know this is an old post, one of many with different ways to update a serialize hash field. I thought I give my version that I accidently found by piecing together some methods. I'll just use my application. This is Rails 7.0.4 and Ruby 3.0. I also use slim templates.
I have a Taxable model that contains semi-persistent tax rates for different Departments. All items are Sales Tax taxable, but in my case, Liquor adds an additional tax. The Taxable table only has two fields with tax being a serialized JSON field.
create_table "taxables", force: :cascade do |t|
t.date "date"
t.string "tax"
...
end
If a Tax is changed or added, the I would add a new record to reflect the change that took place on some date. Any ticket that had a tax in the past would use the record that is the earliest record before the ticket date. Anything new will the new changed record
The Taxable model has a constant that names all taxes that may be used:
TaxesUsed = %w(sales county federal city liquor)
The records would be something like:
[#<Taxable:0x0000000111c7bfc0
id: 2,
date: Sun, 01 Jan 2023,
tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"3.0"} ...
#<Taxable:0x0000000111c7b980
id: 3,
date: Fri, 01 Jan 2021,
tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"4.0"}...
]
I initially had a kludge that worked, which was creating the hash from some un-permitted parameter and updating the record. I then found mention of using form_with to describe the Tax field and to my surprise it worked! The form:
= form_with(model: #taxable) do |form|
div
= form.label :date, style: "display: block"
= form.date_field :date
div
= form.label :tax, style: "display: block", class:"font-bold"
= form.fields_for :tax do |tax|
# #taxable.tax is the existing serialize tax hash or a new default hash
- #taxable.tax.each do |k,v|
div.flex.gap-2
div.w-36.font-bold.text-right = k
div
= tax.text_field k, value:v
div[class="#{btn_submit}"]
= form.submit
I had to define a new taxable_parmam that states that :tax is a Hash
def taxable_params
params.require(:taxable).permit(:date, :tax => {})
end
Submitting the form give me params:
Parameters: {"authenticity_token"=>"[FILTERED]",
"taxable"=>{"date"=>"2021-01-01", "tax"=>{"sales"=>"8.0",
"county"=>"2.0", "federal"=>"0.0", "city"=>"0.0",
"liquor"=>"4.0"}}, "commit"=>"Update Taxable", "id"=>"3"}
and it works! I forgot about form_with but this is about a simple as you can get just using plain ol Rails.
Update: I forgot that stuff coming from form fields is text. I had to get the params to a new hash, change the float values (percents) and update using the new hash

Rails delayed_job argument error with mailer

I'm getting a "wrong number of arguments (2 for 1)" error when I try send a mailer to delayed_job. Everything works perfect if I don't try to push it to the background. Below is my controller with delayed_job:
def export
#user = User.where("id = ?", params[:user_id])
#logs = VehicleMileage.export_logs(params, #user)
if #logs['log_count'] > 0
ExportLogsMailer.delay.email_logs(#user, #logs['sending_to'])
end
respond_to do |format|
formats # index.html.erb
format.json { render json: #logs }
end
end
When I ExportLogsMailer.email_logs(#user, #logs['sending_to']).deliver the mailer works fine. I'm using the gem 'delayed_job_active_record' and gem 'rails', '3.2.13'.
Here is what ExportLogsMailer looks like:
class ExportLogsMailer < ActionMailer::Base
default :from => "support#myconsultantapp.com"
def email_logs(user, emails)
# make sure emails are unique
emails.uniq
first_name = user[0].first_name
last_name = user[0].last_name
# encoded_content = Base64.strict_encode64(File.read("#{Rails.root}/tmp/test.xls"))
# puts encoded_content
# attachments['test.xls'] = { :content => encoded_content,
# :encoding => 'Base64'
# }
# Name of template in your Mandrill template area
headers['X-MC-Template'] = 'Export Logs'
# Tags help classify your messages
headers['X-MC-Tags'] = 'mileage logs'
# Enable open or click-tracking for the message.
# Only can track to at a time. Possibilies: opens, clicks, clicks_htmlonly, clicks_textonly
headers['X-MC-Track'] = 'opens, clicks'
# Automatically generate a plain-text version of the email from the HTML content.
headers['X-MC-Autotext'] = 'true'
# Add dynamic data to replace mergetags that appear in your message content. Should be a JSON-formatted
# object and flat, so if you more than one recipient then add another X-MC-MergeVars to the header. The
# below example will change anywhere where *|FNAME|* or *|LNAME|* to the respective value.
mergeVars =
{
"fname" => first_name,
"lname" => last_name
}
headers['X-MC-MergeVars'] = mergeVars.to_json
# Add Google Analytics tracking to links in your email for the specified domains.
headers['X-MC-GoogleAnalytics'] = 'http://www.myconsultantapp.com/'
# Add an optional value to be used for the utm_campaign parameter in Google Analytics tracked links.
# headers['X-MC-GoogleAnalyticsCampaign'] = ''
# Information about any custom fields or data you want to append to the message.
# Up to 200 bytes of JSON-encoded data as an object. The object should be flat; nested object structures are not supported.
# headers['X-MC-Metadata'] = ''
# Whether to strip querystrings from links for reporting. "true" or "false"
# headers['X-MC-URLStripQS'] = 'true'
# Whether to show recipients of the email other recipients, such as those in the "cc" field. "true" or "false"
headers['X-MC-PreserveRecipients'] = 'false'
message = prepare_message :subject => "1 myConsultant logs",
:to => emails,
:content_type => "multipart/mixed"
message.alternative_content_types_with_attachment(
:text => 'text',
:html => 'html'
) do |i|
i.inline['test.xls'] = File.read("#{Rails.root}/tmp/test.xls")
end
message
end
end
Any help would be greatly appreciated. Thanks you!
Are you trying to retrieve just one user? Then do:
#user = User.find(params[:user_id])
Otherwise, if you're trying to pass an array of objects to delayed_job, I think you need to add all at the end:
#objects = Model.where(...).all

Is there a way to bypass mass assignment protection?

I have a Rails 3 app which JSON encodes objects in order to store them in a Redis key/value store.
When I retrieve the objects, I'm trying to decode the JSON and instantiate them from the data like so:
def decode(json)
self.new(ActiveSupport::JSON.decode(json)["#{self.name.downcase}"])
end
The problem is that doing this involves mass assignment which is disallowed (for good reason I'm told!) for attributes I haven't given attr_writer ability to.
Is there a way I can bypass the mass assignment protection just for this operation only?
assign_attributes with without_protection: true seems less intrusive:
user = User.new
user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
user.name # => "Josh"
user.is_admin? # => true
#tovodeverett mentioned in the comment you can also use it with new, like this in 1 line
user = User.new({ :name => 'Josh', :is_admin => true }, :without_protection => true)
EDIT: kizzx2's Answer is a much better solution.
Kind of a hack, but...
self.new do |n|
n.send "attributes=", JSON.decode( json )["#{self.name.downcase}"], false
end
This invokes attributes= passing false for the guard_protected_attributes parameter which will skip any mass assignment checks.
You can create a user also in this way which is not doing the mass assignment.
User.create do |user|
user.name = "Josh"
end
You may want to put this into a method.
new_user(name)
User.create do |user|
user.name = name
end
end

How to convert a Ruby object to JSON

I would like to do something like this:
require 'json'
class Person
attr_accessor :fname, :lname
end
p = Person.new
p.fname = "Mike"
p.lname = "Smith"
p.to_json
Is it possible?
Yes, you can do it with to_json.
You may need to require 'json' if you're not running Rails.
To make your Ruby class JSON-friendly without touching Rails, you'd define two methods:
to_json, which returns a JSON object
as_json, which returns a hash representation of the object
When your object responds properly to both to_json and as_json, it can behave properly even when it is nested deep inside other standard classes like Array and/or Hash:
#!/usr/bin/env ruby
require 'json'
class Person
attr_accessor :fname, :lname
def as_json(options={})
{
fname: #fname,
lname: #lname
}
end
def to_json(*options)
as_json(*options).to_json(*options)
end
end
p = Person.new
p.fname = "Mike"
p.lname = "Smith"
# case 1
puts p.to_json # output: {"fname":"Mike","lname":"Smith"}
# case 2
puts [p].to_json # output: [{"fname":"Mike","lname":"Smith"}]
# case 3
h = {:some_key => p}
puts h.to_json # output: {"some_key":{"fname":"Mike","lname":"Smith"}}
puts JSON.pretty_generate(h) # output
# {
# "some_key": {
# "fname": "Mike",
# "lname": "Smith"
# }
# }
Also see "Using custom to_json method in nested objects".
Try it. If you're using Ruby on Rails (and the tags say you are), I think this exact code should work already, without requiring anything.
Rails supports JSON output from controllers, so it already pulls in all of the JSON serialization code that you will ever need. If you're planning to output this data through a controller, you might be able to save time by just writing
render :json => #person

Resources