accepts_nested_attributes_for alias? - ruby-on-rails

In Topic model:
class Topic < ActiveRecord::Base
has_many :choices, :dependent => :destroy
accepts_nested_attributes_for :choices
attr_accessible :title, :choices
end
During a POST create, the params submitted is :choices, instead of :choices_attributes expected by Rails, and giving an error:
ActiveRecord::AssociationTypeMismatch (Choice(#70365943501680) expected,
got ActiveSupport::HashWithIndifferentAccess(#70365951899600)):
Is there a way to config accepts_nested_attributes_for to accept params passing as choices instead of choices_attributes in a JSON call?
Currently, I did the attributes creation in the controller (which seems not to be an elegant solution):
def create
choices = params[:topic].delete(:choices)
#topic = Topic.new(params[:topic])
if choices
choices.each do |choice|
#topic.choices.build(choice)
end
end
if #topic.save
render json: #topic, status: :created, location: #topic
else
render json: #topic.errors, status: :unprocessable_entity
end
end

This is an older question, but I just ran into the same problem. Is there any other way around this? It looks like that "_attributes" string is hardcoded in the nested_attributes.rb code (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L337).
Assigning "choices_attributes" to a property when submitting a form is fine, but what if it's being used for an API. In that case it just doesn't make sense.
Does anyone have a way around this or an alternative when passing JSON for an API?
Thanks.
UPDATE:
Well, since I haven't heard any updates on this I'm going to show how I'm getting around this right now. Being new to Rails, I'm open to suggestions, but this is the only way I can figure it out at the moment.
I created an adjust_for_nested_attributes method in my API base_controller.rb
def adjust_for_nested_attributes(attrs)
Array(attrs).each do |param|
if params[param].present?
params["#{param}_attributes"] = params[param]
params.delete(param)
end
end
end
This method basically converts any attributes that are passed in to #{attr}_attributes so that it works with accepts_nested_attributes_for.
Then in each controller that needs this functionality I added a before_action like so
before_action only: [:create] do
adjust_for_nested_attributes(:choices)
end
Right now I'm only worried about creation, but if you needed it for update you could add that into the 'only' clause of the before_action.

You can create method choices= in model as
def choices=(params)
self.choices_attributes = params
end
But you'll break your setter for choices association.
The best way is to modify your form to return choices_attributes instead choices

# Adds support for creating choices associations via `choices=value`
# This is in addition to `choices_attributes=value` method provided by
# `accepts_nested_attributes_for :choices`
def choices=(value)
value.is_a?(Array) && value.first.is_a?(Hash) ? (self.choices_attributes = value) : super
end

Related

Deleting a single ActiveStorage attachment out of a collection via an API call

EDIT: TLDR: This boils down to serializing the attachments. See my response.
I can see two ways to achieve this:
(1) Serialize the attachments (with id and url attributes), thus providing an id to the FE that they can use to DELETE /attachments/:id which would then call ActiveStorage::Attachment.find(:id).purge. The problem is with serializing as attachments do not have built-in models. I tried creating an ActiveStorageAttachment model for the active_storage_attachments table but could not get the url for the attachment as the Rails.application.routes.url_helpers.url_for(#object) requires an ActiveStorage::Attachment object not an ActiveStorageAttachment object.
(2) Another option would be to have a DELETE /attachments/:attachment_url endpoint. For this to work, I'd need to get the ActiveStorage::Attachment object based on the url, in order to call purge on it. Not sure if that is possible?
I'd much prefer the first solution, it feels cleaner and more adaptable. Any help with either approach would be much appreciated!
In the end, I managed to serialize the attached images (option(1) above) using the jsonapi-rb gem.
In the controller, I include: :images and pass in the attachment type using the expose option:
class PostsController < ApplicationController
...
def show
respond_to do |format|
format.html
format.json { render jsonapi: #post, include: :images, expose: {attachment_type: "images"} }
end
end
end
ActiveSupport gives you the post.images_attachments method for free:
class SerializablePost < JSONAPI::Serializable::Resource
type 'posts'
...
has_many :images do
#object.images_attachments
end
end
I created an attachment serializer:
class SerializableAttachment < JSONAPI::Serializable::Resource
include Rails.application.routes.url_helpers
type do
#attachment_type
end
attribute :id
attribute :url do
url_for(#object)
end
end
I needed to tell jsonapi to use this serializer for all attachments:
class ApplicationController < ActionController::Base
...
def jsonapi_class
super.merge(
'ActiveStorage::Attachment': SerializableAttachment
)
end
end
Now I can implement DELETE /attachments/:id.

Creating child instances at the same time as their parent

I'm trying to create a polling system:
# models
class Poll < ActiveRecord::Base
has_many :answers
end
class Answer < ActiveRecord::Base
belongs_to :poll
end
# routes
resources :polls do
resources :answers
end
Poll having one string attribute, question, and Answer having one string attribute, answer, and one integer, votes.
I'd like to have it so when a user creates a Poll, a form for a nested Answer instance is displayed. Then have a button with an AJAX call for another form (allowing up to say 10) so multiple answers can be added.
I know how to do the AJAX part, but I don't know how to add child instances to a parent that doesn't exist yet.
The only way I can think of is to use a text_field_tag to pass it through params, then manually create it in the PollsController create method. Sort of like this:
def create
#poll = #poll.create(poll_params)
if #poll.save
if params[:poll_answer1].present?
#poll.answers.create!(answer: params[:poll_answer1])
end
if params[:poll_answer2].present?
#poll.answers.create!(answer: params[:poll_answer2])
end
flash[:notice] = "Poll created."
redirect_to #poll
else
flash[:error] = "Poll could not be created."
render :new
end
end
But it seems like there should be a better way. I'm not opposed to scrapping the entire setup, and maybe placing the questions in an array attribute on Poll instead. But then there's the issue of counting votes.

Rails undefined method

Why is this undefined? Does it have something to do with the #current_user?
I'm trying to create tasks for my challenges. And the created task should get /achievements. However, I get a GET 500 error.
This is the error I get:
NoMethodError at /achievements
==============================
> undefined method `achievements' for #<User:0x00000105140dd8>
app/controllers/achievements_controller.rb, line 5
--------------------------------------------------
``` ruby
1 class AchievementsController < ApplicationController
2
3
4 def index
> 5 #achievements = #current_user.achievements
6 render :json => #achievements
7 end
8
9 def new 10 #achievement = Achievement.new
This is my code in my controller
class AchievementsController < ApplicationController
def index
#achievements = #current_user.achievements
render :json => #achievements
end
def new
#achievement = Achievement.new
render :json => #achievement
end
#create a new achievment and add it to the current user
#check then set the acheivments pub challenge id to the current pub challenge
def create
#achievement = Achievement.new achievement_params
#achievement.user = #current_user.id
#achievement.pub_challenge = params[:id]
if #achievement.save
# render :json => #achievement #{ status: 'ok'}
else
render :json => {:errors => #achievement.errors}
end
end
def show
#achievement = Achievement.find params[:id]
render :json => #achievement
end
def destroy
#achievement = Achievement.find params[:id]
#achievement.destroy
end
private
def achievement_params
params.require(:achievement).permit(:pub_challenges)
end
end
You are missing the has_many :achievements relation in your User model.
You'll need to create the ActiveRecord associations you require:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :achievements
end
#app/models/achievement.rb
class Achievement < ActiveRecord::Base
belongs_to :user
end
This will give you the ability to call the achievements method on any User objects you have.
Error
The error you have is described as such:
undefined method `achievements' for #<User:0x00000105140dd8>
This basically means that you're trying to call an undefined method on a User object. Might sound simple, but really, most people don't understand it.
To explain properly, you have to remember that Rails, by virtue of being built on Ruby is object orientated. This means that everything you do in Rails should be structured around objects - which are defined in your Models:
This means that each time you call an object, you're actually above to invoke a series of "methods" which will give you the ability to either manipulate the object itself, or any of the associated functionality it has.
The problem you have is that your User object doesn't have the achievements method. Whilst you could simply do the following to fix the issue, because it's Rails, you'll need to populate the record with associative data:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :achievements #-> what you need
def achievements
#this will also fix the error you see, although it's fundamentally incorrect
end
end
Something that helped me with this type of error was that the database table was missing the relevant column. Adding the required column to the database fixed the issue.

How can I capture an instance generically?

I'm using Rails 3.2.19 and Ruby 2.1.2. I've been googling around trying to figure this out, but perhaps I'm not searching for the right thing. Anyway, I'll try and be as concise as possible.
I have a few different models that all have a name attribute. In my views I want to somehow be able to access that name attribute regardless of the instance name passed into the view. Currently my various controllers create instances of their respective models. For instance:
class PagesController < ApplicationController
def show
#page = Page.find(params[:id])
respond_to do |format|
format.html
end
end
end
-
class ProductsController < ApplicationController
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html
end
end
end
While I understand I could simply re-name the instances something generic, I was wondering if there was some way of accessing any/all instances while maintaining unambiguous instance names.
Basically something like this:
page.html.haml
%h1= resources[0].name #equates to #page.name
%h2= #page.some_other_attribute
or
product.html.haml
%h1= resources[0].name #equates to #product.name
%h2= #product.price
Where in each of the above resources[0] would be either #page or #product
You will have to define a route with an additional resource_type parameter to a generic controller or otherwise just include the resource_type into the url query parameter
/resources/product/17
or
/resources/17?resource_type=product
This will allow you to do the following in the controller
class ResourcesController < ApplicationController
def show
#resource = find_resource(params)
respond_to do |format|
format.html
end
end
private
def find_resource(params)
resource_klass = {
product: Product,
page: Page
}[params[:resource_type]]
resource_klass.find(params[:id])
end
end
Another Option would be to introduce another ResourceType Entity and define a polymorphic :has_one :belongs_to association to the actual resource entity (product, page). Then always search for ResourceTypes and load the polymorphic resource entity
class ResourceType < ActiveRecord::Base
belongs_to :resource, polymorphic: true
end
class Product < ActiveRecord::Base
has_one :resource_type, as: :resource
end
class Page < ActiveRecord::Base
has_one :resource_type, as: :resource
end
product_resource_type = ResourceType.create(...)
product = Product.create(resource_type: product_resource_type)
page_resource_type = ResourceType.create(...)
page = Page.create(resource_type: page_resource_type)
ResourceType.find(product_resource_type.id).resource
=> product
ResourceType.find(page_resource_type.id).resource
=> page
I figured this out after discovering instance_variables and instance_variables_get
Those methods will return all instance variables being passed into the view. From there I discovered that the :#_assigns instance variable contained the instances that I was looking for. So I iterated over them to find if any had the name attribute.
- instance_variable_get(:#_assigns).each do |var|
- if var[1].respond_to?("name")
%h1= var[1].name
There is probably a better way of accomplishing this, so if anyone has any opinions, they are welcome.

Any way around putting hidden field in forms for resources with belongs_to association

I'm learning Rails by writing simple TODO tasks aplication.
Two models are:
class List < ActiveRecord::Base
has_many :tasks, :dependent => :destroy
# ...
end
class Task < ActiveRecord::Base
belongs_to :list
# ...
end
Tasks are routed as a nested resources under Lists. So when a new Task is created by user a POST message is sent to /lists/:list_id/tasks. So far in Tasks#new view's form there is
f.hidden_field :list_id, :value => params[:list_id]
but it's a terrible solution, because anyone can change value of that hidden field.
What is the convention here? Should I put something like
#task.list_id = params[:list_id]
in Tasks#create action and get rid of the hidden field, or maybe
#task = List.find(params[:list_id]).tasks.new(params[:task])
if #task.save
# ...
end
or there is even a better way I don't know about?
Edit:
Yeah, well there was similar question and its answer is pretty much covering my question. If you have different one please post it.
You're right - that would be horrible. No need for hidden fields. Something like the following.
In your TasksController:
def new
#list = List.find(params[:list_id])
#task = #list.tasks.build
end
def create
#list = List.find(params[:list_id])
#task = #list.tasks.new(params[:task])
# etc
end
In your Task#new view:
<% form_for [#list, #task] ... %>
...
<% end %>
If you are concerned about security (like one user creating to-dos in another user's lists - and I assume you are, because you didn't want to use a hidden field stating that anyone can change value of that hidden field), I don't see how #bjg solution is any better then yours, since you're getting #list from params anyways, and anybody can manipulate params on the browser (changing the URL to post to is as easy as changing the hidden field value).
One common way to solve this without having to implement a more complex permission solution is to just use current_user association's, like this:
def new
#list = current_user.lists.where(id: params[:list_id]).take
#task = #list.tasks.build
end
def create
#list = current_user.lists.where(id: params[:list_id]).take
#task = #list.tasks.new(params[:task])
# etc
end
This way, no matter what is the value of params[:list_id] (it could have been manipulated by the user), you can rest assured the #task will end up on that user's account, since #list will only find a record that belongs to current_user.
You can evolve this in a real-world app by returning an error message if #list is not found.

Resources