Rails Devise - How to add more data to current_user - ruby-on-rails

Suppose I have a User model
user.rb
class User < ActiveRecord::Base
...
end
with attributes like: name, username, access
access is an enum that tells me if the user is "staff" or "customer"
To get the name and username of the logged in user, I can do:
current_user.name
current_user.username
And suppose I have a Staff model
staff.rb
class Staff < ActiveRecord::Base
belongs_to :user
end
with attributes like: salary, phone_number
And I also have a Customer model
customer.rb
class Customer < ActiveRecord::Base
belongs_to :user
end
with attributes like: address, phone_number
I want to be able to call this on my staff's controller:
current_user.staff.salary
And this on my customer's controller:
current_user.customer.address
WHAT I TRIED SO FAR
I overwrote sessions_controller.rb
def create
super
model_name = current_user.access.capitalize.constantize
spec = model_name.where(user_id: current_user.id).take
session[:spec] = spec
end
So I'm able to access it via session[:spec], but not via current_user. Any ideas?

Well to begin with, your User model should reference the staff or customer, even if they are to stay blank
class User
has_one :staff
has_one :address
Just by doing this, you should be able to use current_user.customer.address. However...
I suggest you add some convenient methods in ApplicationController or a module that you include
def staff_signed_in?
#staff_signed_in ||= (user_signed_in? and current_user.access == :staff)
end
def current_staff
#current_staff ||= (current_user.staff if staff_logged_in?)
end
# same for customer
# Note that I use instance variables so any database queries are executed only once !
Then you can simply call
<% if customer_signed_in? %>
<h2>Logged in as customer</h2>
<p>Address : <%= current_customer.address %>
<% end %>
EDIT : about your concerns concerning database hits
You gave the example of current_user.customer.cart.products
This is indeed quite a nested association. My suggestion above already reduces it by one level (ie current_customer == current_user.customer). Then you have to go through carts to reach products... it isn't so bad in my opinion.
If you need to call that often (current_customercustomer.cart) you can override the current_customer for a given controller and eager load the resources you know you will use use.
def UserShopController < ApplicationController
# Let's assume current_customer is defined in ApplicationController like I showed above
# UserShopController always uses the customer cart, so let's load it right at the beginning
...
private
# Override with eager loading
def current_customer
#current_customer ||= (current_user.customer.includes(:cart) if customer_logged_in?)
end

add has_one :customer to your user.rb
Your user model should be like below to accessing related model.
class User < ActiveRecord::Base
has_one :customer
end

Related

Rails 4 + Devise: After create user, create XYZ

I have a User model that gets created through Devise, and after it's creation, I would like to automatically create a new Client (another model in my app). The new Client's atrribute, :user_id, should be equal to the :id of the User that was just created. I believe I need to use something like:
class Users::RegistrationsController < Devise::RegistrationsController
after_create :create_client
def create_client
Client.create(:user_id, :id) # Not sure what should go here
end
end
Is this the correct way to accomplish this? Also, if associations are important Client belongs_to :user and User has_one :client
You can add an after_create callback in User model(user.rb), check here for more information on how to create has_one associations.
class User < ActiveRecord::Base
after_save :add_client
def add_client
self.create_client(client_attribute1: value, client_attribute2: value)
end
end

Event Registration Form

newbie here...
I am trying to create an events registration page where anybody can register for an event without logging into the system.
My problem is trying to figure out how to tie the registration info to the specific event. I've created all the associations but can't figure how to tell the db that the person is registering for a specific event.
Here are my associations:
class Event < ActiveRecord::Base
belongs_to :user
has_many :event_regs
has_many :regs, through: :event_regs
class Reg < ActiveRecord::Base
has_many :event_regs
class Reg < ActiveRecord::Base
has_many :event_regs
Thanks in advance
Newbie here
Welcome!
Here's what you'll need:
#app/models/event.rb
class Event < ActiveRecord::Base
has_many :registrations
end
#app/models/registration.rb
class Registration < ActiveRecord::Base
belongs_to :event
end
This will allow you to use the following:
#config/routes.rb
resources :events do #-> url.com/events/:id
resources :registrations #-> url.com/events/:event_id/registrations/
end
#app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
def new
#event = Event.find params[:event_id]
#registration = #event.registration.new
end
def create
#event = Event.find params[:event_id]
#registration = #event.registration.new registration_params
end
private
def registration_params
params.require(:registration).permit(:all, :your, :params)
end
end
This will create a new registration record in your db, associating it with the Event record you've accessed through the route.
--
From this setup, you'll be able to use the following:
#app/controllers/events_controller.rb
class EventsController < ApplicationController
def show
#event = Event.find params[:id]
end
end
#app/views/events/show.html.erb
<% #event.registrations.each do |registration| %>
# -> output registration object here
<% end %>
Foreign Keys
In order to understand how this works, you'll be best looking at something called foreign keys...
This is a relational database principle which allows you to associate two or more records in different database tables.
Since Rails is designed to work with relational databases, each association you use will require the use of a "foreign key" in some respect.
In your case, I would recommend using a has_many/belongs_to relationship:
You'll need to make sure you add the event_id column to your registrations database.

When updating a nested form, how do I do something with new records?

