split 1 big form into several sub forms - ruby-on-rails

What would be the best way to split a user profile model and its form into several sub forms that you can update separately?
like
* basic details
details details
my photos
my interests
You can only have 1 edit action, so what would be the preferred way of handling this?

So in reality, what you really probably want to do is create a set of nested resources for the user, so that you can treat each of these separately.
resources :users do
resource :basic_details
resource :detailed_details
resources: :photos
resources: interests
end
Which gives you routes like: edit_user_basic_details(#user), so then, you can have forms that hit the update actions of these sub resources, like this:
<%= form_for :basic_details, url: user_basic_details_path(#user) do |form| %>
<%= form.text_field :name %>
<%= form.submit %>
<%= end %>
This way, you can setup controllers like this:
class BasicDetailsController < ApplicationController
def edit
#user = User.find(params[:user_id])
end
def update
#user = User.find(params[:user_id])
#user.update_attribures(params[:basic_details])
end
end
This is a very quick and dirty way to implement this, but its meant to show you have to get started. You don't have to think about form and controllers as only editing tables in your database, sometimes its much more convenient to think about particular parts of one of your models as its own resource which can be edit separately.
Hope this gets you started.

If you're only updating you might just be able to use the standard edit/update action and just make smaller forms. Just create the forms as usual and simply include just the fields you want and have them all point to your normal update action.
If you're creating a new user from the smaller forms you might run into problems with validating different fields, but if you're just updating then the minium requirements for validation should already be met

Related

Rails 4 should I set the instance variables or chain on the methods?

I am on Rails 4 and have a very simple question.
Say you have a User model which has_one Account and Account belongs_to the User
On the user show page, I would like to display user attributes as well as account attributes.
In the users_controller I could do it like this:
def show
#user = User.find(params[:id])
#account = #user.account
end
and then in the view:
<%= #user.name %>
<%= #account.id %>
OR
I could just set the user instance variable:
def show
#user = User.find(params[:id])
end
and in the view:
<%= #user.name %>
<%= #user.account.id %>
Is there a difference in these? Is one of them the 'Rails Way'? I may be over thinking this, but am just curious as to what is correct here.
I would argue that it's really a matter of preference. If I were the one writing it, I would assign it to a variable if it were going to be used multiple times across the view. If it is only used once, it seems redundant to assign it.
Recommended best practice is that you limit to one instance variable per controller action and #person.account.id (chaining) is not allowed.
See https://robots.thoughtbot.com/sandi-metz-rules-for-developers
Using decorators is good idea (e.g. Draper). The above example too simple to add another gem. You can use delegate in rails
See http://apidock.com/rails/Module/delegate

Restricting a model to only view their own items in the has_many

So I'm trying to think about how to route my site and I need a little help. I have a business who can .build (as in business creates) buildings (sorry for the repetition haha) in a has_many. Each property has many something else.
I would like it so even though there will be more than one building, each business should only be able to view their own buildings, so if someone tries to alter a url, it would redirect home.
I have
resources :buildings
so as it is set up, anyone could just type in
host/buildings/whatever
I would like to redirect with an error if the building ID does not belong to the current_business (devise) it will redirect to their home page. each building has a business_id
Would I have to break the RESTful for this?
Thank you!
Assuming you have user_id in builduing resource:
buildings_controller.rb
def index
#buildings = current_user.buildings
end
def show
#building = current_user.buildings.find(params[:id])
end
buildings/index.html.erb
<% #buildings.each do |building| %>
<%= building.whatever_atribute %>
<% end %>
buildings/show.html.erb
<%= #building.whatever_atribute %>
With the above code when user will go to /buildings he will see only his buildings, and if he'll go to buildings/3 he will see this building if he owns it, in other case he will see a not found error that you can customize it with a redirect or display a styled page.

passing non-model fields with ActiveRecord

I have a situation in which I'm passing additional fields to form_for function, which don't belong to a model. Those fields are: redirect_to, redirect_time. I use those fields to know where to redirect the user after the form is successfully submitted - I don't store them, just read in the controller.
Now, everything is working perfectly when I have:
<%= form_for #mymodel .. %>
<%= if defined?(redirect_to) %>
<%= hidden_field_tag :redirect_to, redirect_to %>
<% end %>
<% end %>
And read in controller:
def action
if params['redirect_to']
redirect_to params['redirect_to']
end
end
But the problem occurs when the form has an error, Rails of course, won't pass those params that don't belong to the current resource.
Whats the most elegant way to deal with this situation in Rails to preserve those fields when form submission fails?
Thanks for help
I think you'd be better off storing these variables within the session rather than the form. Firstly it will make them much harder to interfere with if people started poking about in the source of your web page, and secondly they'll be available to whatever action or controller you need them in once you're done with processing the submitted form.
Since they're not related to the form itself it feels much cleaner abstracting them outside of it.
Just set the session variables you need in the controller prior to rendering the form:
session[:redirect_to] = url_to_redirect_to_after_submitting_form
Then you can redirect there after saving the record in your create action:
def create
# ... save record
redirect_to session[:redirect_to]
end
Passing non-model fields through ActiveRecord is poor solution to your problem of routing.
Keep to convention and use ActiveRecord as an interface to the state of your application data.

Rails using 'accepts nested attributes for' with a 'belongs to' relationship

Some pseudocode from my app:
User has many Products
User has many Projects
Project and Product belong to User
Furthermore:
Project has one Video
Video belongs to Project
I have a multi-step wizard built using the Wicked gem. In step one I create and save a Project. In step two I add a Video to that Project:
= form_for #project do |f|
= f.fields_for :video_attributes do |v|
= v.file_field :file
Everything works fine, but I'd like to add a Product to the Project's User during this same step. I'm a little confused as to how accepts nested attributes works for this sort of thing.
I imagine I need to do something like this in my wicked controller:
#user = current_user
# wicked makes us use :project_id as it hijacks :id
#project = #user.projects.find(params[:project_id])
#user.products.build
But where do I stick the 'nested attributes for' call? Do I need more than one call to accepts_nested_attributes_for? Would this work?
Make Project model accept nested attributes for User
Make User model accept nested attributes for Product
= form_for #product do |f|
= f.fields_for :user_attributes do |u|
= u.fields_for :product_attributes do |p|
= p.file_field :image
I can't try the code out till tomorrow, but i will sleep better knowing i can solve this when i get to it.
You certainly can extend nested attributes through several objects by nesting the fields_for calls... But you can sometimes get into trouble if you jump back and forth between objects like it looks like you're headed towards here. I've had problems with circular save issues resulting from structures like that. For this reason, I recommend keeping the accepts_nested_attributes_for as a one-way street. So, if a user accepts_nested_attributes_for projects then a project should not also accepts_nested_attributes_for users. Given that, your form has to be built based on the root object. I don't know your project but for mine that was the user. Basically, it's more likely the user would be the central relationship. Hope this helps.
Also, I'm not sure why your fields_for calls are using <some object>_attributes. Unless you're doing something special, those should be relation names like f.fields_for :video. This way the fields_for call loops through each object of that type in the collection.

How to hide parts of the view given a user role on Rails 4

I'm trying to hide parts of my views depending on the User role.
So let's say I want only admins to be able to destroy Products. Besides the code in the controller for preventing regular users from destroying records, I would do the following in the view:
<% if current_user.admin? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
The previous code works, but it's prone to errors of omission, which may cause regular users to see links to actions they are not allowed to execute.
Also, if I decide later on that a new role (e.g. "moderator") can delete Products, I would have to find the views that display a delete link and add the logic allowing moderators to see it.
And if there are many models that can be deleted only by admin users (e.g. Promotion, User) maitenance of all the ifs would be pretty challenging.
Is there a better way of doing it? Maybe using helpers, or something similar? I'm looking for something maybe like this:
<%= destroy_link 'Delete', product %> # Only admins can see it
<%= edit_link 'Edit', promotion %> # Again, only admins see this link
<%= show_link 'Show', comment %> # Everyone sees this one
I found these two questions that are similar to mine, but none of them answered my question:
Show and hide based on user role in rails
Ruby on Rails (3) hiding parts of the view
I strongly recommend pundit.
It allows you to create "policies" for each model. For your Product model you might have a ProductPolicy that looks something like this
class ProductPolicy < ApplicationPolicy
def delete?
user.admin?
end
end
In your view you can do something like this
<% if policy(#post).delete? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
If later on you want to add a moderator role, just modify the policy method
class ProductPolicy < ApplicationPolicy
def delete?
user.admin? || user.moderator?
end
end
So I kind of figured a way to move the IFs out of the view. First, I override the link_to helper in my application_helper.rb:
def link_to(text, path, options={})
super(text, path, options) unless options[:admin] and !current_user.admin?
end
Then on my views I use it as:
<%= link_to 'Edit Product', product, admin: true, ... %>
This prevents regular users from seeing admin links, but for other html tags with content inside, such as divs, tables etc., an if would still be needed.
CanCan is another gem that lets you define "Abilities" per user role.
In views you can use something like if can? :delete, #post to check if the
user may delete that specific post.
Using the CanCan and Role gems, what is still needed is a way to Check The Route and see if "current_user" has permissions to access that Route based on their role(s) - then show/hide based on that.
This saves the user clicking on things and getting told they cannot see it - or us having to write per-item "if" logic specifying what roles can see what list-items (which the customer will change periodically, as roles are changed/refined) around every single link in one's menu (consider a bootstrap menu with 50+ items nested in groups with html formatting, etc), which is insane.
If we must put if-logic around each menu-item, let's use the exact same logic for every item by checking the role/permissions we already defined in the Ability file.
But in our menu-list, we have route-helpers - not "controller/method" info, so how to test the user's ability to hit the controller-action specified for the "path" in each link?
To get the controller and method (action) of a path (my examples use the 'users_path' route-helper) ...
Rails.application.routes.recognize_path(app.users_path)
=> {:controller=>"users", :action=>"index"}
Get just the controller-name
Rails.application.routes.recognize_path(app.users_path)[:controller]
=> "users"
Ability uses the Model for its breakdown, so convert from controller name to it's model (assuming default naming used) ...
Rails.application.routes.recognize_path(app.users_path)[:controller].classify
=> "User"
Get just the action-name
Rails.application.routes.recognize_path(app.users_path)[:action]
=> "index"
And since the "can?" method needs a Symbol for the action, and Constant for the model, for each menu-item we get this:
path_hash = Rails.application.routes.recognize_path(app.users_path)
model = path_hash[:controller].classify.constantize
action = path_hash[:action].to_sym
Then use our existing Abilty system to check if the current_user can access it, we have to pass the action as a symbol and the Model as a constant, so ...
<% if can? action model %>
<%= link_to "Users List", users_path %>
<% end %>
Now we can change who can see this resource and link from the Ability file, without ever messing with the menu, again. But to make this a bit cleaner, I extracted out the lookup for each menu-item with this in the app-controller:
def get_path_parts(path)
path_hash = Rails.application.routes.recognize_path(path)
model_name = path_hash[:controller].classify.constantize
action_name = path_hash[:action].to_sym
return [model_name, action_name]
end
helper_method :get_path_parts
... so I could do this in the view (I took out all the html-formatting from the links for simplicity, here):
<% path_parts = get_path_parts(users_path); if can?(path_parts[1], path_parts[0]) %>
<%= link_to "Users Listing", users_path %>
<% end %>
... and to make this not take all day typing these per-menu-item if-wraps, I used regex find/replace with capture and wildcards to wrap this around every list-item in the menu-item listing in one pass.
It's far from ideal, and I could do a lot more to make it much better, but I don't have spare-time to write the rest of this missing-piece of the Role/CanCan system. I hope this part helps someone out.

Resources