Only allow several several values for has_many association - ruby-on-rails

I have a homepage for my sports club wich consists of several departments.
At the moment the authorization system is realized by using cancan.
Each department can have multiple users and each user can belong to multiple departments:
#department.rb
has_and_belongs_to_many :users
\user.rb
has_and_belongs_to_many :departments
This works very well. But I want to have a possibility to administrate this association in the User form. There I have a group of checkboxes for each department. This is realized by this line (using simple_form):
<%= f.association :departments,
:as => :check_boxes,
:collection => Department.specific.order('name' => :asc),
:label_method => :name,
:value_method => :id %>
Now I only want to allow to change several values. On client side I can achieve this by simply hiding or disabling some checkboxes. But this is not save on server when I do not check it again.
The checkbox values will be transmitted by an array of ids. There is a huge potential to manipulate ids in this array.
On the server side I would have to check if the current user has the permission to assign a user to the departments. When he has no rights I need to leave this association unchanged.
Is there an elegant way to achieve this?
I am using Rails 4.0 with strong parameters.
Thanks

So your question is basically about server-side validation -- checking if a user can edit an association on the server side?
I'd look at setting dynamic strong params to start with, with a view to integrate into the model if it works:
#app/controllers/your_controller.rb
def update
#item = Model.find(params[:id])
#params = (can? :update, #item) ? "" : "departments: []"
end
private
def controller_params
params.require(:controller).permit(:attributes, #params.to_sym)
end

Related

Rails accepts_nested_attributes_for with belongs_to. Why I can't set id?

I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I try to set rubric id to new item by rubric_attributes.
describe 'create material' do
it 'should set rubric: :id' do
# prepare
item = FactoryBot.build(:material)
rubric = FactoryBot.create(:rubric)
# action
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(rubric.id)
end
end
But I have an error:
Failure/Error:
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
ActiveRecord::RecordNotFound:
Couldn't find Rubric with ID=1 for Material with ID=1
And I have the same error with updating a material.
Is it a predictable behavior of accepts_nested_attributes_for, and I can't use rubric_attributes for setting existed rubric id?
Docs say:
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
It suggest that if you pass id in nested attributes, it's treated as an existing record that should be updated.
You most likely don't need accepts_nested_attributes_for in the first place.
If you want the user to be able to select records with a select you don't actually need to do anything besides create a select and whitelist the material_id attribute:
<%= form_for(#material) do |f| %>
<div class="field">
<%= f.label :rubic_id %>
<%= f.collection_select :rubic_id, Rubic.all :id, :name %>
</div>
<%= f.submit %>
<% end %>
The select will create an array in the params.
class MaterialsController
# POST /materials
def create
#material = Material.new(material_params)
if #material.save
redirect_to #material
else
render :new
end
end
private
def material_params
params.require(:material)
.permit(:foo, :bar, material_ids: [])
end
end
accepts_nested_attributes_for is really intended for the case where you need to create / edit nested resources in the same request. The only reason you would use it here is:
The user should be able to create the material in the same form.
You have a join table with additional attributes (like quantity for example) which you want the user to be able to set.
You can still do 1. together with the select above, but you can't use accepts_nested_attributes_for to set a simple belongs_to association. Nor would you want to either as its like using a rocket to beat in a nail.
Just leaving this in case somebody else may have a problem as I did, populating nested children records in a Rails backend via an API, but using hash_ids via friendly_id.
Came about this when trying to PATCH Rails records via an API. First setup was to mirror the Rails way of sending the record values in nested form fashion. Meaning, I've purposefully built the params hash I was sending from the frontend over to a Rails backend like in a typical nested form transmission:
{ "children": {
"0": {
"name": "foo",
"category_id": "1",
"hash_id": "HDJPQT"
}
}
accepts_nested_attributes_for needs id to PATCH records. Otherwise it is going to create a new record altogether. Which i did not want in my scenario. I was sending over hash_id and therefore creating new records unintentionally.
Solution
For the time being I am not replicating a nested form value hash anymore to send to the Rails backend anymore. Instead I am simply updating children records separately in a chained fetch query from the Javascript frontend.
Note:
If you want to keep sending a mirrored nested form array of hashes, there could be a way to change the primary key of the database table to hash_id or UUID, depending on your needs. Have not yet tested this solution.

Rails 5 - exclude specific instances from edit path on parent controller

I have models in my Rails 5 app for User, Proposal and Potential.
Users create Proposals which they themselves and others can then create comments on.
The associations between models are:
User
has_many :proposals, dependent: :destroy
has_many :potentials
Proposal
belongs_to :user
has_many :potentials, inverse_of: :proposal
accepts_nested_attributes_for :potentials, reject_if: :all_blank, allow_destroy: true
Potential
belongs_to :proposal, inverse_of: :potentials
belongs_to :user
In my routes file, I have two resources for potentials. I'm not sure if I've gone off piste with this bit- I cant find an example of how to do this otherwise. I have both:
resources :potentials
as well as:
resources :proposals do
resources :potentials
The reason I have done this is so that when the user is creating a proposal, they can use a nested fields form to add potential attributes. When another user sees that proposal, they get a new form to add a :potential set of attributes (they don't do it via the proposal form).
In my potentials view folder, I have views for new which renders form for as well as potential_fields_for which is incorporated in my proposals form (only the proposal creator can use the nested fields).
The new/render form has:
<%= simple_form_for [ #proposal, #potential ] do |f| %>
f
The proposal form has:
<%= f.simple_fields_for :potentials do |f| %>
<%= f.error_notification %>
<%= render 'potentials/potential_fields', f: f %>
<% end %>
<%= link_to_add_association 'Add another novel aspect', f, :potentials, partial: 'potentials/potential_fields' %>
</div>
In my proposals controller, I'm trying to find a way to exclude 3rd party created :potentials from the fields displayed in the proposal form.
def edit
#proposal.potentials_build unless #proposal.potentials || #proposal.potential.user_id != current_user.id
I don't want the proposal creator to be able to edit those fields from the proposal form but even if I don't touch them, the user id on the 3rd party potential gets updated to the proposal creator id when I update the proposal form (without updating those specific 3rd party potential attributes).
I tried to change the edit action in the proposals controller be excluding potentials created by a user that is not the current user id. Only the proposal creator can edit the proposal, so I expect that this will exclude instances of proposal.potential that have a user id other than the proposal.user_id.
That doesnt work.
Is there a way that I can limit the proposal#edit action to only those potential instances that are not created by the proposal creator?
TARYN'S SUGGESTION
I tried to adopt Taryn's thoughts about this.
In my proposal.rb, I made 2 scopes:
scope :owner_potentials, ->{ where(user_id: potential.user_id ) }
scope :third_party_potentials, ->{ where(user_id: != potential.user_id) }
I am not confident that this is the correct way to write a scope. Although I cant find a reference to how to write them, on previous occasions when I have tried to learn how to compose them, I have received advice to set them out in this format:
scope :owner_potentials, ->(user){ where(user_id: potential.user_id ) }
scope :third_party_potentials, ->(user){ where(user_id: != potential.user_id) }
I tried this way as well, but I get the same error as I do if I don't include "(user)". I don't understand what that inclusion does or should do.
In my proposal controller, edit action, I have then tried each of the following:
# #proposal.potentials_build unless #proposal.potentials || #proposal.potential.user_id != current_user.id
# #proposal.owner_potentials.build unless #proposal.owner_potentials
##potentials = #proposal.owner_potentials || #proposal.owner_potentials.build
The first one was my original attempt. It didnt work and prompted me to write this post.
The second one is the way I think it should be written.
The third is just incorporating Taryn's idea directly to see if that's how it should be written (although I think that was more of general way of describing what to do).
None of these work. In the case of the 2nd version of this attempt, I get an error that says:
NoMethodError at /proposals/17/edit
undefined method `owner_potentials' for #<Proposal:0x007f84f6ca1700>
I think the reason why this isnt working is that the scope is run on the class as a table, not the specific instance to be edited. I think this post explains that.
Proposals have many potentials, so the idea is to check all of the potentials belonging to the specific proposal to see whether any of those potentials have the user id that is the same as the user id on the proposal instance. How can I do that?
I cant write:
Proposal.owner_potentials.build unless Proposal.owner_potentials
in the proposal controller edit action because the set_proposal method is picking out the correct proposal for edit to apply on.
Can I use a scope in a controller edit action where there is a has_many relationship that is being tested by the scope?
NEXT ATTEMPT
My next thought is to try to define the scopes in the Potential model so that the scope can run on the class.
I tried:
scope :owner_potentials, ->{ where('user_id == potential.proposal.user_id' ) }
scope :third_party_potentials, ->{ where('user_id != potential.proposal.user_id') }
I spent hours on codementor trying to learn scopes and my takeaway from that session is the syntax i used above is incorrect - but the way I was shown to write them (which is at the top of the post, gives an error with the undefined variable). I don't know how to figure out how to learn to write a scope.
Anyway- next I tried changing the edit action in my Proposal controller to:
#proposal.potentials.owner_potentials.build unless #proposal.potentials.owner_potentials
Now, I get no errors when I save this and try it, but when I try to edit the proposal, I can still edit potentials that have been created by third parties. I don't understand whether Im not writing the scope effectively, or if this solution isnt going to work for another reason.
I would consider adding a custom scoped association eg owners_potentials/third_party_potentials that limits potentials to those created by the owner/ by somebody other than the owner. You can then use these scopes whenever you need them.
eg
has_many :potentials, inverse_of: :proposal
has_many :owner_potentials, ->{ where("potentials.user_id = proposals.creator_id") }
has_many :third_party_potentials, ->{ where("potentials.user_id != proposals.creator_id") }
Then you could do something like:
def edit
#potentials = #proposal.owner_potentials || #proposal.owner_potentials.build
Then in the form, be specific about using the one you've specified:
<%= f.simple_fields_for #potentials do |f| %>
Note: code has not been tested or sanity-checked, it's here to give an idea of the kind of thing you can do - getting it to actually work is left as an exercise for the reader ;)

Creation of object which doesn't have model, while creation need to create several entries in other tables

In application user can enter new post which contain title, content of the post and category of post. So creating new post will be through some simple html form with few fields. Now i don't know where to put logic for creating new post for following reasons:
Post(or posts collection) is object which is constructed from different tables, for example.
#posts = User.joins(entries: [{storage: :vote}, :category])
.where("votes.count > ?", 0)
.select("users.username AS username,
storages.id AS storage_id,
storages.title AS title,
storages.content AS content,
votes.count AS votes,
categories.category_name AS category_name")
.order("votes.count DESC")
So when user create new post application must create new entries in different tables:
1.Create new entry in entries table. (id, user_id, category_id)
2. Create new entry in storages table.(id, title, content, entry_id)
3. Create new entry in vote table.(id, count, storage_id)
In situation where post is model i can use something like resources: posts then in posts controller through new and create i can create new post, but what in situation like this where i don't need posts controller nor post model? So, question is: which place is more appropriate to put logic for creating new post? Q1
My solution is to craete Storages controller with resource: storages, :only => [:new, :create] then through new and create of this controller to populate different tables in db? I'm forcing here only because i dont see any point of other CRUD actions here(like showing all or one storage), because i will not use storages individually but in conjunction with other tables. So from views/storages through new.html.erb and create.html.erb i can construct new post? Q2
Another solution is to create Post controller which doesn't have "corresponding" post model as i stated before. Here i'm guessing i can't use restful routes(CRUD) because there is not :id of post? I only can make manually non-restful routes like:
post 'posts/create/:title/:content/:category' => 'posts#create', :as => 'create_post' then from params[:title], params[:content] and params[:category] to populate other tables. Q3
Im new to rails so dont yell please :D
This sound like a call for nested forms, its covered in a screen cast
here.
You use the resources of the top model, in this case Entry.
and drill down to the 3rd model.
Simple sample of what to do is bellow.
Model should look like so,
entry.rb
class Entry < ActiveRecord::Base
has_many :storages, :dependent => :destroy
accepts_nested_attributes_for :storages, :allow_destroy => true
end
storage.rb
class Storage < ActiveRecord::Base
belongs_to :entry
has_many :votes, :dependent => :destroy
accepts_nested_attributes_for :votes, :allow_destroy => true
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :storage
end
and the form should look like so, in simple_form style
<%= simple_form_for #entry do |f| %>
<%= f.simple_fields_for :storages do |storage_fields| %>
<%= storage_fields_for :votes do |vote_fields| %>
<% end %>
<% end %>
<% end %>
and if you have all the models set up, you shouldn't have to do anything to the controller.
This approach is also nice because you can add multiple storages and votes ajax style(without reloading the page) if needed, which is always nice.
I'd use a form class instead of nested attributes any day, see http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ for an example of this pattern (Chapter "3. Extract Form Objects")
I've used the pattern often enough to gemify it https://github.com/bbozo/simple_form_class and it's used roughly in this way: https://gist.github.com/bbozo/5036937, if you're interested to use it I'll push some docs
EDIT: reason why I tend to go the form class route most of the time is because nested attributes never failed to bite me in the end, either because strong parameter handling got cumbersome, or validators get too complicated (if persisted? else ...), or persistence logic needs to be extended to support some little knack that resolves into callback hell or recursive saves in the model (before/after save spaghetti troubles)

Rails ActiveAdmin and has_many association

I am fairly new to active_admin, I was wondering if there is a way to achieve the following
I have two models
User
belongs_to :group
Group
has_many :users
I have successfully created pages in activeadmin for groups and users, now what I want is show users that belong to a certain group. I have button manage_members on groups index page which should show members of only that group. Where I can remove members from the group or add more members.
This is what I have been able to do so far
member_action :manage_members do
#group = Group.find(params[:id])
#page_title = "Manage Groups > ##{#group.name}, Edit Members"
end
and a the view app/vies/admin/groups/manage_users.html.arb
table_for assigns[:group].users do
column "Name" do |u|
u.user_id
end
column "email" do |u|
u.user.email
end
column "Created Date" do |u|
u.user.created_at
end
column "OfficePhone" do |u|
u.user.office_no
end
end
This shows the member of the groups, but I have to do all the work on this page to add edit delete a member, I cannot have active_admin filters and other cool stuff here, this is like a custom page,
Is there a way to have an index page (with all goodness of filters batch actions etc) ( like that of users ) but only showing users of a group. Something like a scoped index page which shows on users from a group and I have the same control over that page as any active admin index page ? More like the image below
rather than having to do all that work my self which currently looks like
Very new to active_admin so apologies if there something really straight forward that I am missing.
Thanks
Maybe a filter will do. That would look like (put it in the same file where you put the member_action)
filter :group, :as => :select, :collection => proc { Group.for_select }
The proc is there to make sure changes to groups (adding/removing/..) are immediately reflected to the select-list in the filter. That has to do with class caching in production.
Dont forget to put this scope in your Group model.
scope :for_select, :select => [:id, :name], :order => ['name asc']
Another way is to use scopes. If you have a field in your Group model, like a slug/label that could serve as a method header then you could do something like this in your activeadmin user register block:
Group.all.each do |group|
# note the sanitization of the Group name in the gsub
scope "#{group.name.gsub(/-/,'_')}".to_sym
end
And this in your User model:
Group.all.each do |group|
# note the sanitization of the Group name in the gsub
scope "#{group.name.gsub(/-/,'_')}".to_sym, joins(:group).where("group.name = ?",role.name)
# using joins(:group) or joins(:groups) makes a difference,
# so not sure as I have not tested, but maybe the where should be
# ....where("groups.name = ....
end
It should give you nice buttons above your index views like here: http://demo.activeadmin.info/admin/orders
If you want this for a has_and_belongs_to_many relation, I suggest you take a look at this Rails3 Active Admin - How to filter only records that meet all checked items in collection
Good luck!

Building nested attributes from form checkboxes in a model callback?

I am building a simple sample tracker for a small analytical lab and I'm bashing my head against a problem. Structurally, the database consists of users, batches, samples, instruments, and assays. Please excuse messy or poor code, I'm new to Rails and programming. Generally I know where I'm being sloppy and how to fix it, it's just a matter of me breaking a problem down. Here's some explicit abridged code:
class Batch
belongs_to :user
has_many :samples
has_many :submissions
has_many :assays, :through => :submissions
accepts_nested_attributes_for :samples
accepts_nested_attributes_for :submissions
end
class Assay
belongs_to :instrument
has_many :submissions
has_many :batches, :through => :submissions
end
class Submission
belongs_to :batches
belongs_to :assays
end
I have created a batch submission form that accepts batch information, sample names as a nested attribute, and I'm trying to accept submissions as a nested attribute as well. My form currently looks like this (Please note comment line):
= semantic_form_for #batch do |f|
- #instruments.each do |instrument|
%ol
%li
= instrument.instrument_name
%ol
- instrument.assays.each do |assay|
%li
= assay.assay_name
# Some magic check boxes that fix my problem.
= f.inputs :name => "Batch Info" do
= f.input :sampling_date, :required => false
= f.input :experiment_name, :required => false
= f.input :notes, :as => :text
= f.semantic_fields_for :samples do |sample_form|
= sample_form.input :sample_name
= f.buttons do
= f.commit_button :label => "Submit batch"
What I would like to have is a check box for each assay that passes the assay_ids to the submissions table as a nested batch attribute, magically making sure that each batch is associated with the chosen assays.
My problem is deciding when and how to build the submissions in the controller/model and how to populate them. It seems like I should make some sort of paramater that holds the assay_ids, and use some sort of model callback (before_create or before_save) that builds a params[assay_ids].length number of dummy submissions that I can fill with the assay_id params. Unfortunately all my attempts at building a method that does this have been futile.
I've watched all pertinent Railscasts I could find, read API entries, and I have been grinding on this for far too many hours. I can feel the answer on the very tip of my brain, and I am in desperate need of an Aha! moment. Thanks for any help you can provide!
Notice: This method is very brittle! I worked out a solution to make batches a true nested attribute and I will write it up ASAP.
So after a day of thinking the answer popped into my head (I swear I use Stack Overflow to officially start a 12 hour timer on when I'll epiphanically figure out what I got wrong). Here's how I did it:
Essentially I was trying to only make as many submissions as I needed so I didn't end up crapping my database up with empty ones, but I also wanted them built on the fly in-form. Because I didn't want to shove a bunch of extra logic into my view, I set up my magic check box as check_box_tag "assay_ids[]", assay.id to make a nice param separate from the batches param. Then, in my batches controller under create I built a little helper method:
def build_submissions(params)
#submitted_assays = params[:assay_ids]
for submitted_assay in #submitted_assays do
#batch.submissions.build(:assay_id => submitted_assay)
end
end
That builds up just as many submissions as I need. This fix works wonderfully, but I would welcome any suggestions as to how to solve the problem differently.

Resources