Separating multiple models in Rails form submission - ruby-on-rails

Specifically, I have a form for creating a User. One of the fields is Group, which is a separate model. During the User#create action I call Group.find_or_create_by_name to check and see if the Group already exists by pulling out params[:user][:group], and create the Group if it doesn't exist.
But, when I create the User, I can't pass params[:user], because params[:user][:group] is not a group, it's a String. This would be a lot easier if I could supply params[:user] and params[:group] to my controller, instead of everything bundled under a single variable, but I don't know how to do that.
Relevant code:
User#create
#group = Group.find_or_create_by_name(params[:user][:group])
#group.save!
#user = #group.users.build(params[:user])
Partial User Schema
create_table "users", :force => true do |t|
t.string "name", :default => "", :null => false
t.string "email", :null => false
t.integer "group_id"
Partial Group Schema
create_table "groups", :force => true do |t|
t.string "name"
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
Params Dump from form submission
{"commit"=>"Register",
"authenticity_token"=>"x1KgPdpJop5H2NldsPtk0+mBDtrmpM/oNABOxjpabIU=",
"utf8"=>"✓",
"user"=>{"name"=>"aName",
"group"=>"aGroupName",
"password_confirmation"=>"[FILTERED]",
"password"=>"[FILTERED]",
"email"=>"anEmail"}}
The best explanation I've run across is this question.
I don't really want to start rearranging the params has inside of my controller as that really makes me queasy, and seems like terrible programming. Which really makes me question whether I've got a bigger problem.
If it goes against Rails conventions to do this, what then is the best way to do it, and why should I not?

I don't think the proper way to do this here is to build the user off the group. That'd be appropriate if you were in an action of the GroupsController that, for example, added new users to a group. I think the best approach here would be to do the following:
#group = Group.find_or_create_by_name(params[:user][:group])
#group.save!
#user = User.new(params[:user])
#user.group_id = #group.id
#user.save
Since you're in the new action of the UsersController, it seems more fitting to be creating a new user instead of building it off an association and then adding that user to the group that either already existed or was just created.
Does that make sense?

So, I'm going to spell out everything I've learned, in case other people are having trouble understanding this like I did. Also, if I get anything wrong here, please correct me.
If you're using a form that creates multiple model instances at once (preferably associated ones), you first need to use the helper accepts_nested_attributes_for in your model definition (probably right underneath your declared associations). The reason for this is it creates a setter method that knows how to write that type of associated model. (Note: you can also define this method yourself in your main model). Once you've done that you can nest a fields_for inside of a form_for, and Rails will know how to make the proper assignments.
I initially thought that accepts_nested_attributes_for was referring to nested resources, which is definitely not the case. If you're looking for more information, refer to section 11.8.3 (pp 343-347) of The Rails 3 Way.

Related

In Rails where do I put the error detection logic to validate user input from a form?

