Ruby on Rails / Devise - Split user edit page into 2 pages - ruby-on-rails

I'm trying to split the users edit page (app/views/devise/registrations/edit.html.erb) into 2 pages for better UI, like:
/settings
/settings/profile
I'm fairly new to Rails, did Michael Hartl's tutorial and had read a few more I got my hands on, just building my first application, even if I have some experience with php
This is the view I try to split in 2, it is a view provided by the Devise gem (app/views/devise/registrations/edit.html.erb)
<h2>Login Details</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<%# sensitive info %>
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
....
<%= f.label :current_password %> <i>(to confirm changes)</i><br />
<%= f.password_field :current_password, autocomplete: "off" %>
<h2>Profile Details</h2>
<%# non-sensitive info %>
<%= f.label :name %><br />
<%= f.text_field :name %>
<%= f.submit "Update" %>
<% end %>
It uses a custom RegistrationsController (app/controllers/registrations_controller.rb)
class RegistrationsController < Devise::RegistrationsController
def update
....
end
end
Further more, this view is accessed via this route:
edit_user GET /users/:id/edit(.:format) users#edit
My main question is, how do I split this page into 2:
/settings containing Login Details
/settings/profile containing Profile details
and both to processed by the same controller, or the same action
Do I need to create a new controler/route/view, like:
controller: SettingsProfile
route: get 'settings/profile' => 'settings_profile#new'
view: app/views/settings_profile/new.html.erb
If so how do I pass the view the "resource" information, or any information for the matter of fact:
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
Things are pretty fuzzy at this point, please bear with me on this one

You don't need a separate controller, especially since you're already extending the default Devise RegistrationsController, which already works fine for updating user attributes.
Edit: If these aren't just extended user attributes, and profile is it's own object with its own logic and behaviour, then consider creating it's own controller, to manage the CRUD for that object.
If you're using devise's user/edit page as part one, all you need to do is add a profile action in your custom controller, and create a view file to go with it.
# this is all that's in the edit action from Devise
def edit
render :edit
end
# add this to your custom RegistrationsController
def profile
render :profile
end
Then you can fiddle with your routes (see this and this) until they route the URLs you want to use to the correct controller:
# you probably have this, which covers your current user/edit route
devise_for :users
# but you can add this to extend these routes
devise_scope :user do
# will route /profile to the profile action on User::RegistrationsController
get :profile, to: 'users/registrations'
# or if you want more control over the specifics
get 'users/settings/profile', to: 'users/registrations#profile', as: :user_profile
end
For your second view/form to update user attributes from another, non-devise controller, you can use form_for current_user, { url: user_registration_path }
If you do want to use resource, you'll have to add this to the top of your registrations controller, so that the resource gets defined on your profile action as well:
prepend_before_filter :authenticate_scope!, only: [:edit, :profile, :update, :destroy]
Take a look at devise's documentation around strong parameters to see how to make sure whatever additional attributes you're going to add to your user are white listed by your custom RegistrationsController.

As you suggested, one method would be to create a new controller, route, and view to handle this.
I might create a UserProfilesController controller with two actions: UserProfilesController#show and UserProfilesController#edit.
Then as you suggested, a route, e.g.,
get 'user_profiles/:id' => 'user_profiles#show'
get 'user_profiles/:id/edit' => 'user_profiles#edit'
In Devise parlance, the resource refers to the user. So the :id being passed above must be a User id of course. If you don't want to do that, you could always just assume you meant the current_user in which case you can skip using :id in the routes and just retrieve it in the controllers via current_user.id.
Finally, you just have to split out the profile details from the Devise view and create some under app/views/user_profiles/new.html.erb and similarly for edit.html.erb. Remember to remove the profile bits from the Devise view and I think you're on your way.
An Addendum
#AmrNoman made a good suggestion re: the update method. If you are following with my solution, you would add another action UserProfilesController#update, and a new route in your routes.rb file:
put 'user_profiles/:id/update' => 'user_profiles#update'
Additionally, if you intend to later refactor User to remove the profile details and handle them in a separate model, it may be prudent to replace my references to :id in the above code to :user_id. In this way, if you at some point create, e.g., a model called UserProfile it will be clearer that the :id is not the UserProfile#id but the UserProfile#user_id foreign key. This will leave you the ability to use :id to refer to UserProfile.id directly without affecting any API consumer in your app.
It may be a bit overkill but I think it's good practice.

