Inviting Users with devise_invitable Discussion
What made you laugh at 7:25? Is that a bird?! When do we get meet her/him? :)
That's actually my cat. :) He does some chirp-y sounding meows when he sees things outside like birds or squirrels. I'll have to let him in one of the videos soon.
Cool video and very clear explanations !
Maybe you can give me a hint on how you would deal with a not so different event management app I'm working on :
User manage its Collaborators like on its phone, meaning full control on Collaborator profiles, and add them to an Event. Two dedicated models are used since Collaborator isn't unique (many User can create a Collaborator which is the same person). The thing is a User is notified of each event with a Collaborator that share the same email, and can choose to take control of it (Collaborator become User). This is the only way I found to have a fully working app before all collaborators joined it as user. It works, but I'm kind of stuck when, on a project, I have to loop through User and Collaborator to show the Event team efficiently. Is there a good way to do this ? Thank you for your very helpful advices
Great stuff. Probably saved me a lot of digging around time. Thanks.
Would have liked to see some aspect of testing addressed. I have come to appreciate good tests, and am still getting comfortable with good testing habits and strategies. I really like how you show real-world stumbling blocks and how to navigate them well. Seeing similar struggle and success with testing would be helpful for me.
I do see devise has some example tests posted, so maybe those are enough to get me going.
Thanks again!
I am using rails 4 and got the error below. Any ideas what I am doing wrong? I used the attr_accessor :email in the User.rb model. I have that feeling that I missed something really simple but I am having a brain freeze LOL
NameError in TeamsController#create
undefined local variable or method `email' for #<teamuser:0x007fcef90e37b8>
Extracted source (around line #8):
def set_user_id
existing_user = User.find_by(email: email)
self.user = if existing_user.present?
existing_user
else
BTW, In my case this is a CRM application so we are inviting users to Teams rather than Projects obviously
Awesome episode as usual Chris.
How would you modify this to do a single click invite? i.e. say you are looking through a list of users and you want to "invite them" to your project or w/e, how would you do that?
Just create a new action on ProjectUsers controller, pass the email associated with that link that was pressed and then execute `User.invite!`?
The reason I am asking is that I was trying to pass params to the devise_invitable controller and it seems to be more tricky than you would expect it to be on the surface, so I am trying to explore other ways to do do what i am trying to do.
Yeah exactly. I would create my own controller and pass over the user_id which can then look up the user and associate them. Since the user might already have an account, you might just send them an email notice saying they now have access rather than an invite since they already have an account. You could also do an approval process there before it's finally accepted, kind of up to you.
Chris, was able to get emails sent to already-existing users this way below from the ProjectUser model
def set_user_id
existing_user = User.find_by(email: email)
self.user = if existing_user.present?
existing_user
else
User.invite!(email: email)
end
if existing_user.present?
InviteMailer.existing_user_invite(email).deliver
end
end
About your post here about creating a separate controller, is there a better way to deal with existing users and identify project params? I don't think you can do so from the model here above so assuming that's why you said do it from controller. But issue I have or misunderstanding is how to invite both nonexisting users (new users) and already existing users from the same form input so you wouldn't have to set up two different ones - one for new devise invitable users and a separate one for already-existing users. Seems unnecessary or confusing to users. Tried to overried the devise controller but kind of confusing here and doesn't act as expected from the devise action that is there to invite already existing users. Tried to add it from their documentation but only messes up other things here I'm guessing because of the model setup and inheritance issues. Any quick solution so can send send projectuser or project params to the already-existing user in the email? Thanks a lot.
Chris great episode, thank you so much! One question, everything works great on my end. But, the emails don't actually reach their destinations. Is that because we are in development or do we need to have something extra installed in our app? Thanks mate!
If you've got a real Actionmailer config setup to hit a real smtp server, you can have it send real emails in development. If they aren't arriving, then you will want to double check your configuration to make sure that it's authenticating correctly and you see the emails being sent from your email provider logs.
If you want add "remove user" functionality, here is an example.
In project_users_controller.rb :
def destroy
@project.project_users.find(params[:id]).destroy
redirect_to projects_path, notice: "Member removed"
end
In projects/show.html.erb :
<h4>Users</h4>
<% @project.project_users.each do |project_user| %>
<div><%= project_user.user.email %>
<td><%= link_to "Remove member",
project_project_user_path(@project, project_user), method: :delete %></td></div>
<% end %>
The video seems different then the text. I get an error when following the text. When we make the edit to app/views/projects/show.html.erb. I also was getting an error when I followed the video alone. I had to combine what was in the text and video to make it to where I am at now(still not done). Can we get a repo or an update on this? I actually joined for this very lesson.
If I follow the text I get this error:
NoMethodError in Projects#show, undefined method `email' for #
Oh this was b/c in the text it says to add the email to the user model but in the video you add the email to the project_user model.
How do you actually check that role field later, i.e. vs `current_user`?
I figured it out (not perfect, but it works):
@owner = @project.project_users.where( role: "owner" ).first.user
Chris thanks for this great series. If you wanted to allow some projects to be publicly viewable (i.e. not just for invited people who get invited through devise invitable), is there an easy way to toggle this with a 'public/private' option. Just having trouble figuring out how to model in the database. Can create new projects - that's great - but it seems that all new projects must be associated with invited project users. How to have the option to have some publicly available so that not just project users can view them? Any help would be greatly appreciated. Thanks.
Generally for that, you'd want a public
or private
boolean on the Project.
Then you'd change your query for finding the project. A high level example:
def set_project
@project = Project.find(params[:id])
# Private projects require use to verify the user has access.
if @project.private?
raise ActiveRecord::NotFoundError unless @project.users.include?(current_user)
end
end
Ideally, you'd use something like Pundit to do the authorization. That way it can be applied always and not be forgotten somewhere.
Thanks Chris. Makes it less complicated than having a separate user has_many projects table if that's even possible given the already-existing join table. I will check out the pundit gem also through Cancancan seems to be working fine so far for me. For the devise invitable links that are sent out in the email notifications here, they all seem to go to root page or sign in page but maybe I'll check out the documentation to see if there's a way to get the url pointing to the project itself after signing in but if you have any advice on that would appreciate it. Your great series saves a lot of time and really appreciate your insights.
Yeah, basically private projects need a ProjectUser association to keep track of who has access. A public one can ignore that since it's open to everyone.
If you're already using CanCan, keep using it. Pundit's just an alternative.
Devise invitable does have a method for doing that, it's in the docs somewhere. 👍
And glad I can be of help!
Chris if you wanted to change button text based on whether someone has been invited to a group (saying already invited for instance) would you go the route of just doing it through js entirely like in the button ainimations or would you do css selectors based on database values that proved invitation? I guess it must br a combination of both? Just seems more complex in this case of invitations going out because it isn't a simple boolean or some other db object that id being referenced by css with id selectors but instead there's an actual invitation outstanding to a particular person . Is this too much database referencing to efficiently and quickly render say an index page of people who have the correct invite or invited buttons? Just wondering how to go about this useful button feature without slowing or messing up an index page. Your insights are always valuable - thank you
I would use the database values. It's not going to slow you down to do that, since you'll already have the database records in memory to display them on the page. You can then do whatever you find easiest to change the buttons.
I would probably just use an if statement to check the different statuses of a user's invitation and then display different buttons accordingly.
I've seen this implemented in other apps and I'm trying to extend it. It's not neccesiarly a devise invitable question but a general question using a role
attribute.
Take a User that has and belongs to many Stores via a Team join table. A User that owns the Store has an Team role attribute of Owner, and he can invite Team Members to collaborate with restricted permissions via Pundit. Team Members that are invited have a Team role attribute of User (for restricted permissions). An Owner could promote a Team Member from User to Owner status (to gain full permissions over the Store model). An Owner can also demote a Team Member from Owner to User. How can I add a guard to only allow an Owner to be demoted if there is another Owner of the Store? Meaning, a Store could never be left without an Owner. make_user
should have some type of return if the current_company.members.owners is equal to or less than 1. What's a logical way to add that guard or is the above sufficient?
def make_owner
member = current_company.members.find(params[:id])
member.update_attribute(:role, "owner")
redirect_to admin_dashboard_path, notice: "#{member.user.email} is now an owner."
end
def make_user
member = current_company.members.find(params[:id])
member.update_attribute(:role, "user")
redirect_to admin_dashboard_path, notice: "#{member.user.email} is now a user."
end
I would add a validation to the TeamMember for that.
class TeamMember
belongs_to :team
validate :team_has_owner
def team_has_owner
error.add(:base, "Team must have at least one owner.") unless team.team_members.where(role: :owner).exists?
end
end
I felt close with this one! If you see the trace below it still manages to demote the Owner to a User, and then when trying to promote the User back to an Owner, it throws "Team must have at least one owner", since it just demoted the last and only owner. How can you check that as the only Owner, you can't demote yourself? unless team.team_members.where(role: :owner).exists?
will return true.
Member Exists? (0.5ms) SELECT 1 AS one FROM "members" WHERE "members"."store_id" = $1 AND "members"."role" = $2 LIMIT $3 [["store_id", 1], ["role", 0], ["LIMIT", 1]]
15:15:12 web.1 | ↳ app/models/member.rb:21:in `store_has_owner'
15:15:12 web.1 | Member Update (0.4ms) UPDATE "members" SET "role" = $1, "updated_at" = $2 WHERE "members"."id" = $3 [["role", 1], ["updated_at", "2019-08-26 19:15:12.965431"], ["id", 1]]
15:15:12 web.1 | ↳ app/controllers/admin/members_controller.rb:39:in `make_user'
15:15:12 web.1 | (0.6ms) COMMIT
15:15:12 web.1 | ↳ app/controllers/admin/members_controller.rb:39:in `make_user'
15:15:12 web.1 | Redirected to http://lvh.me:5000/admin/dashboard
15:15:12 web.1 | Completed 302 Found in 24ms (ActiveRecord: 3.8ms | Allocations: 9037)
Hi Chris,
Im kind of lost for ideas, hope you have time to help/give a hint in the right direction.
- Im using Devise and Devise Invitable.
- I have created two models user and admin.
Now I want to be able to have Admins invite Users, but I don't now how to do that and have Googled until my fingers bleed :)
In my routes I only have the normal User routes to the invitation pages.
Hope my question make sense :)
Nothing special for that really. If you have two models, you have current_user
and current_admin
because Devise separates those out. Devise Invitable has a polymorphic invited_by
column, so you can pass in any object for that.
Your invite code would look like this:
User.invite!({ email: 'new_user@example.com' }, current_admin)
Hey Chris, I'm trying to figure out your solution above, User.invite!({email:
with the solution you presented in this video where you added a set_user_id method to ProjectUser.rb
new_user@example.com'}, current_admin)
Since current_admin is a controller helper, how would you pass that to the ProjectUser model? Thanks!
Hey there! thanks a lot for the video, I learned a lot from it. I was wondering if you could provide any guidance on how to customize the email that the user will receive, I would like to be able to mention the name of the project for instance in the email, I'm pretty new to rails and havent been able to figure this out yet.
You can copy their mailer and views from the gem into your app and then make any tweaks you need. 👍
thanks Chris! How can I pass more parameters to this mail views? I generated them already but I'd like my email to say "you have been invited to collaborate on the <=% @project.name %> project.". I know its super basic but I'm a bit lost there
I'm wondering the same thing. There's a bunch of answers out there but none of them seem to leverage the built-in args* on invite!
. I can see the options being passed into sidekiq but I'm not sure how to access them in the template.
Answering my own question here - the opts or args splat is not for random additional information in devise mail methods, it's specifically for headers. https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise/mailers/helpers.rb#L31
Setting up a custom invitation email with more information is how to do it. You can modify the invite!
method by passing a block. For example:
user.invite! do |user|
user.skip_invitation = true
user.skip_confirmation!
end
# NOTE We must use deliver_now to not leak invitation token to Redis
CustomMailer.custom_mail_method(user, project_name).deliver_now