I'm writing a minimal rails app as a way to learn a bit more about rails.
The app is going to track stuff (books to start with). So I need a "Location" to identify where a given item is.
create_table "locations", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "located_at"
t.integer "sort"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_locations_on_name", unique: true
end
"sort" is irrelevant for this question.
In the form to create a new location the user gets to enter a name (say "X") and optionally say where "X" itself is located (say "Y").
So to create the location in the controller I will do something like
#location = Location.new(location_params)
But I need to turn "Y" into a location.id for "Y". In addition, if "Y" doesn't exist, I need to raise some kind of error.
What I have now is a virtual variable located_at_text and in the controller I do this:
modified_location_params = location_params
located_at_text = modified_location_params["located_at_text"]
located = nil
located = Location.find_by_name(located_at_text) unless located_at_text.nil? or located_at_text.strip.empty?
modified_location_params["located_at"] = located.nil? ? 0 : located.id
#location = Location.new(modified_location_params)
(I don't know why I can't fiddle with location_params ... but that would be a different question ... I'll worry about that once I know where I'm best off putting my code. Also my app does't mind a 0 for location.id).
Various tutorials suggest that some of this logic should be in the model, but various examples also do similar work in the controller.
Which is the "rails way"?
I made some assumptions because not all of the example code made sense to me, but the basic outline would be:
Use ActiveRecord callbacks instead of checking input in the controller
Make sure relationships are correctly set so you can build objects from them
Use first_or_create to clean up the parent lookup/creation
In my experience, manipulating input params is a code smell. Sometimes you have to do it, but usually it's telling you something is incorrectly designed.
Some of the param manipulation you can accomplish with ActiveRecord callbacks. One note, it looks like located_at_text is not part of a model, you will need to check this in the controller or add an attr_reader to the model (but I don't think you should, this is another code smell).
class Location < ApplicationRecord
validates :name, presence: true
end
Set up the relationships in the model. This will let you build out the related records.
class Location < ApplicationRecord
belongs_to :located_at
validates :name, presence: true
end
class LocatedAt < ApplicationRecord
has_many :locations
end
Normally, this setup would carry the ID for LocatedAt in the url (e.g. .../#{located_at_id}/location/new), but this isn't required if you are going to explicitly pass a located_at id which it looks like you are.
This is outside the scope of your question, but if you are breaking from the url pattern where parent ids are passed in the url, it would be very useful for your users to give them some kind of auto-complete search or a UI element that lets them know explicitly that they are creating a new LocatedAt record. It will also make your life easier because you can pass in the id resulting from the search or a trigger param to create the new record first.
Finally, use first_or_create to check that located_at exists (and if not create it). This assumes that there is only one field on LocatedAt and it is called name.
Note: I changed the model from Location since it didn't make sense to me that Location was going to look for itself in the example.
#located_at = LocatedAt
.where(name: params[:form_name][:located_at_text])
.first_or_create
#location = #located_at.locations.new(location_params)

Ruby on Rails 5 strong parameters

I'm having some troubles with a project I'm working on. Be warned I consider myself very much a beginner/novice at all this still :)
To keep things short and sweet, I'm using Rails & active admin to build up an admin interface where i can perform CRUD operations on my database models, which is all working great. However I recently decided I wanted to add another field to one of my models, a "description" field, so generated a migration, ran rake db:migrate and updated my list of allowed params in my controller & active admin resource.
My problem is data is not saved for this new "description" field - wether its via creating a new entry or updating an existing one. I can see the output in the terminal confirms it is being filtered out by strong params; returning Unpermitted parameter: :Description However i am under the impression i have set up my strong params correctly, so I'm unsure if i have set up my permit params properly or what else i can do.
Using Rails 5.1.0 & will post code below.
class CellsController < InheritedResources::Base
def index
end
private
def cell_params
params.require(:cell).permit(:name, :description)
end
end
#database schema for my cell model
create_table "cells", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "Description"
end
#Active Admin resource
ActiveAdmin.register Cell do
permit_params :name, :description
end
Again, greatly appreciate any help as I'm sure I've overlooked something, happy to provide any other information that is required :)
Thankyou!
To me it looks like the description param is not accepted because the model only has a Description column (with a capitalised D). To fix that, either change each params.permit(:description) to params.permit(:Description) or just rename the column inside a new migration:
def change
rename_column :cells, :Description, :description
end
I recommend renaming the column as it will avoid any trouble with the column in the future.

Ruby on Rails: wrong number of arguments (0 for 1) even if provided

We inherited a rails project (the whole thing is based on elasticsearch). Thus, the application lists all the documents meeting the provided search criteria by a user. Imagine a facet or so, once you've selected i.e. a specific range of dates it gives you the documents that were created at that time.
We have been currently working on expanding its functionality accordingly:
Our priority is to utilize the elastic query such that all
matched documents by that query could be multiply altered (in
our case, we've been trying to hide all the documents). Then we want to send the query to appropriate rake task that would take care of it.
After the button responsible for mass hiding is clicked the controller's create method gets called, thereby creating a mass_hiding record in database (for convenience we keep a track of hidings which would allow us to revert the mass action afterwards).
def create
mh = current_user.mass_hidings.build(params[:mass_hiding])
mh.save!
mass_hide(mh.query_params)
redirect_to search_documents_path(mass_hide.search_parameters)
end
def mass_hide(query)
search = factic.create_restrictions_search(MultiJson.load(query.to_json))
Resque.enqueue(Document::Jobs::HideDocuments, nil, search.to_scrollable.build_query)
end
However, the line 3 of the create method mass_hide(mh.query_params) triggers the following exception once it gets executed:
ArgumentError in Admin::MassHidingsController#create
wrong number of arguments (0 for 1)
Full trace can be found here.
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"6mZvRcp4HJuoBWMRToA2gPec9Wv8T82hiTJQ/STf1j/sDhQ+16mBW3QkRmhqlJIHKR0kvX/kqwQh205hp6RuDg==",
"mass_hiding"=>{"serialized_query_params"=>"{}",
"description"=>"test"},
"commit"=>"Hide documents"}
Further, here is the schema representation of mass_hiding as well as the appropriate model:
schema.rb
create_table "mass_hidings", force: :cascade do |t|
t.integer "user_id"
t.text "serialized_query_params"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
mass_hiding.rb
class MassHiding < ActiveRecord::Base
attr_accessible :serialized_query_params, :description
belongs_to :user
def query_params=(params)
self.serialized_query_params = Oj.dump(params)
end
def query_params
Oj.load(self.serialized_query_params).with_indifferent_access
end
def self.find_or_initialize_by_user_and_query_params(user, query_params)
self.find_or_initialize_by_user_id_and_serialized_query_params(user.id, Oj.dump(query_params))
end
end
Thank you for your help in advance.
The problem is that you have defined this helper function:
def mass_hide(query)
But you are calling it with zero arguments.
First you call it with one argument, when you say mass_hide(mh.query_params). That's fine.
But then on the next line you also say mass_hide.search_parameters. To Ruby that means mass_hide().search_parameters. Perhaps you meant to write mh.search_parameters? I'm not sure. But mass_hide is the method that wants 1 param, and that's the callsite where you pass it 0.
I hope that helps!

Subscription based restrictions for rails queries

I have a rather complex issue that I could some assistance with, as all implementations I've been trying are flawed (Rails 4.2.1).
Users have access to devices:
create_table "devices", force: :cascade do |t|
t.string "device_guid"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_ids"
end
and devices have many data entries:
create_table "datas", force: :cascade do |t|
t.string "device_id"
t.datetime "start_time"
t.datetime "end_time"
end
We want to restrict all queries to the data based on Subscriptions.
create_table "subscriptions", force: :cascade do |t|
t.integer "device_id"
t.integer "user_id"
t.datetime "start_date"
t.datetime "end_date"
end
So effectively, data should not be accessed unless Subscriptions allows it.
I wanted to have this restriction done in one place (not by individual method to access it), so other people writing code still deal with the subscription restriction. I don't want to use a 3rd party gem as the data is very sensitive.
I thought the most obvious way to do this would be set a default_scope on the Data model, but I ran into several problems:
1) You can't access #user from the Data model. I got around this with an ugly hack to access User.current from the model.
2) I can't use current_user since we have admins that pose as other users, and need to see the data as they would. Used the same workaround as #1
3) There can be multiple subscriptions with different dates for the same user and device. There isn't any way to do an ActiveRecord Query that I know of that can account for two separate date ranges (I got around this by using arel_table).
4) This makes all of the methods written that query the data to fail when I'm in the rails console (and theoretically through an API interface). I got around this by ignoring the default scope if there is no User.current, but obviously that's a security concern.
This is my current implementation (in psuedocode):
query = []
Subscription.each do |s|
if (User.current.id == s.user_id)
temp = Data.all.where(data[:start_time] >= s.start_date).where(data[:end_time] <= s.end_date).where(data[:device_id] == Device.find_by(id: s.device_id).device_guid)
end
end
query += temp
end
default_scope { query }
I put this code just directly into the model. I didn't think it would work, but it does... sometimes. It acts oddly, sometimes completely restricting a user when it shouldn't, although it hasn't (yet) given somebody access to data they should not have.
I'm looking to redesign this from the ground up, and do it the best way. I'm open to any suggestions (except 3rd-party, sorry) on how best to approach it. I don't mind if it takes a lot of work to implement, I'd rather do it right this time. Any account for expandability (such as if we implemented an API for remote queries) would be a great feature as well, though we don't currently need it.
Just to reiterate, I insist that this restriction be "automatic" to all queries, so that if a front coder for instance writes:
<%= #user.device.find(1).data.all.count %>
in a view, it won't show any data the user doesn't have a subscription for.
Thanks for any suggestions. Sorry for the long wall of text, this has been a rather perplexing problem.

Rails routing for link tables without primary key?

I have a link table which looks like this:
create_table "links", :id => false, :force => true do |t|
t.integer model1_id
t.integer model2_id
t.string someotherinfo
end
I'm currently defining routes like this:
match '/links/:model1_id/:model2_id/' => buggable_links#validate
It seems like I ought to be able to do something more like resources rather than writing out all the match statements. What's the right way of having rails generate resource routes on models which do not have a single primary key, such that URLs contain two IDs?
N.B. I'm aware that one possible answer is 'just add an autoincrementing pk'. The pros and cons of that are discussed in this question, but for the purposes of this question let's assume I want to leave my DB schema as it is.
This is the correct way to do it.
The only thing you should add is in the resources of buggable_links add the validate function as a get method.

Resources