I am trying to get to grips with the basics of authentication in Rails. To start with I have used the nifty_authentication generator by Ryan Bates. It's helping me learn the basic options for user logins etc.
I have a simple application the has a person and gift table in the database. The idea is, each user creates a list of people and then assigned possible gifts to each of those people.
So from a structural point of view:
person belongs to user
gift belongs to person
So I have the models set up as follows.
person model
class Person < ActiveRecord::Base
has_many :gifts
end
gift model
class Gift < ActiveRecord::Base
belongs_to :person
end
user model
currently doesn't contain any belongs_to has_many etc.
How do I go about making sure each user has their own list of people. So one user cannot see another users list of people or gifts.
Would I simply add the following to the user model?
has_many :people
and the following to the person model?
belongs_to :user
Would that work, or am I missing something?
Thanks,
Danny
UPDATE:
The app so far is on Heroku and Github.
http://giftapp.heroku.com/
http://github.com/dannyweb/GiftApp
Would that work, or am I missing
something?
Very short answer: yes that would work; no you are not missing something.
I looked at your code.
Instead of:
def index
#people = Person.find(:all)
end
You need something along the lines of:
def index
#people = current_user.people
end
Where current_user is the User object that refers to the logged in user.
In the create method you will need to assign the newly created person to the current_user:
def create
#person = Person.new(params[:person])
#person.user = current_user # This associates #person with current_user
if #person.save
flash[:notice] = "Successfully created person."
redirect_to #person
else
render :action => 'new'
end
end
Related
My Rails5 application has an organization model and a user model (1:n relationship). The workflow of creating an organization should include the creation of the organization's first user as well. I thought this would be able with ActiveRecord through nested models, however the create action fails with the error message "Users organization must exist".
class Organization < ApplicationRecord
has_many :users, dependent: :destroy
accepts_nested_attributes_for :users
end
class User < ApplicationRecord
belongs_to :organization
end
class OrganizationsController < ApplicationController
def new
#organization = Organization.new
#organization.users.build
end
def create
#organization = Organization.new(organization_params)
if #organization.save
redirect_to #organization
else
render 'new'
end
end
def organization_params
params.require(:organization).permit(:name, users_attributes: [:name, :email, :password, :password_confirmation])
end
end
In the view I use the <%= f.fields_for :users do |user_form| %> helper.
Is this a bug on my side, or isn't this supported by ActiveRecord at all? Couldn't find anything about it in the rails guides. After all, this should be (theoretically) possible: First do the INSERT for the organization, then the INSERT of the user (the order matters, to know the id of the organization for the foreign key of the user).
As described in https://github.com/rails/rails/issues/18233, Rails5 requires integrity checks. Because I didn't like a wishy-washy solution like disabling the integrity checks, I followed DHH's advice from the issue linked above:
I like aggregation through regular Ruby objects. For example, we have a Signup model that's just a Ruby object orchestrating the build process. So I'd give that a go!
I wrote a ruby class called Signup which encapsulates the organization and user model and offers a save/create interface like an ActiveRecord model would. Furthermore, by including ActiveModel::Model, useful stuff comes in to the class for free (attribute hash constructor etc., see http://guides.rubyonrails.org/active_model_basics.html#model).
# The Signup model encapsulates an organization and a user model.
# It's used in the signup process and helps persisting a new organization
# and a referenced user (the owner of the organization).
class Signup
include ActiveModel::Model
attr_accessor :organization_name, :user_name, :user_email, :user_password, :user_password_confirmation
# A save method that acts like ActiveRecord's save method.
def save
#organization = build_organization
return false unless #organization.save
#user = build_user
#user.save
end
# Checks validity of the model.
def valid?
#organization = build_organization
#user = build_user
#organization.valid? and #user.valid?
end
# A create method that acts like ActiveRecord's create method.
# This builds the object from an attributes hash and saves it.
def self.create(attributes = {})
signup = Signup.new(attributes)
signup.save
end
private
# Build an organization object from the attributes.
def build_organization
#organization = Organization.new(name: #organization_name)
end
# Build a user object from the attributes. For integritiy reasons,
# a organization object must already exist.
def build_user
#user = User.new(name: #user_name, email: #user_email, password: #user_password, password_confirmation: #user_password_confirmation, organization: #organization)
end
end
Special thanks to #engineersmnky for pointing me to the corresponding github issue.
You're looking for "Association Callbacks". Once you send those params to your organization model you have access to them inside that model. If everytime an organization is created there will be a new user assigned to it you can just do the following in your Organization Model:
has_many :users, dependent: :destroy, after_add: :create_orgs_first_user
attr_accessor: :username #create virtual atts for all the user params and then assign them as if they were organizational attributes in the controller. This means changing your `organization_params` method to not nest user attributes inside the array `users_attributes`
def create_orgs_first_user
User.create(name: self.username, organization_id: self.id, etc.) # You can probably do self.users.create(params here) but I didn't try it that way.
end
The "Users organization must exist" error should not occur. ActiveRecord is "smart," in that it should execute two INSERTs. First, it will save the model on the has_many side, so that it has an id, and then it will save the model on the belongs_to side, populating the foreign key value. The problem is actually caused by a bug in accepts_nested_attributes_for in Rails 5 versions prior to 5.1.1. See https://github.com/rails/rails/issues/25198 and Trouble with accepts_nested_attributes_for in Rails 5.0.0.beta3, -api option.
The solution is to use the inverse_of: option or, better yet, upgrade to Rails 5.1.1.
You can prove that this is true by removing the accepts_nested_attributes_for in your Organization model and, in the Rails console, creating a new Organization model and a new User model, associating them (eg myorg.users << myuser) and trying a save (eg myorg.save). You'll find that it will work as expected.
I implemented a signin/out machinery as Michael Hartl suggested in his tutorial (http://ruby.railstutorial.org/chapters/sign-in-sign-out). All worked perfectly: creating, deleting, updating user from a profile page.
Then I created a Teacher model as a subclass of User (STI). It hasn't its own validation and should use the same validation as parent model (user).
But updating attributes for user with type = "Teacher" is no more possible (for the others users it continues to work). When I compile the update form it returns:
`error messages say me teacher.attributes.password is missing validation
while the teacher should inherit the same validation of user.
In routes.rb, after the creation of the teacher model I added:
resources :teachers, :controller => 'users', :type => "Teacher"
This is the update method in the user controller:
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
redirect_to #user ...
If I change #user.update_attributes(params[:user]) to #user.update_attributes(params[:teacher]) It works, but obviously only for teacher.
I think the solution is quite easy but I'm a novice.
Any help would be appreciated!
I solved my problem thanks to the solution proposed by d11wtq here:
Is subclassing a User model really bad to do in Rails?
I edited the Teacher model like this:
class Teacher < User
def self.model_name
User.model_name
end
end
So I know this question has been ask a ton of times but my question goes a little bit further.
When modeling my application I have two types of users that have a polymorphic association to the user model. Such as:
class User < ActiveRecord::Base
belongs_to :profileable, :polymorphic => true
end
class User_Type_1 < ActiveRecord::Base
has_one :user, :as => :profileable
end
class User_Type_2 < ActiveRecord::Base
has_one :user, :as => :profileable
end
The reason I did this, instead of an STI, is because User_Type_1 has something like 4 fields and User_Type_2 has something like 20 fields and I didn't want the user table to have so many fields (yes 24-ish fields is not a lot but I'd rather not have ~20 fields empty most of the time)
I understand how this works, my question is I want the sign up form to only be used to sign up users of type User_Type_1 but the sign in form to be used to both. (I will have an admin side of the application which will create users of User_Type_2)
I know I can use the after_sign_in_path_for(resource) override in AppicationController somehow to redirect to the right part of the site on sign in. Something like:
def after_sign_in_path_for(resource)
case current_user.profileable_type
when "user_type_1"
return user_type_1_index_path
when "user_type_2"
return user_type_1_index_path
end
end
So I guess my questions are how would I make the form to work with Devise and only allow signups of type User_Type_1 and then sign them in after sign_up?
Also, if I am going about this the wrong way, what is the right way?
I was able to answer my own question and am putting it here so that maybe it can help someone else with the same problem.
The login problem was easy, just use the default devise login and the after_sign_in_path_for in ApplicationController as described above
I realized the answer to the form question as typing it out here:
I just created a normal form for the User_Type_1 with nested attributes for User
and had it post to the UserType1Controller
Then saved both objects and called the sign_in_and_redirect helper from Devise
class UserType1Controller < ApplicationController
...
def create
#user = User.new(params[:user])
#user_type_1 = UserType1.new(params[:patron])
#user.profileable = #user_type_1
#user_type_1.save
#user.save
sign_in_and_redirect #user
end
...
end
Then the after_sign_in_path_for method from above sent it to the right place and it was all good.
How can I make it so that an organization is only created if the user is NOT signed in?
That's a little vague, so let me make that a little clearer. I have two tables; one organization table, and another user table. Organizations has many users, and users belong to an organization.
# models/organizations.rb
class Organization < ActiveRecord::Base
has_many :users
I made it so that when a user signs up, an organization is created along with his account.
# models/user.rb
class User < ActiveRecord::Base
belongs_to :organization
before_create :create_organization
private
def create_organization
self.organization = Organization.create :name => self.name
end
Cool, that works.
But users should also be able to sign other people up for an account, and that new user's organization_id should be the same as current_user.organization_id
*# controllers/users_controller.rb*
def create
#user = User.new(params[:user])
if current_user
#user.organization_id = current_user.organization_id
end
end
This solution is limiting me, because the model is creating an organization even if the user is already signed in. They work independently (if I take out either the before_filter in the model or the if current_user in the controller), but not together.
I'm having a difficult time making them work together because you can't access current_user in the user model.
I'd put the organization_id as a paramater in the sign up link, but that's not very secure as that means any user could simply change the organization_id in the URL and automatically get an account to another organization that doesn't belong to them.
If it matters, I'm using Authlogic as my authentication solution.
Is there anything I can do in the user model or the user controller to accomplish this?
Just check if organization is present in the before_filter:
def create_organization
self.organization = Organization.create(:name => self.name) unless self.organization.present?
end
If a user is signed in, the new user will be associated with an organization already and the before_filter will not create a new one.
I want to add the ability for users to invite a friend.
The email should be generated so that, if someone clicks on the link and register, that person is automatically a friend.
Not sure what the options are, but wanted some ideas and strategies as an alternative to building it from scratch.
I'm not aware of any gems that handle the entire process (user >> email >> signup). If you're just looking to create the relationship when a user comes from a specific link, create a special invitation route (the separate controller isn't necessary but just to make it clear):
# routes.rb
match '/invite/:friend_id' => 'public#invite', :as => :invite
# PublicController
def invite
session[:referring_friend] = params[:friend_id]
redirect_to root_path
end
# UsersController
def create
#user = User.new(params[:user])
if #user.save
#user.create_friendship(session[:referring_friend]) if session[:referring_friend]
...
else
...
end
end
If you want to track conversion metrics, I'd recommend creating a link model and using that to track clicks and signups:
class Link < ActiveRecord::Base
belongs_to :user
attr_accessible :user, :user_id, :clicks, :conversions
def click!
self.class.increment_count(:clicks, self.id)
end
def convert!
self.class.increment_count(:conversions, self.id)
end
end
# routes.rb
match '/invite/:link_id' => 'links#hit', :as => :invite
# LinksController
def hit
link = Link.find(params[:link_id])
link.click!
session[:referring_link_id] = link.id
redirect_to root_path # or whatever path (maybe provided by link...)
end
# UsersController
def create
#user = User.new(params[:user])
if #user.save
if session[:referring_link_id]
link = Link.find(session[:referring_link_id])
link.convert!
#user.create_friendship(link.user_id)
end
...
else
...
end
end
Which method you choose depends on what you'll want to track down the road.
I don't know gem for rails. But there's an extension for Spree, rails based e-commerce project. Check it out & probably you can refer how it's implemented.
https://github.com/spree/spree_email_to_friend
I don't know about some gem to support this, but solution should be rather trivial. I guess you need Friendship model, you can place some status in it like 'waiting_for_approvment' and send in mail link with that Friendship model id. When user accepts either way you just change status to 'approved' or even 'rejected' if you want to track that too.
Start by defining the relationship:
class User < ActiveRecord::Base
has_and_belongs_to_many :friends, :class_name => "User", :join_table => "friends_users"
end
So really, User relates to itself with a different name. Then you can use something along the lines of:
#current_user.friends << #selected_user
in your controller.