Using devise_invitable with enum User roles - ruby-on-rails

1. Description core problem
I am implementing the devices_invitable gem for inviting users. The gem is correctly added, as I am able to send users the standard devices_invitable email to invite them for the application.
The problem that I am encountering, is that I am not sure how to enable an admin (to be identified with enum) to assign the role of the user they are inviting (e.g. they should enter an email AND role of the user they invite, which should then be correctly added to my DB).
Note: As you can see below, I'm currently trying to solve it using nested forms with the cocoon gem, but I am open to other approaches.
2. My current application
The application has 2 tables with a many to-many relationship between them:
(i) Users, with role attributes defined by enum.
(ii) Parks
The idea is to let an admin invite a new user to a park (not entire application) by entering email+role.
3. The code
user.rb
class User < ApplicationRecord
has_many :user_parks, dependent: :destroy
has_many :parks, :through => :user_parks
enum role: [:owner, :admin, :employee, :accountant]
after_initialize :set_default_role, :if => :new_record?
def set_default_role
self.role ||= :admin
end
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :invitable
end
park.rb
class Park < ApplicationRecord
has_many :user_parks, dependent: :destroy
has_many :users, :through => :user_parks
accepts_nested_attributes_for :users, allow_destroy: true, reject_if: ->(attrs) { attrs['email'].blank? || attrs['role'].blank?}
end
parks_controller.rb
class ParksController < ApplicationController
def update
#park = Park.find(params[:id])
authorize #park
url = Rails.application.routes.recognize_path(request.referrer)
last_action = url[:action]
if last_action == 'edit_users' & #park.user.last.nil?
#user=#park.user.last.invite!(email: park_params[:user][:email])
end
#park = #park.update_attributes(park_params)
redirect_to parks_path
end
def edit_users
#park = Park.find(params[:id])
authorize #park
end
private
def park_params
params.require(:park).permit(:name, users_attributes: [:email, :role, :_destroy])
end
end
edit_users.html.erb
<%= render 'users_edit_form', park: #park%>
_users_edit_form
<%= simple_form_for [#park] do |f|%>
<h1>Park</h1>
<% #park.users.each do |user| %>
<%= user.email %>
<% end %>
<div>
<% f.simple_fields_for User.new, :url=> new_user_invitation_path, html: { class: 'form-inline' } do |user| %>
<%= user.input :email %>
<%= user.input :role, priority: [:employee], collection:[:owner, :admin, :employee, :accountant] %>
<%= f.button :submit, 'Invite User', class: 'btn-primary' %>
<% end %>
</div>
#TEST for standard view (sends invite but does not assign park/role)
<%# link_to "new user add", new_user_invitation_path %>
<%= f.button :submit, 'Invite User', class: 'btn-primary' %>
<% end %>

1st issue I can see is you are using a bitwise & on this line:
if last_action == 'edit_users' & #park.user.last.nil?
you would usually use a logical and && or and
2nd issue with that line is #park.user should be #park.users if it's declared as a has_many
3rd issue is I'm not sure you need to be looking at request.referrer?
Assuming you can get to the invite! line then you might want to check the docs for the 2 different versions of invite!
When the user is already created:
user = User.find(42)
user.invite!(current_user) # current user is optional to set the invited_by attribute
see example in README
OR
When the user isn't created yet:
User.invite!(email: 'new_user#example.com', name: 'John Doe')
# => an invitation email will be sent to new_user#example.com
see example in README

Related

How to access the edit method of a nested resource with simple_form in rails?

I have 3 models: Book, UserBook, and User (from devise). User and Book are linked though UserBook with a has_many:users through: :user_books.
I've been trying to access the edit and destroy methods in user_books using a simple form in books#show to change a boolean value have_or_want from false to true. These are the models:
class Book < ApplicationRecord
has_many :user_books
has_many :users, through: :user_books
validates :title, :author, presence: true
validates :rating, inclusion: {in: [1,2,3,4,5], allow_nill: false }
end
class UserBook < ApplicationRecord
belongs_to :user
belongs_to :book
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :user_books
has_many :books, through: :user_books
end
The idea is that you can look a book in books#show and an if statement decides which of three simple_forms is shown:
If there is no UserBook linking the current_user to the book there will be a radio button which if you want to add it to your own book collection (#user_book.have_or_want: true) or add it to your reading list (#user_book.have_or_want: false). This bit work and a new UserBook is created correctly using user_books#update.
I am now trying to have a form for when the book is on the users reading list(#user_book.have_or_want) to give the option marking the book as read (change #user_book.have_or_want to true with user_books#update) or to remove the book (user_books#destroy).
Also a third form for if it is in the book collection (#user_book.have_or_want: true) to remove it by deleting the #user_book.
In the routes UserBook is nested in Book.
The if statement and simple forms in books#show looks like this and the first part works.
<% if current_user.books.exclude? #book %>
<%= simple_form_for [#book, #user_book] do |f| %>
<%= f.input :have_or_want,
:collection => [[true, 'Add to Bookshelf'], [false, 'Add to resding
list']],
:label_method => :last,
:value_method => :first,
:as => :radio_buttons %>
<%= f.submit "Submit" %>
<% end %>
<% elsif #book.user_books[0].have_or_want == false %>
<!-- If is is on the reading list you can add to bookshelf(changes
have_or_want to true) or can remove from reading list (delete
user_book instance) -->
<%= simple_form_for [#book, #user_book], url:
book_user_book_path(#book, #user_book), method: :patch do |f| %>
<%= f.input :have_or_want %>
<%= f.submit "Submit" %>
<% end %>
<% else %>
<!-- It is on the bookshelf so option to remove - user_books#destroy?
-->
<% end %>
I have tried several ways to do the second part and this is my best guess at the moment. There error I get is:
No route matches {:action=>"show", :book_id=>"1", :controller=>"user_books", :id=>nil}, missing required keys: [:id]
Also #user_book is a user_book with all values of nil.

Rails 4 - Access attributes of nested model in form

I have Account model which have a has_many relationship with User model:
class Account < ActiveRecord::Base
has_many :users, -> { uniq }
accepts_nested_attributes_for :users
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :confirmable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
belongs_to :account
I added avatar attribute to User model using paperclip.
I want each user to have access to the common account settings, and inside it having the possibility to upload his/her own avatar.
I use simple_form so I tried this:
<%= simple_form_for current_account, :html => { :multipart => true } do |f| %>
<%# here come account settings %>
<%= f.input :time_zone, :label => t(".timezone"),
:
:
<%# here I need to access current user attributes %>
<%= f.simple_fields_for :user, current_account.users.first do |user_form| %>
<%= user_form.file_field :avatar, :error => false %>
<% end %>
<% end %>
First problem:
I need some logic to access current_user instead of current_account.users.first. Since there is a superadmin which can access all accounts, use current_user is not enough.
Second (and bigger) problem:
I added in my controller the avatar parameter to the whitelist:
def allowed_params
params.require(:account).permit(:time_zone, :logo, :description, user: [:avatar])
end
When I try to update my model:
if current_account.update(allowed_params)
I get this error:
unknown attribute: user
I also tried:
params.require(:account).permit(:language, :time_zone, :logo, :description, :user_attributes => [:avatar])
and:
params.require(:account).permit(:language, :time_zone, :logo, :description, :users_attributes => [:avatar])
(in plural)
but since I use ActionController::Parameters.action_on_unpermitted_parameters = :raise I get:
found unpermitted parameters: user
It must be something very easy, some help please?
Ok, got it!!
The problem is the one-to-many relationship and the way I tried to access a single instance of user. The correct way to do it is:
<% current_account.users.each_with_index do |user, index|%>
<%= f.simple_fields_for :users, user do |user_form| %>
<%= user_form.file_field :avatar, :error => false %>
<% end %>
<% end %>
As you can see, the iteration should be done over the relation, and only when having a
single instance "in hand" we can user the simple_fields_for.
Also, notice that the first parameter passed to simple_fields_for is :users and not :user, since this is a one-to-many relationship.

Rails Devise registration with an additional model

I've searched for about an hour now and found an immense amount of questions describing how to add fields to the Devise user model. However I couldn't find any that explain in a clear way how to add one or multiple models to the registration process.
At registration I want the user to fill out an e-mailaddress, password and in addition my client model, company model and address model (so I have all the information the webapplication needs to run properly).
My models are like this
user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :email, :password, :password_confirmation, :remember_me, :client
belongs_to :client
end
client.rb
class Client < ActiveRecord::Base
attr_accessible :bankaccount, :email, :logo, :mobile, :phone, :website
has_many :users
has_one :company
has_one :address
accepts_nested_attributes_for :company, :address
end
I think the only way to do this is to create my own RegistrationsController so I can do #client = Client.new and then do this in my view:
<%= f.simple_fields_for #client do |ff| %>
<%= f.simple_fields_for :company do |fff| %>
<% field_set_tag t(:company) do %>
<%= ff.input :name %>
<% end %>
<% end %>
<%= f.simple_fields_for :address do |fff| %>
//address inputs
<% end %>
<% end %>
<fieldset>
<legend><%= t(:other) %></legend>
// other inputs
</fieldset>
<% end %>
The reason I need it to work this way is because I have multiple users who can represent the same client (and thus need access to the same data). My client owns all the data in the application and therefor needs to be created before the application can be used.
Okay, it took me about 8 hours but I finally figured out how to make it work (if someone has a better/cleaner way of doing this, please let me know).
First I've created my own Devise::RegistrationsController to properly build the resource:
class Users::RegistrationsController < Devise::RegistrationsController
def new
resource = build_resource({})
resource.build_client
resource.client.build_company
resource.client.build_address
respond_with resource
end
end
After that I just needed to adjust config/routes.rb to make it work:
devise_for :users, :controllers => { :registrations => "users/registrations" } do
get '/users/sign_up', :to => 'users/registrations#new'
end
Also I had an error in my devise/registrations/new.html.erb. It should have been f.simple_fields_for :client instead of f.simple_fields_for #client.
Now it properly creates all the objects for the nested attributes and automatically saves it when saving the resource.

Displaying a link in microposts and comments that links to the author's profile?

I created a Micropost model that have the following attributes:
<Micropost id: 1, content: "test", user_id: 1, created_at: "2012-01-25 15:34:30", updated_at: "2012-01-29 11:07:53", title: "asdasdad">
an User model with the following attributes:
<User id: 1, email: "alex#gmail.com", username: nil, etc...>
and a Comment model with the following attributes:
<Comment id: 1, content: "sdf", micropost_id: 1, user_id: nil, created_at: "2012-01-29 11:10:42", updated_at: "2012-01-29 11:10:42">
So far, I've only accomplished this:
Display the username of the author of a micropost
Display the ID of the authors of the microposts' comments
<h2>Micropost Index</h2>
<% #microposts.each do |micropost| %>
<%= micropost.title %></td>
<%= micropost.content %></td>
<%= link_to 'Show', micropost %></td>
<%= link_to 'Edit', edit_micropost_path(micropost) %></td>
<%= link_to 'Destroy', micropost, confirm: 'Are you sure?', method: :delete %>
<h2>Comments</h2>
<% #micropost.comments.each do |comment| %>
<p>
<b>Comment:</b>
<%= comment.content %>
</p>
<p>
<b>Commenter</b>
<%= comment.user_id %>
</p>
<% end %>
I don't know how to create a link to the authors profile (e.g. mysite.com/users/1).
I don't how to retrieve the name of the author of a comment and the link to his/her profile
EDIT:
Models:
user.rb:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :username
has_many :microposts
has_many :comments
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = User.where(:email => data.email).first
user
else # Create a user with a stub password.
User.create!(:email => data.email, :password => Devise.friendly_token[0,20])
end
end
end
micropost.rb:
class Micropost < ActiveRecord::Base
attr_accessible :title, :content
belongs_to :user
has_many :comments
end
comment.rb:
class Comment < ActiveRecord::Base
attr_accessible :content, :user_id
belongs_to :micropost
belongs_to :user
end
Micropost controller:
controllers/microposts.rb
def show
#micropost = Micropost.find(params[:id])
end
def new
#micropost = Micropost.new
end
def create
#user = current_user
#micropost = #user.microposts.new(params[:micropost])
#micropost.save
redirect_to #micropost
end
Any suggestions to accomplish this?
To make a link to the user you can use
<%= link_to comment.user.username, comment.user %>
In general part of the "Rails Magic" is, that you if you set up an association correctly, that you can access the related objects through the dot notation. That means, you don't need to say comment.user_id but instead go directly for the associated user object, e.g. comment.user.username or comment.user.email ... you get the idea :)
In order to to this you should have set up your models like this:
class User < ActiveRecord::Base
validates_presence_of :username #username should obviously not allow nil values
has_many :microposts
has_many :comments
end
class MicroPost < ActiveRecord::Base
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :user
end
# Link:
=link_to "User profile", user_path(comment.user)
# Name of the author
=comment.user.username
or since you have user_id in both Micropost and Comment
# Link:
=link_to "User profile", (#micropost.user)
# Name of the author
=#micropost.user.username

Use both Account and User tables with Devise

I'm working with Rails 3.1.0 and Devise 1.4.8, and am new to both.
I want to allow multiple users for an account. The first user to sign up creates the account (probably for their company), then that user can add more users. A user is always linked to exactly one account.
I have Users and Accounts tables. Abbreviated models:
class User < ActiveRecord::Base
belongs_to :account
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable, :lockable, :timeoutable
attr_accessible :email, :password, :password_confirmation, :remember_me
end
class Account < ActiveRecord::Base
has_many :users, :dependent => :destroy
attr_accessible :name, :account_type
end
The question is, when the first user signs up, how do I create both the Account and User?
Do I need to modify/override the Devise registrations_controller,
something like the answer here? I couldn't figure out how to
create the Account then pass it to Devise for creating the User.
account_id is already in the User model. Should I add account_name
and account_type to the User model, and create a new the Account
record if account_id is not passed in? In this case, I'm trying to hide Account creation from Devise, but not sure that will work since I still need to prompt for account_name and account_type on the registration page.
Finally got this to work using nested attributes. As discussed in the comments on Kenton's answer, that example is reversed. If you want multiple users per account, you have to create the Account first, then the User--even if you only create one user to start with. Then you write your own Accounts controller and view, bypassing the Devise view. The Devise functionality for sending confirmation emails etc. still seems to work if you just create a user directly, i.e. that functionality must be part of automagical stuff in the Devise model; it doesn't require using the Devise controller.
Excerpts from the relevant files:
Models in app/models
class Account < ActiveRecord::Base
has_many :users, :inverse_of => :account, :dependent => :destroy
accepts_nested_attributes_for :users
attr_accessible :name, :users_attributes
end
class User < ActiveRecord::Base
belongs_to :account, :inverse_of => :users
validates :account, :presence => true
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable, :lockable, :timeoutable
attr_accessible :email, :password, :password_confirmation, :remember_me
end
spec/models/account_spec.rb RSpec model test
it "should create account AND user through accepts_nested_attributes_for" do
#AccountWithUser = { :name => "Test Account with User",
:users_attributes => [ { :email => "user#example.com",
:password => "testpass",
:password_confirmation => "testpass" } ] }
au = Account.create!(#AccountWithUser)
au.id.should_not be_nil
au.users[0].id.should_not be_nil
au.users[0].account.should == au
au.users[0].account_id.should == au.id
end
config/routes.rb
resources :accounts, :only => [:index, :new, :create, :destroy]
controllers/accounts_controller.rb
class AccountsController < ApplicationController
def new
#account = Account.new
#account.users.build # build a blank user or the child form won't display
end
def create
#account = Account.new(params[:account])
if #account.save
flash[:success] = "Account created"
redirect_to accounts_path
else
render 'new'
end
end
end
views/accounts/new.html.erb view
<h2>Create Account</h2>
<%= form_for(#account) do |f| %>
<%= render 'shared/error_messages', :object => f.object %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :users do |user_form| %>
<div class="field"><%= user_form.label :email %><br />
<%= user_form.email_field :email %></div>
<div class="field"><%= user_form.label :password %><br />
<%= user_form.password_field :password %></div>
<div class="field"><%= user_form.label :password_confirmation %><br />
<%= user_form.password_field :password_confirmation %></div>
<% end %>
<div class="actions">
<%= f.submit "Create account" %>
</div>
<% end %>
Rails is quite picky about plural vs. singular. Since we say Account has_many Users:
it expects users_attributes (not user_attributes) in the model and tests
it expects an array of hashes for the test, even if there is only one element in the array, hence the [] around the {user attributes}.
it expects #account.users.build in the controller. I was not able to get the f.object.build_users syntax to work directly in the view.
Couldn't you use something like what's covered in these RailsCasts?:
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
You could setup your models as described in those screencasts, using accepts_nested_attributes_for.
Then, your views/devise/registrations/new.html.erb form would be for :user like normal, and could include a nested form for :account.
So something like this within that default form:
<%= f.fields_for :account do |account_form| %>
<div>
<p>
<%= account_form.label :name, "Account Name", :class => "label" %>
<%= account_form.text_field :name, :class => "text_field" %>
<span class="description">(e.g., enter your account name here)</span>
</p>
</div>
<div>
<p>
<%= account_form.label :company, "Company Name", :class => "label" %>
<%= account_form.text_field :company, :class => "text_field" %>
</p>
</div>
<% end %>
This is sample code from an app I'm working on and I'm using the simple_form gem, so the helpers used in your app may be different, but you'll probably get the idea.
So when a user is created (when they register), they can also fill in the info that'll be used by the Account model to create their account once they hit the "Sign Up" button.
And you may want to set an attribute on that user like "admin" too...sounds like this initial user will be the "admin" user for the company, though other users may have access too.
Hope that helps.
The best solution would be to use a gem.
Easy way: milia gem
Subdomain way: apartment gem

Resources