Chris Cameron's answer is right, but I think this is closer to what you want:
First create your routes in routes.rb:
# this gets the edit page for login details
get "settings" => "user_profiles#edit_credentials", as: "edit_credentials"
# this gets the edit page for other profile info
get "settings/edit" => "user_profiles#edit_profile", as: "edit_profile"
# update method shared by both forms
# (you can create another one to handle both forms separately if you want)
put "settings" => "user_profiles#update"
Then your controller user_profiles_controller.rb:
class UserProfilesController < ApplicationController
# fill the methods as you need, you can always get the user using current_user
def edit_credentials
end
def edit_profile
end
def update
end
end
and finally your views, first views/user_profiles/edit_credentials.erb.html, here you show form for login details:
<%= form_for(current_user, url: settings_path) do |f| %>
<% end %>
then same thing in views/user_profiles/edit_profile.erb.html, just change your form contents:
<%= form_for(current_user, url: settings_path) do |f| %>
<% end %>
There might be some errors, but hopefully this gets you in the right direction (also make sure to handle authentication and authorization).

Related

Rails Need help to capture form field in model from product view

I need to capture a field added by a user in a form_for, inside the product show page.
My product.rb model as follows:
belongs_to :user
has_many :complaints
My complaint.rb model as follows:
belongs_to :product
belongs_to :user
My user.rb model as follows:
has_many :products
My product controller is a basic controller with all the new, create, edit, update actions and all the routes are good.
User looks at the product show page like this, and it's all good
http://localhost:3000/products/1
My goal is to create a complaint from the product show page, when user views the specific product. So I have created a complaints_controller.rb to capture all the details of the product, and create a complaint. I have an issue with capturing the complaint_number which is a field inside the complaints table.
Here is my form inside the product show page
<%= form_for([#product, #product.complaints.new]) do |f| %>
<%= f.number_field :complaint_number, placeholder: "Enter complaint number you were given" %>
<%= f.submit 'Complaint' %>
<% end %>
Here is my complaints_controller.rb
Goal is to capture the complaint_number fields and run the make_complaint method to create a complaint and populate rest of the fields in the newly created row of the complains table.
class ComplaintsController < ApplicationController
before_action :authenticate_user!
def create
# Will Get product_id from the action in the form in product show page.
product = Product.find(params[:product_id])
# This complaint_number does not seem to work
complaint_number = product.complaints.find_by(complaint_number: params[:complaint_number])
# Now I want to run a make_complaint method and pass the product and the complaint number. This fails, I can't capture the complaint_number in the form from user input.
make_complaint(product, complaint_number)
redirect_to request.referrer
end
private
def make_complaint(product, complaint_number)
complaint = product.complaints.new
complaint.title = product.title
complaint.owner_name = product.user.name
complaint.owner_id = product.user.id
# Note: complaint_number and current_complaint are a fields in the Orders table
# Note:
complaint.current_complaint = complaint_number
if complaint.save
flash[:notice] = "Your complaint has been sent!"
else
flash[:alert] = complaint.errors.full_messages
end
end
end
For routes I have added resources :complaint, only: [:create] inside the resources of products to get products/:id/complaints
My routes.rb is like this
Rails.application.routes.draw do
get 'products/new'
get 'products/create'
get 'products/edit'
get 'products/update'
get 'products/show'
root 'pages#home'
get '/users/:id', to: 'users#show'
post '/users/edit', to: 'users#update'
resources :products do
member do
delete :remove_image
post :upload_image
end
resources :complaint, only: [:create]
end
devise_for :users, path: '', path_names: { sign_in: 'login', sign_up: 'register', sign_out: 'logout', edit: 'profile' }
Your form has complaint_quantity:
<%= form_for([#product, #product.complaints.new]) do |f| %>
<%= f.number_field :complaint_quantity, placeholder: "Enter complaint number you were given" %>
<%= f.submit 'Complaint' %>
<% end %>
Your controller has complaint_number:
complaint_number = product.complaints.find_by(complaint_number: params[:complaint_number])
If you check your params from the server log, I bet you'll see the value you are looking for is coming across as complaint_quantity and not complaint_number.
UPDATE
With the form misspelling corrected, the error persists, so let's check into more areas:
complaint_number = product.complaints.find_by(complaint_number: params[:complaint_number])
So, break that down:
1. What does params actually include?
Is :complaint_number being submitted from the form?
If not, the form still has an error somewhere.
2. Does product.complaints actually include a complaint that could be matched by complaint_number?
I don't know your data structure well enough to tell, but it looks to me like you might actually want to do:
Complaint.find_by(complaint_number: params[:complaint_number])
instead of:
products.complaints.find_by(complaint_number: params[:complaint_number])
UPDATE #2
You know the problem is with your params.
I'm confident you aren't accessing your params correctly since you are using a nested form:
form_for([#product, #product.complaints.new])
Should mean your params are structured like { product: { complaint: { complaint_number: 1234 }}}
So params[: complaint_number] is nil because it should really be something like params[:product][:complaint][:complaint_number]
Please look at your server log in your terminal right after you submit the form to see the structure of your params. Or insert a debugger in the controller action and see what params returns.
ALSO, Instead of accessing params directly, you should whitelist params as a private method in your controller.
Something along these lines:
private
def product_complaint_params
params.require(:product).permit(:id, complaint_params: [ :complaint_number ])
end
See this: https://api.rubyonrails.org/classes/ActionController/StrongParameters.html

Search box to find user in rails

I'm somewhat new to rails. I'm going through making the classic twitter clone right now. I want to have a search bar on my homepage that allows the user to search for a twitter handle, and if the handle exists, it will send the user to the show page for that twitter handle.
I've been following a RailsCast on how to implement a simple search, but instead of doing it on the index like the video, I want to do it on the show action. I've run into some problems though. The form sits on my user index view.
Here is the error:
ActionController::UrlGenerationError in Users#index
Showing c:/Sites/Projects/twitterapp/twitter/app/views/users/index.html.erb where line #2 raised:
No route matches {:action=>"show", :controller=>"users"} missing required keys: [:id]
Here is the form:
<%= form_tag(user_path, method: 'get') do %>
<%= text_field_tag(:search, params[:search]) %>
<%= submit_tag("Search", name: nil) %>
<% end %>
Here is my show action:
def show
#user = User.search(params[:search])
end
And here is my search method in my user model:
def self.search(search)
if search
find(:all, conditions:['name LIKE ?', "%#{search}%"])
else
find(:all)
end
end
Actually you cannot use the show method as a search result finder. Because according to the rails convention:
For any resource like users, rails scaffold generates index,new, show, create, update, delete methods based on your routes files.
Thus based on the conventional way, show method always asks for an object. Lets say you are using UserContoller show method. It asks for a user object. Which you haven't provide in the form. that's why :id missing error is given.
I would tell you to do some more learning. And for searching create a different method in a different controller and define that controller method to the routes.rb file. This is the best way to do.
If you still want to use the show method, then change the show methods routing from the routes.rb file. You've to manually declare the show action on routes file.
you are using user_path and path need to inform id from present user
you can do this in action :index but I recommend you to create a action to this
view
<%= form_tag(search_users_path, method: 'get') do %>
<%= text_field_tag(:search, params[:search]) %>
<%= submit_tag("Search", name: nil) %>
<% end %>
routes.rb
resources :users do
post 'search', :on => :collection
end
users_controller.rb
def search
#user = User.search(params[:search])
end
You should to create a view search.html.erb similar as index.html.erb
As Emu and Breno pointed what causing the problem user_path requires an user id
Solution idea:
Why not just point to users index action? like this:
<%= form_tag(users_path, method: 'get') do %>
<%= text_field_tag(:search, params[:search]) %>
<%= submit_tag("Search", name: nil) %>
<% end %>
users_controller.rb:
def index
if params[:search]
#user = User.search(params[:search])
end
end
and you can use ajax remote: true to handle the returned user object
Found your question via Google, but the responses and suggestions didn't work for me. Found another solution that did, so seems worth posting here.
"Search and Filter Rails Models Without Bloating Your Controller":
http://www.justinweiss.com/articles/search-and-filter-rails-models-without-bloating-your-controller/

How to pass params to new view in Ruby on Rails app?

I'm trying to make simple app. I input my first name and last name to simple <%= form_for #data do |f| %> rails form and after submitting it, app should render simple text like this. My first name is <%= data.first_name %> and my last name is <%= data.last_name %>. I don't know why but my app is saying this error:
undefined local variable or method `data' for
It's probably saying it because no params are passed to view.
Here is my code.
routes.rb
resources :data, only: [:new, :create, :index]
data_controller.rb
class DataController < ApplicationController
def new
#data = Data.new
end
def index
end
def create
#data = Data.new(data_params)
if #data.valid?
redirect_to #data
else
render :new
end
end
private
def data_params
params.require(:data).permit(:first_name, :second_name)
end
end
/views/data/new.html.erb
<%= form_for #data do |f| %>
<%= f.label :first_name %>
<%= f.text_field :first_name %>
<%= f.label :second_name %>
<%= f.text_field :second_name %>
<%= f.submit 'Continue', class: 'button' %>
<% end %>
/views/data/index.html.erb
<h2>Coolest app ever :D</h2>
<p>My first name is: <%= data.first_name %>.</p>
<p>And my second name is: <%= data.second_name %>.</p>
/models/data.rb
class Data
include ActiveModel::Model
attr_accessor :first_name, :second_name
validates :first_name, :second_name, presence: true
end
Please help to find out why params are not passing to next page. Thanks anyways :D
Your view should look like this:
<h2>Coolest app ever :D</h2>
<p>My first name is: <%= #data.first_name %>.</p>
<p>And my second name is: <%= #data.second_name %>.</p>
Also, I would suggest that calling a model something generic like Data is not a very Rails-y approach. Generally, domain models correspond to real-world things like User and Article, which are easy to understand and relate to. It'll get confusing quite fast if you use need to make another model and want to call it Data2 or something :)
Edit:
Since you specified that you do not wish to use the database, I would recommend passing in the object params through the redirect:
redirect_to(data_path(data: #data))
and in your controller's index method:
def index
#data = Data.new(params[:data])
end
Now your view should render properly, since you're passing the in-memory #data object attributes as params within the redirect. You then recreate this object in the index page or wherever you wish to redirect to.
To expand on Matt's answer, the reason you're getting NilClass errors is because:
You're redirecting to a data#show action when no show action has been enabled within your routes file. Since you've set your views up for the index, I'm assuming you want to redirect there when the #data object has been verified as valid:
redirect_to data_path
However I would recommend you follow Rails conventions and specify the data#show route within your routes.rb:
resources :data, only: [:index, :new, :create, :show]
and in your data_controller.rb:
def show
#data = Data.find(params[:id])
end
Another problem is that you're not actually saving the #data object upon creating it. The new method populates the attributes, and valid? runs all the validations within the specified context of your defined model and returns true if no errors are found, false otherwise. You want to do something like:
def create
#data = Data.new(data_params)
if #data.save
redirect_to data_path
else
render :new
end
end
Using save attempts to save the record to the database, and runs a validation check anyways - if validation fails the save command will return false, the record will not be saved, and the new template will be re-rendered. If it is saved properly, the controller will redirect to the index page, where you can call upon the particular data object you want and display it within your view.

Trying to add a foreign relationship to my users model with Devise, then update it

This is for Rails 4.04 and Ruby 2.1. I'd like to allow my users to have addresses. So I generated an address table and gave it columns (NUMBER, STREET, CITY, STATE). Now when I go to the following url, I'd like to be able edit this information:
webapp.com/users/edit/
However I noticed it only showed the same old information (name, password, email). So I went to the view and added simple_fields for my new relationship so the view now looks like this:
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<div class="form-inputs">
<%= f.input :email, required: true, autofocus: true %>
<%= f.input :name, required: false %>
<%= f.simple_fields_for :addresses do |a| %>
<%= a.input :number %>
<%= a.input :street %>
<%= a.input :city %>
<%= a.input :state %>
<%= a.input :country %>
<% end %>
<%end%>
However it still doesn't generate the fields needed for address. I think this is because none of my users currently have any addresses attached to their account profile (because this migration was just created). However, in this case there should be blank fields generated so I can ADD address information.
I feel like I need to do something in the Users#Edit action like this
#users.each do |user|
user.address.build
end
Is that right? How can I override the users controller because this was created by Devise and I don't actually have a users controller (I looked for it it and couldn't find it).
UPDATE
Ok, I'm getting closer. I had to create my own controller to override Devise's default registrations controller as explained in the second answer of this stack overflow article:
Override devise registrations controller
So now I am getting into that controller which currently looks like this:
class Users::RegistrationsController < Devise::RegistrationsController
def edit
super
end
end
However, when I get to my view, it's still SKIPPING the block that starts like this:
<%= f.simple_fields_for :addresses do |a| %>
However, if I go manually into my DB and add a record in the addresses table and link it to my currently_signed in user via the foreign key, then the block does not get skipped. So whats the best way to generate this connection if the address record does not yet exist? Is it the build method? e.g.
user.address.build
in the controller
SOLUTION
Yes, I needed to added this method to my new registrations_controller.rb file
def edit
if resource.addresses.size == 0
resource.addresses.build
end
super
end
It is now working the way I intended it.
You need to do it like this:
#app/models/user.rb
Class User < ActiveRecord::Base
has_one :address
before_create :build_address, unless: Proc {|x| x.address.present? } #-> not sure about the if statement
end
#app/models/address.rb
Class Address < ActiveRecord::Base
belongs_to :user
end
--
Devise does come with Controllers
These controllers are not shown in your app (they are installed with the Devise gem, but only visible in production):
- confirmations_controller.rb
- omniauth_callbacks_controller.rb
- passwords_controller.rb
- registrations_controller.rb
- sessions_controller.rb
- unlocks_controller.rb
--
You Don't Need Them
Whilst you can override these controllers, you won't need to, as your edit action will be tied to the users controller:
#config/routes.rb
resources :users
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def edit
#user = User.find params[:id]
end
end
#app/views/users/edit.html.erb
<%= form_for #user do |f| %>
...
<%= f.fields_for :address do |a| %>
<% end %>
<% end %>
Update
Sorry for not explaining. You can use resources :users with devise_for :users:
#config/routes.rb
devise_for :users
resources :users, only: [:edit, :update]
If this does not work, you may need to change the path_names argument for devise, like so:
#config/routes.rb
devise_for :users, path_names: { sign_in: 'login', password: 'forgot', confirmation: 'confirm', unlock: 'unblock', sign_up: 'register', sign_out: 'logout'}
-
Form
The form can have resource, but I think you need to work out what you're trying to change. If you're trying to change a devise object, then use the resource helper; but if you're trying to change the User model directly - I'd be partial to changing that!
The issue I think you have is if you're using resource, it's going to route to Devise controller actions. I would try setting #user in the edit action of your users controller, and use the conventional way to update
it looks like you didn't made the relation between those models add to addresses column called user_id and add to user model:
has_many :addresses
and into addresses:
belongs_to :users
then in your view you can build this form using nested attributes see this post:
Rails 4 Nested Attributes Unpermitted Parameters
another option that you can do is to show that addresses form after the user already signed in then when he update the form find the user_id with current_user and build the record using this id but using strong params in Rails 4 is recommended to solve your issue.

How do you create a rails app without using all of resource in create,update,delete?

In the rails guides tutorial creating a blog app after we create the rails app and create a resources in the routes then we start working on a form_for for creating a posts title and text in the guide it tells me that we need to add this line <%= form_for :post, url: posts_path do |f| %>
the posts_path helper is passed to the :url option. What Rails will do with this is that it will point the form to the create action of the current controller, the PostsController, and will send a POST request to that route.
so what am trying to understand is the passing to 'create action' you see I have a simple app where i want is when a text is entered in the title field and the submit button is entered I want it to pass to the create action where I just out put the text in the create action view or another view, the rails guide goes through teaching the 'CRUD' but I just want to understand How to build an app that doesn't use 'CRUD' for instance an app that takes an input and outputs it in another view?
my form:
<h1>Here Lets create a simple post</h1>
<%= form_for :post, url: posts_path do|f| %>
<p>
<%= f.label :title %>
<%= f.text_field :title %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
posts controller:
class PostsController < ApplicationController
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
end
def post_params
params_require(:post).permit(:title)
end
end
create view:
<h1>THis is the post create action</h1>
<%= #post.title %>
routes:
Learnnobase::Application.routes.draw do
resources :posts
root "welcome#home"
end
Right now am getting an error stating uninitialized constant PostsController::Post highlighting my create method? I've done so many rails app tutorials using 'CRUD' I really wanna learn building a simple app without using 'CRUD', I was trying to experiment with this app even though I do use the create action of "CRUD".
We generally use Rails to build database-backed applications, but for learning purposes, you can do it this way.
The problem you are facing here is: You are tyring to create an object of the Post class, that will be the model in the example you are referring to. The error comes up since you have not created the Post model.
To meet your requirement you can make your create action be:
def create
#post = post_params #this will be a hash
end
Then change your view to:
<h1>THis is the post create action</h1>
<%= #post[:title] %>
Since you have started with rails, I would ask you how did your posts/new page load with #post = Post.new in posts/new action when you do not have post model file and class?
It is not possible. Second, with what Manoj Monga has suggested you to use params by assigning it to an instance variable(wrong way to do so with params), if you try to use create_path for posts resources which literally is '/posts' you would end up hitting posts/index action.
Rails has standard reserve action names like :index(GET, /posts), :show(GET, /posts/:id), :new(GET, /posts/new), :create(POST, /posts), :edit(GET, /posts/:id/edit), :update(PATCH, /posts/:id) You should not attempt at overriding their purpose.
What I understand that you did use post model class and loaded posts/new page, then you deleted post model class and tried with what you have asked about in your question. You should respect Rails' standards.

Resources