class Photo < ActiveRecord::Base
has_many :item_photos
has_many :items, through: :item_photos
end
class Item < ActiveRecord::Base
has_many :item_photos
has_many :photos, through: :item_photos
accepts_nested_attributes_for :photos
end
class ItemPhotos < ActiveRecord::Base
belongs_to :photo
belongs_to :item
end
When I edit or create an Item, I also upload or remove Photos. However, more than one user can view an Item. These users should only be able to view their own Photos.
Item #1 has three Photos. Amy has access to one. Barry has access to two. Barry loads up /items/1 and edits it. He deletes one Photo, ignores the other, and adds two new Photos.
class ItemsController < ApplicationController
def update
if #item.update(item_params)
# Give Barry access to the Photo he just made.
#item.only_the_photos_barry_just_made.each do |p|
current_user.add_role :viewer, p
end
end
end
end
I don't want to pollute models/photo.rb with methods to access session information (like current_user). Is there an idiomatic way to get these records? If not, is there a clean way to get these records?
Thanks for any help.
A simple solution would be to add a :creator relation to photo.
rails g migration AddCreatorToPhotos creator:references
class Photo < ActiveRecord::Base
belongs_to :creator, class: User
# ...
end
And then you can simply add a rule in your Abilty class:
class Ability
include CanCan::Ability
# Define abilities for the passed in user here.
# #see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
def initialize(user = nil)
user ||= User.new # guest user (not logged in)
# ...
can :read, Photo do |photo|
photo.creator.id == user.id
end
end
end
You can then get the photos that can by read by the current user with:
#photos = #item.photos.accessible_by(current_ability)
Edit:
If you want to authorize though roles instead you just need to alter the conditions in the authorization rule:
class Ability
include CanCan::Ability
# Define abilities for the passed in user here.
# #see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
def initialize(user = nil)
user ||= User.new # guest user (not logged in)
# ...
can :read, Photo do |photo|
user.has_role? :viewer, photo
end
end
end
Edit 2:
An approach to creating the role could be to add a callback to Photo. But as you already have surmised, accessing the user via the session from a model is not a good approach.
Instead you can pass the to the user to Photo when it is instantiated. You can either setup the belongs_to :creator, class: User relationship or create a virtual attribute:
class Photo < ActiveRecord::Base
attr_accessor: :creator_id
end
You can then pass the user by a hidden field (remember to whitelist it!):
# GET /items/new
def new
#item = Item.new
#item.photos.build(creator: current_user) # or creator_id: current_user.id
end
<%= fields_for(:photos) do %>
<%= f.hidden_field :creator_id %>
<% end %>
So, how do we create our callback?
class Photo < ActiveRecord::Base
attr_accessor: :creator_id
after_commit :add_viewer_role_to_creator!, on: :create
def add_viewer_role_to_creator!
creator.add_role(:viewer, self)
true # We must return true or we break the callback chain
end
end
There is one issue though:
We don't want to allow malicious users to be assign their ID to existing photos on update.
We can do this by setting up some custom params whitelisting:
def item_params
whitelist = params.require(:item).permit(:foo, photos_attributes: [:a,:b, :creator_id]
current_user_photos = whitelist[:photos_attributes].select do |attrs|
attrs[:id].nil? && attrs[:creator_id] == current_user.id
end
whitelist.merge(photos_attributes: current_user_photos)
end

Saving association I.D upon creation?

If I have a user that has_many user_logins and a user_logins that belongs to user - When a user_login is created I'm using UserLogin.create(userlogin_params) and then my strong params permits the user_id column - but this alone is not saving the current users I.D to the column as it is coming out as nil.
How do I make it save the I.D?
User model:
has_many :user_logins
UserLogin model
belongs_to :user
accepts_nested_attributes_for :user
UserLoginController:
...
def create
#user_login = UserLogin.new(user_login_params)
...
end
...
def user_login_params
param.require(:user_login).permit(
:user_login_attribute1,
:user_login_attribute2,
user_attributes: [
:id,
:user_attribute1,
:user_attribute2
]
)
end
Tell me if it helps.
There are 2 issues here at hand.
First: How do you create an association with the parent record automagically there?
Second: How do you do this so your controller action isn't a giant hole waiting for a hacker to stick their nose in.
You need to start from the parent, then build the child, not start with the child and build the parent.
Consider the following:
class User < ActiveRecord::Base
has_many :logins, class_name: "UserLogin"
end
class UserLogin < ActiveRecord::Base
belongs_to :user
end
class UserLoginsController < ApplicationController
def create
if new_user_login(user_login_params).save
redirect_to :wherever
else
render :new
end
end
private
def new_user_login(attrs={})
current_user.logins.create(attrs)
end
def user_login_params
param.require(:user_login).permit(:attr_1, :attr_1)
end
Do not pass IDs into any secure params hash unless that ID is selectable by the user. If you allow an ID into secure params, a hacker can start moving records around to other objects and destroy your database integrity.
If you would like pairing help on this problem live and in person, you can check out my codementor profile at https://codementor.io/rubycasts/#reviews

Creating a one-to-one relation

class User
has_one :super_admin
end
class SuperAdmin
belongs_to :user
end
How would I create a SuperAdmin instance associating a certain user, from withing the User model?
I'm looking for something like this (in the User model), but it's not working:
def promote_to_super
self.super_admin.create!
end
You can use create_association(attributes = {}):
def promote_to_super
self.create_super_admin
end
See more here.

Resources