Rails 5 API - saving data using virtual attributes does not save data - ruby-on-rails

I have a model which exposes a field called 'body' in the api. The user submit json including that field, and it should get saved to a field called 'custom_body' in the database.
If custom_body is empty, then I use i18n to return a default string for the 'body' field.
This is the model:
class Reminder < ApplicationRecord
# Relationships
belongs_to :user
def body=(value)
self.custom_body = value
end
def body
custom_body.presence || I18n.t('reminder.default_body', name: self.user.name)
end
end
The controller is scaffolded so standard and works fine. Here is the update action:
# PATCH/PUT /reminders/1
def update
if #reminder.update(reminder_params)
render json: #reminder
else
render json: #reminder.errors, status: :unprocessable_entity
end
end
and here are the whitelisted params:
# Only allow a trusted parameter "white list" through.
def reminder_params
params.require(:reminder).permit( :id, :user_id, :subject, :greeting, :body, :is_follow_up)
end
The problem is the default returns fine, but when the user submits 'body' it does not get persisted into 'custom_body', which I thought is solved by this method:
def body=(value)
self.custom_body = value
end
It works in this gorails cast (see 10 min mark), so what am I missing?

Just in case someone else stumbles on this question the Rails code is correct.
The problem here was strong parameters. In the controller notice the require:
def reminder_params
params.require(:reminder).permit( :id, :user_id, :subject, :greeting, :body, :is_follow_up)
end
The json body of the patch request therefore must be structured like this:
{ "reminder":
{ ... all the fields ... }
}
The client was just sending a list of fields and not putting them inside the 'reminder' object.
For example, it needs to be like this:
Parameters:
{"reminder"=>
{"subject"=>"your checklist reminder!",
"greeting"=>"Hi",
"body"=>"\nThis is a quick reminder about the outstanding items still on your checklist.\n\nPlease click the button below to review the items we're still waiting on.\n\nThank you,\n\nRichard Owner",
"id"=>"33b29a76-298f-4ef2-a76a-ca4438c6d1ce"
}
}

Related

Custom Validator on: :create not running on rails app

I have a rails application for creating volumes and have written two custom validators using ActiveModel::Validator.
volume.rb:
class Volume < ActiveRecord::Base
include UrlSafeCode
include PgSearch::Model
include ActiveModel::Validations
validates :user_id, presence: true
validates_with Validators::VolumeValidator
validates_with Validators::CreateVolumeValidator, on: :create
def self.digest text
Digest::SHA256.hexdigest(text)
end
def text=(new_text)
new_text.rstrip!
new_text.downcase!
self.text_digest = Volume.digest(new_text)
super(new_text)
end
My Problem: The CreateVolumeValidator checks if a record with the same text_digest is already in the database. I only want to run this when creating a new volume so that I can still update existing volumes. However, adding on: :create to the CustomVolumeValidator causes the validator to stop working.
I've read through a lot of the other entries about similar issues and haven't found a solution. I am pretty sure I am missing something about when different attributes are getting created, validated, and saved, but I haven't worked with custom validations much, and I'm lost.
Here is the other relevant code.
volumes_controller.rb
def new
#volume = Volume.new
end
def create
our_params = params
.permit(:text, :description)
if params[:text].nil?
render :retry
return
end
text = params[:text].read.to_s
text_digest = Volume.digest(text)
#description = our_params[:description]
begin
#volume = Volume.where(text_digest: text_digest)
.first_or_create(text: text, user: current_user, description: our_params[:description])
rescue ActiveRecord::RecordNotUnique
retry
end
if #volume.invalid?
render :retry
return
end
render :create
end
def edit
get_volume
end
def update
get_volume
unless #volume
render nothing: true, status: :not_found
return
end
#volume.update(params.require(:volume).permit(:text, :description))
if #volume.save
redirect_to volume_path(#volume.code)
else
flash[:notice] = #volume.errors.full_messages.join('\n')
render :edit
end
end
def get_volume
#volume = Volume.where(code: params.require(:code)).first
end
create_volume_validator.rb
class Validators::CreateVolumeValidator < ActiveModel::Validator
def validate(volume)
existing_volume = Volume.where(text_digest: volume.text_digest).first
if existing_volume
existing_volume_link = "<a href='#{Rails.application.routes.url_helpers.volume_path(existing_volume.code)}'>here</a>."
volume.errors.add :base, ("This volume is already part of the referral archive and is available " + existing_volume_link).html_safe
end
end
end
If your goal is for all Volume records to have unique text_digest, you are better off with a simple :uniqueness validator (and associated DB unique index).
However, the reason your existing code isn't working is:
Volume.where(text_digest: text_digest).first_or_create(...)
This returns either the first Volume with the matching text_digest or creates a new one. But that means if there is a conflict, no object is created, and therefore your (on: :create) validation doesn't run. Instead, it simply sets #volume to the existing object, which is, by definition, valid. If there is no matching record, it does call your validator, but there's nothing to validate because you've already proved there is no text_digest conflict.
You could resolve by replacing the first_or_create with create, but again, you are vastly better off with a unique index & validator (with custom message if you like).

Rails API - sending a customized email without using a view, how can I validate the data?

I have a Rails API which will receive requests that contain email details (to, subject, from etc) in the request of the body.
Since these emails are sent to customers, I set up the route, controller action and mailer as follows:
routes.rb
resource :customers do
member do
post 'send_checklist'
end
end
customers_controller.rb
def send_checklist
CustomerMailer.send_checklist(customer_params[:email]).deliver_now
end
customer_mailer.rb
class CustomerMailer < ApplicationMailer
def send_checklist(data)
mail(
to: data.to,
from: 'support#abc.com',
subject: data[:subject],
body: data[:body],
content_type: 'text/html'
)
end
end
json structure in body of post request
"customer": {
"mail": {
"to":"test#test.com, test1#test.com"
"subject":"Test message"
"body":"This is the email message"
}
}
This works, however I cannot validate that the 'to' property has at least one email address and that each email address is a valid email.
How can I validate the 'to' property and return a 422 unprocessible entity error from the controller action?
So in psuedo-code the action would be something like this:
def send_checklist
if data_is_valid
send_email
respond with 200 ok (or 204 No Content?)
else
respond with 422
end
end
I am thinking that somehow this validation logic should go into the CustomerMailer class itself, or maybe I should create a Mail model class with attr_accessors (to, subject, body) with validators? Not sure what is the correct solution, hence the question.
EDIT - proposed solution, which fails
Based on the first two proposed answers, I wrote this code:
customers_controller.rb
def send_checklist
from_user = "#{current_api_user.name} <#{current_api_user.email}>"
if CustomerMailer.emails_valid?(customer_params[:mail][:to])
CustomerMailer.send_checklist(from_user, customer_params[:mail]).deliver_now
render json: nil, status: :ok
else
render json: ErrorSerializer.serialize('Invalid email address found'), status: :unprocessable_entity
end
end
customer_mailer.rb
def emails_valid?(emails_list)
Rails.logger.debug "*** HERE ***" + emails_list
emails = emails_list.split(/\s+,\s+/)
Rails.logger.debug emails
is_valid = true
emails.each do |email|
Rails.logger.debug email
is_valid = false unless email=~ /([^\s]+)#([^\s]+)/
end
return is_valid
end
The emails_valid? is not called by the controller - nothing appears in the log from that method. It seems the controller does not call it and the if statement always returns true.
On further investigation, it seems I cannot put the emails_valid? method in the mailer class because of the way ActionMailer works as explained here.
EDIT - clarification
Just in case it is not clear, this is not a normal mail sending flow that you see in a Rails application:
This is an API, there are no rails forms.
The client sends a post request with the JSON shown above - the "To" field contains a string of comma-separated email addresses.
There is NO model here. None of the data sent in the request is stored in the database, it simply used to construct and send an email.
The only 'relationship' involved is that the post request is sent to customers/id - this is so later I can log that an action (send_checklist) was taken for this customer.
If you've a model to store email then you could check the param by querying the model
emails_arr = params[:to].split(",").map(&:strip)
This will give you an array of email from the params. Then you could use query to check whether the DB has the email present in the array. Assuming you've a Customer model you could do
Customer.where(email: email_arr).pluck(:email).join(",")
Pass this to the mailer's to param.
If you don't have a Customer model to query then you could use simple regex like below to check the validity of the email.
/\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
Ruby Regex docs for your reference
Assuming these users aren't existing users in your system this is how I would approach it.
in your Controller:
def send_checklist
from_user = "#{current_api_user.name} <#{current_api_user.email}>"
render json: ErrorSerializer.serialize('Invalid email address found'), status: :unprocessable_entity unless email_valid?(customer_params[:mail][:to])
CustomerMailer.send_checklist(from_user, customer_params[:mail]).deliver_now
render json: nil, status: :ok
end
private
def email_valid?(emails_list)
emails = emails_list.split(/\s+,\s+/)
true unless emails.any? {|email| email=!~ /([^\s]+)#([^\s]+)/ }
end
Or something to that effect. I am also not sure why you would need to validate multiple email addresses based on your code but I noticed your solution was taking a list so I made mine take a list as well. Your use case is a bit uncommon I think because most of the time the email is validated on saving to the db then the mail is sent after that but you don't have that step. You may also need to cast customer_params[:mail][:to] to an array if it is not coming over as such.

How to implement update controller method

I am working on web app development using ruby on rails. I want to enable users to upload images for their favorite food. I have food_item model and a food_image model. In the food_item model:
has_many :food_images
has_many :food_portions #this is the work done by another teammate
I also define in the food_item controller:
def food_item_params
params.require(:food_items).permit(:name, :category, :description, food_images_attributes: [:id, :food_item_id, :avatar]).tap do |whitelisted|
whitelisted[:portion] = params[:food_items][:portion]
whitelisted[:price] = params[:food_items][:price]
end
There are two issues:
You need to save your updated object
You should be doing this within the bounds of resources (although not essential)
Resourceful
The first step is to ensure you're using this with the correct routes etc.
You shouldn't have an add_images method in your controller - you could achieve what you need with edit/update:
#config/routes.rb
resources :food_items do
resources :images #-> if necessary, this should be its own controller rather than adding needless methods to your other controller
end
You should use the following controller setup:
#app/models/food_item.rb
class FoodItem < ActiveRecord::Base
accepts_nested_attributes_for :food_images
end
#app/controllers/food_items_controller.rb
class FoodItemsController < ApplicationController
def edit
#food_item = FoodItem.find params[:id]
#food_item.food_images.build
end
def update
#food_item = FootItem.find params[:id]
respond_to do |format|
if #food_item.update food_item_params
...
end
end
end
private
def food_item_params
params.require(:food_items).permit(:name, :category, :description, food_images_attributes: [:id, :food_item_id, :avatar]) #-> this is enough (no need to "whitelist")
end
end
This will give you the ability to load the following:
#url.com/food_items/:id/edit
#app/views/food_items/edit.html.erb
<%= form_for #food_item do |f| %>
= form_for #food_item, html: { :multipart => true } do |f|
= f.label :title
= f.text_field :title
= f.fields_for :food_images do |p|
= p.label :avatar
= p.file_field :avatar
.actions
= f.submit
<% end %>
This will submit to the "update" method, which should save the required object for you.
You should only upload one file at a time
If you need to upload multiple files, you'll need to use a gem such as cocoon to add them. Rails is great but not magical -- it has to build a single object with each fields_for.
I can explain more about this if required.
--
To give you context on why you should be using the edit / update methods for this, you need to look up the resourceful principle for object orientated programming.
This is built on the "resources" principle put forward at the inception of HTTP -- a standardized set of technologies which allow browsers to send and retrieve data from servers.
In short, it means there are certain conventions you should abide by to keep your app extensible.
Because Ruby/Rails is object orientated, everything you do in a well-tailored application should revolve around objects. Like resources, these allow you to create a system which is both flexible and extensible if done properly.
Thus, with your code, you have to remember that you're trying to add an image to the food items object. You should therefore be editing the food items object, updating it with the extra image; which the above code will help you achieve.
Like Badheka said: "why are you querying and just saving the #food_item?
Anyways, Notice that when you try to create food_images, you did:
food_item_params[:food_image]['avatar'].each do |a|
#food_image = #food_item.food_images.create!(:avatar=>a)
end
You were looping through the avatar in food_image attributes of food_item_params
However, if you check your food_item_params, you have:
def food_item_params
params.require(:food_items).permit(food_images_attributes: [:id, :food_item_id, :avatar])
end
With this, in all possibility, the structure of the data that this params is expecting will be something like:
{
food_items:
{
food_images_attributes:
{
id: "",
food_item_id: "",
avatar: ""
}
}
}
while what is being returned by your food_item_params will be as follow:
{
food_images_attributes:
{
id: "",
food_item_id: "",
avatar: ""
}
}
There are a number of issues with this:
1) The error you are getting: param is missing or the value is empty: food_items this suggests that the param that is coming in is not in the format specified above. the parent attribute food_items is missing, so that is the first part of your code that you need to rectify from inside the view where you are sending the avatar, or conform your food_item_params method to meet the structure of what is coming in through the params.
2) There is no food_item_params[:food_image]['avatar'] to loop on, since thee is no food_image in your params. What you have is food_image_attributes, so your loop will be on food_item_params[:food_image_attributes][:avatar]
3) Looping through the food_item_params[:food_image_attributes][:avatar] suggests that it is an array. If so, then the way you are permitting it is wrong too. You will have to specify that it is an array from inside the food_item_params method as follow:
def food_item_params
params.require(:food_items).permit(food_images_attributes: [:id, :food_item_id, avatar: []])
end
If however, as I suspect, what is coming through your param is as follow:
food_images_attributes:
{
id: "",
food_item_id: "",
avatar: []
}
Then your food_item_params method should be as follows:
def food_item_params
params.require(:food_images_attributes).permit(:id, :food_item_id, avatar: [])
end
I'll advise you check your params and paste what it looks like here.
And as a last word, check what you are sending from the view, and set your controller accordingly, or check what you want the controller to expect, and send same from the view.
You need to change food_item_params method :
From :
def food_item_params
params.require(:food_items).permit(food_images_attributes: [:id, :food_item_id, :avatar])
end
To :
def food_item_params
params.require(:food_item).permit(food_images_attributes: [:id, :food_item_id, :avatar])
end
Also, you can improve your code :
Add accepts_nested_attributes_for :food_images in food_item model, then make changes in controller's method
def add_image
#food_item = FoodItem.find(params[:id])
respond_to do |format|
if #food_item.update_attributes(food_item_params)
format.html { redirect_to #food_item, notice: 'Images uploaded successfully' }
else
format.html { redirect_to add_image_food_item_path}
end
end
end

Upload image through Paperclip without saving object

I'm working in Rails and I have two models, a prelaunch and an initiative. Basically I want a user to be able to create an initiative using the attributes of the prelaunch. Basically what I want to have happen is when a user visit's their prelaunch and is ready to turn it into an initiative, it brings them to a form that has their prelaunch information already populated and they can just add the additional info. I've managed to do this for every attribute so far except for the attached image, called :cover_image.
I think the problem is that I'm setting the initiative's cover_image to the prelaunch's cover_image on the new action of my controller, but because this is the new action and not create, I'm not saving the initiative yet. I think this means the cover_image isn't getting reuploaded yet, so #iniative.cover_image.url doesn't point to anything. It also doesn't appear to be prepopulating the file field of my form with anything.
I'm not entirely sure how feasible all of this is, but it's what the client asked for so I'm trying to make it work for them.
Here's my controller:
def new
#initiative = Initiative.new
populate_defaults(#initiative)
#initiative.build_location
3.times{ #initiative.rewards.build }
#initiative.user = current_user
if !params[:prelaunch_id].nil? && !params[:prelaunch_id].empty?
# if user is transferring a prelaunch, assign its attributes to the intiative
#prelaunch = Prelaunch.find(params[:prelaunch_id])
#initiative.assign_attributes(title: #prelaunch.title,
teaser: #prelaunch.teaser,
category: #prelaunch.category,
funding_goal: #prelaunch.funding_goal,
term: #prelaunch.campaign.term,
story: #prelaunch.story,
location: #prelaunch.campaign.location,
video_url: #prelaunch.video_url,
EIN: #prelaunch.campaign.EIN,
nonprofit: #prelaunch.nonprofit,
organization_name: #prelaunch.campaign.organization.name)
end
end
Edit:
Thanks to peterept's answer below I've managed to get the prelaunch cover_image into the form and into the create action of the initiatives controller. The problem now is that everything seems to work perfectly in the create action: the initiative gets the prelaunch's cover image, it saves without error, and it redirects to the show action.
UNFORTUNATELY, By the time it reaches the show action of the controller, #initiative.cover_image is set to the default again. I can't figure out what could possibly be happening between the successful create action and the show action.
Here are the create and show actions of the initiatives controller:
def create
if !params[:initiative][:prelaunch_id].nil? && !params[:initiative][:prelaunch_id].empty?
#prelaunch = Prelaunch.find(params[:initiative][:prelaunch_id]) # find the prelaunch if it exists
end
#initiative = Initiative.new(initiatives_params)
#initiative.user = current_user
begin
#payment_processor.create_account(#initiative)
if #initiative.save
# #prelaunch.destroy # destroy the prelaunch now that the user has created an initiative
flash[:alert] = "Your initiative will not be submitted until you review the initiative and then press 'Go Live' on the initiative page"
redirect_to initiative_path(#initiative)
else
flash[:alert] = "Initiative could not be saved: " + #initiative.errors.messages.to_s
render :new
end
rescue Exception => e
logger.error e.message
flash[:error] = "Unable to process request - #{e.message}"
render :new
end
end
def show
#initiative = Initiative.find(params[:id])
#other_initiatives = Initiative.approved.limit(3)
end
And here is the initiatives_params method from the same controller:
def initiatives_params
initiative_params = params.require(:initiative).permit(
:terms_accepted,
:title,
:teaser,
:term,
:category,
:funding_goal,
:funding_type,
:video_url,
:story,
:cover_image,
:nonprofit,
:EIN,
:role,
:send_receipt,
:organization_name,
:crop_x, :crop_y, :crop_h, :crop_w,
location_attributes: [:address],
rewards_attributes: [:id, :name, :description, :donation, :arrival_time, :availability, :_destroy, :estimated_value])
if #prelaunch.media.cover_image
initiative_params[:cover_image] = #prelaunch.media.cover_image
end
initiative_params
end
You can pass the Image URL and display it on the page.
The user can then override this by uploading a new image (as per normal).
In you're create action, if they have not supplied a new image, then set it to the one in the assocoiated prelaunch - you'd want to copy the original so it doesn't get replaced if they upload a new one. (If you don't know which was the prelaunch, you could pass the ID down to the page).
I was able to make it work by saving the Paperclip object only. This is my model:
class Grade < ActiveRecord::Base
has_attached_file :certificate
end
If I run the following:
#grade.certificate = new_file
#grade.certificate.save
It saves/overwrite the file, but don't update the Grade object.
Versions: ruby-2.3.8, Rails 4.2.11.3 and paperclip (4.3.6)

How to persist hasMany association in a single Ember.js form using Ember Data & Rails?

I'm having trouble determining the correct way to persist a hasMany association with a single form using Ember.js, Ember Data and Rails. A Client hasMany Projects. I have a new project form that has two fields: project name and client name. http://cl.ly/image/3z0P0R3M1t2u
I've tried to keep my create logic within the Ember.js ClientsController & ProjectsController, but will I need to move some of that to the submit action on my ProjectsNewView?
Update: I've updated my code after finding this issue. I'm getting closer, but the Rails ProjectsController is still not receiving the associated client_id. It is not a part of the params that the controller receives. It still feels like I'm probably not going about this the best way.
Models:
Rails
class Client < ActiveRecord::Base
attr_accessible :name
has_many :projects
accepts_nested_attributes_for :projects
validates :name, :presence => true
end
class Project < ActiveRecord::Base
attr_accessible :name, :client_id
belongs_to :client
validates :name, :presence => true
validates :client, :presence => true
end
Ember.js
App.Client = DS.Model.extend
name: DS.attr('string')
projects: DS.hasMany('App.Project', { embedded: true })
validate: ->
if #get('name') is null or #get('name') is ''
'Client requires a name.'
App.Project = DS.Model.extend
name: DS.attr('string')
client: DS.belongsTo('App.Client')
validate: ->
if #get('name') is `undefined` or #get('name') is null or #get('name') is ''
return 'Projects require a name.'
if #get('client') is `undefined` or #get('client') is null or #get('client') is ''
'Projects require a client.'
Controllers:
Rails
class Api::ClientsController < Api::BaseController
def create
#client = Client.find_or_create_by_name(params[:client][:name])
respond_to do |format|
if #client.save
format.json { render json: #client, status: :create }
else
format.json { render json: #client.errors, status: :unprocessable_entry }
end
end
end
end
class Api::ProjectsController < Api::BaseController
def create
#project = Project.new(params[:project])
respond_to do |format|
if #project.save
format.json { render json: #project, status: :created, location: #project }
else
format.json { render json: #project.errors, status: :unprocessable_entry }
end
end
end
end
Ember.js
App.ClientsController = Em.ArrayController.extend
createClient: (data) ->
#transaction = App.store.transaction()
client = #transaction.createRecord(App.Client, data)
project_data = data.projects_attributes[0]
client.get('projects').createRecord(project_data)
validation_errors = client.validate()
if validation_errors
App.displayError validation_errors
client.destroy()
else
#transaction.commit()
App.ProjectsController = Em.ArrayController.extend
createProject: (data) ->
#transaction = App.store.transaction()
project = #transaction.createRecord(App.Project, data)
validation_errors = project.validate()
if validation_errors
App.displayError validation_errors
project.destroy()
else
#transaction.commit()
App.get('router').transitionTo('projects')
Views:
Ember.js
App.ProjectsNewView = Em.View.extend
classNames: ['form row']
tagName: 'form'
templateName: 'projects/new'
init: ->
#_super()
submit: (event) ->
event.preventDefault()
client = {}
client.name = #get('client')
project = {}
project.name = #get('name')
client.projects_attributes = []
client.projects_attributes.push project
App.router.clientsController.createClient(client)
#David i am no expert but i was looking at your question again and i think to get the associated client_id, you will have to call toJSON method on the RestAdapter and pass it:
{associations: true}
The links below explain how to do that:
https://stackoverflow.com/questions/10184213/is-there-a-way-to-post-embedded-objects-back-to-the-api-with-ember-data
Ember-data embedded objects stored as separate objects
Emberjs - unable to query for embedded model or association
Update
Before i answer the question of where in the code to use toJSON, i want to go one step back to something i just observed with the design of your code.
You are trying to create a parentRecord within child record because Client is the parent while project is the child. If you observe well, issue-115 that you linked to, categorically says that the parentRecord store would be used to create the record. Also if you examine the test in evenutual pull-request that was merged in respect to issue-115, you will see it again stated as Create a child record within the parentRecord.
But you are doing the reverse, that is creating parentRecord within child record. You need to reverse that.
Also, on the rails side, in the Client Model, you called accepts_nested_attributes_for :projects, which just like emberjs does only allows you to create project records from Client and not vice-versa. All the examples of how to use accepts_nested_attributes_for in the rails guide, railscasts and this on nested attributes, show the fields for the child model created from the forms in the parent model.
So both rails and emberjs are in this use case to create parentRecord from child record. This could be why you having the client-id issue. So reverse your design and try it again. If it still doesn't send the client-Id. Then you should called toJSON on the Client Model which is the parent association as shown in the first link from the initial links i posted recommending you call {associations: true} on toJson in the restAdapter.
App.Client
##
toJSON: (options={}) ->
options['associations'] = true
#_super(options)
I hope this helps. Kindly comment back on what worked or did not work, so that others experiencing similar issues, who find this post can know where to start from.
Finally, it wouldn't hurt to create your rails api with active_model_serializer gem, which is ember-core team's recommended gem for creating rails api for ember-data and the RestAdapter. That way you can sideload and embed associations both from the ember side and the rails side.

Resources