Rails Polymorphism for User modeling - authentication

I'm new to Rails 5 and I'm trying to model the following scenario using Devise and CanCanCan: a store application.
Relevant figures are: Admin, StoreManager, StoreOfficer, Customer, Technician.
Admin creates StoreManager.
StoreOfficer can create Customer's information to register new Customers.
StoreOfficer can see the complete list of all Customers with relative information
StoreOfficer can see the complete list of all Technicians with relative information
StoreManager can see the complete list of all StoreOfficers with relative information
StoreManager can enable StoreOfficers to use the system and edit their information
StoreManager can enable Customers created by StoreOfficers to use the system
StoreManager can see the complete list of all Customers with relative information
There are also some other paths, but it's possible for me to develop them as these presented cases are done.
Any help/tutorial please?
Thanks,
FZ

After trying some different things, I created the following satisfying schema, which I post here for others so that they can use something solid:
Class User<ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
belongs_to :userable, polymorphic: true
end
Then all the other roles classes similar to the following one:
class Customer < ApplicationRecord
belongs_to :technician
belongs_to :store_officer
has_one :user, as: :userable, dependent: :destroy
has_one :address, as: :addressable, dependent: :destroy
accepts_nested_attributes_for :user, :address
end
Accordingly, I created all the needed fields using migrations in the database.
Then I put in every controller the code before_action :authenticate_user!.
After populating the db a bit, I can now login with different users inserted using rake db:seed.
This is my routes.rb file:
Rails.application.routes.draw do
devise_for :users
resources :store_managers
resources :store_officers
resources :items
resources :technicians
resources :customers
resources :addresses
resources :users
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
What do you all think about this setup?
Now I want to introduce CanCanCan to my project. Please validate the following steps I'm going to do:
Run rails g cancan:ability
Put all the rules in the ability.rb file:
if user.admin?
can :manage, :all
elsif user.store_manager?
can :read, Customer
can :create, Customer
can :update, Item do |item|
item.try(:user) == user
end
can :destroy, Item do |item|
item.try(:user) == user
end
elsif user.store_officer?
can :read, Customer
end
.
.
.
Create the methods in user.rb model to check roles:
def customer?
self.userable_type == Customer
end
Put load_and_authorize_resource in every controller
Do you think this approach is the correct one? Or maybe I should put methods/definitions in other files?
Thanks,
FZ

Related

Tricky Ruby/Rails loop and query logic. Advice needed. SQL join or group?

I'm still pretty green in Ruby, Rails, and SQL (and programming in general) but I know the basics. Here's my little puzzle involving a wine cellar organization app:
I have models. Wine - with the attributes :name and :vintage, Winery - which Wine belongs to, User - who has many wines, Location - the wine's cellar location which is a string attribute :name, and Bottle - every single bottle has a record with user, wine, and location ids. Here's my gist with models, schema, the controller, and the view I want to work with:
https://gist.github.com/mpron/8725840
Here's what I want to do:
Show rows of a user's wine sorted alphabetically by wine name
I want to show multiple rows of the same wine name if bottles of that wine exist at multiple locations (if there are bottles with the same wine name in 3 different locations, then I want to show three separate rows for each location)
I need to show the number of wine bottles at each location on each of those rows for that one wine name (so a bottle '.count' for the bottles with the same wine.name).
Here is my rough, slightly pseudocode solution that I have so far:
- if #bottles.count > 0
You have
= #bottles.count
bottles
%table.table
%tr
%th Location
%th Vintage
%th Name
%th Winery
%th Quantity
%th Actions
- #wines.each do |wine|
- #locations each do |loc|
- if wine.#bottles.each exists in loc
%tr
%td
= "#{loc}"
%td
= "#{wine.vintage}"
%td
= "#{wine.name}"
%td
= "#{wine.winery.name}"
%td
= "#{loc.bottles.where(name: wine.name).count}"
%td
= link_to "Delete", bottle_path(bottle), :method => "delete", :data => {:confirm => "Are you sure?"}, :class => "btn btn-danger btn-sm"
This is also in the gist along with other relevant files.
My questions are:
1. What is the best way to query the database efficiently for this data?
2. Does this belong in the view, or should it exist somewhere else, maybe a partial?
If there's a more efficient way to model this, I'm open to significantly revamping this. I just don't know enough SQL to figure out how I might use something like GROUP BY or a JOIN to make this work well. A friend suggested something like Location.joins(:bottles).where(name: wine.name) but later decided it might result in bad performance.
(this is for a personal project, not homework or anything. I'm just stumped.)
My initial answer to your question would be to go with better associations. Refactor how your associations are structured. For instance, remove the association of belongs_to :user from bottle:
class Bottle < ActiveRecord::Base
belongs_to :wine
# belongs_to :user
belongs_to :location
end
Add a belongs_to in the wine model:
class Wine < ActiveRecord::Base
belongs_to :user
belongs_to :winery
has_many :bottles
#has_many :locations
end
And add a has_many through: to user:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :wines
has_many :bottles, through :wines
end
You will have to adjust your columns accordingly to get rid of foreign keys you don't need and add any new ones you need. You don't need an explicit key for has_many through as Rails will do the work for you. user -> wine -> bottles See this as a decent example: Getting count by drilling down through relations
This will give you the ability to just pull a User:
#user = User.first
and you will automatically get joins that can be access with methods like:
my_wines = #user.wines # wines in a user's cellar
my_bottles = #user.bottles # bottles in a user's cellar
these all return ActiveRecord Relations that represent arrays of AR objects. You can then access those object like:
my_bottles.each do |bottle|
puts bottle.name
puts bottle.location
end
I actually needed to learn more about using the .joins method because I had learned the has_many through so early on I didn't even realize it was doing joins for me. At the very least do some tutorials on has_many through and anything you can find on model and helper methods. The CodeSchool.com link I posted in the comments is a HUGE help. It's $25 for a month but worth every cent.
.joins is good for when you need to bring together information in separate tables that doesn't have the explicit associations you've defined. Or when you want to pull the information from two table, but just the info that relates to a certain user. For performance I would watch out for doing any kind of SQL request inside of a loop. I didn't see any here, but just a heads up.
Your view will be much cleaner by removing all of the nested loops and laying out your logic in associations and in a helper method or two. Think of it this way, that triple nested loop in your view is basically a layered object. You should leverage your associations and build some methods that encapsulate that logic so you only have one .each statement in your view. Refactor you associations, and if you still don't see an easier way to encapsulate your view logic let me know and I can give some more direction.
#Beartech helped get me thinking about how I could utilize the has_many through: associations, so after re-studying those, I came up with my solution:
wine.rb
class Wine < ActiveRecord::Base
belongs_to :winery
has_many :bottles
has_many :locations, through: :bottles
end
user.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :wines, through: :bottles
has_many :bottles
end
location.rb
class Location < ActiveRecord::Base
has_many :bottles
has_many :wines, through: :bottles
end
bottle.rb
class Bottle < ActiveRecord::Base
belongs_to :wine
belongs_to :user
belongs_to :location
end
section of users_controller.rb
def show
#user_wines = #user.wines.order('wines.name')
#bottles = #user.bottles
end
show.html.haml
%table.table
%tr
%th Location
%th Vintage
%th Name
%th Winery
%th Quantity
%th Actions
- #user_wines.each do |wine|
- wine.locations.each do |loc|
%tr
%td= loc
%td= wine.vintage
%td= wine.name
%td= wine.winery.name
%td= loc.bottles.where(:wine_id => wine.id).count
That will run through a much narrower subset of the wines in the database, and an even smaller subset of the locations in the database so that my nested loop isn't querying as much.

Implement "Add to favorites" in Rails 3 & 4

I am building an app where users can create recipes, see all recipes created, view their own recipes in a member area and finally i would like for users to add "favorites" to their account.
I am new to Rails but have read through docs and this is my understanding of what it should look like in the backend. Could someone confirm that this looks correct or advise of any errors please, with explanations if I have done something wrong (which is probably the case)?
So this is my code:
User Model
has_many :recipes
has_many_favorites, :through => :recipes
Recipe Model
belongs_to :user
has_many :ingredients #created seperate db for ingredients
has_many :prepererations #created seperate db for prep steps
Favorite Model
belongs_to :user
has_many :recipes, :through => :user
#this model has one column for the FK, :user_id
Favorites Controller
def create
#favrecipes =current_user.favorites.create(params[:user_id])
end
I then wanted to have a button to post to the db, so I have this:
<%= button_to("Add to Favorites" :action => "create", :controller => "favorites" %>
I think I am probably missing something in my routes but I am unsure.
The particular setup you describe mixes several types of associations.
A) User and Recipe
First we have a User model and second a Recipe model. Each recipe belonging to one user, hence we have a User :has_many recipes, Recipe belongs_to :user association. This relationship is stored in the recipe's user_id field.
$ rails g model Recipe user_id:integer ...
$ rails g model User ...
class Recipe < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :recipes
end
B) FavoriteRecipe
Next we need to decide on how to implement the story that a user should be able to mark favorite recipes.
This can be done by using a join model - let's call it FavoriteRecipe - with the columns :user_id and :recipe_id. The association we're building here is a has_many :through association.
A User
- has_many :favorite_recipes
- has_many :favorites, through: :favorite_recipes, source: :recipe
A Recipe
- has_many :favorite_recipes
- has_many :favorited_by, through: :favorite_recipes, source: :user
# returns the users that favorite a recipe
Adding this favorites has_many :through association to the models, we get our final results.
$ rails g model FavoriteRecipe recipe_id:integer user_id:integer
# Join model connecting user and favorites
class FavoriteRecipe < ActiveRecord::Base
belongs_to :recipe
belongs_to :user
end
---
class User < ActiveRecord::Base
has_many :recipes
# Favorite recipes of user
has_many :favorite_recipes # just the 'relationships'
has_many :favorites, through: :favorite_recipes, source: :recipe # the actual recipes a user favorites
end
class Recipe < ActiveRecord::Base
belongs_to :user
# Favorited by users
has_many :favorite_recipes # just the 'relationships'
has_many :favorited_by, through: :favorite_recipes, source: :user # the actual users favoriting a recipe
end
C) Interacting with the associations
##
# Association "A"
# Find recipes the current_user created
current_user.recipes
# Create recipe for current_user
current_user.recipes.create!(...)
# Load user that created a recipe
#recipe = Recipe.find(1)
#recipe.user
##
# Association "B"
# Find favorites for current_user
current_user.favorites
# Find which users favorite #recipe
#recipe = Recipe.find(1)
#recipe.favorited_by # Retrieves users that have favorited this recipe
# Add an existing recipe to current_user's favorites
#recipe = Recipe.find(1)
current_user.favorites << #recipe
# Remove a recipe from current_user's favorites
#recipe = Recipe.find(1)
current_user.favorites.delete(#recipe) # (Validate)
D) Controller Actions
There may be several approaches on how to implement Controller actions and routing. I quite like the one by Ryan Bates shown in Railscast #364 on the ActiveRecord Reputation System. The part of a solution described below is structured along the lines of the voting up and down mechanism there.
In our Routes file we add a member route on recipes called favorite. It should respond to post requests. This will add a favorite_recipe_path(#recipe) url helper for our view.
# config/routes.rb
resources :recipes do
put :favorite, on: :member
end
In our RecipesController we can now add the corresponding favorite action. In there we need to determine what the user wants to do, favoriting or unfavoriting. For this a request parameter called e.g. type can be introduced, that we'll have to pass into our link helper later too.
class RecipesController < ...
# Add and remove favorite recipes
# for current_user
def favorite
type = params[:type]
if type == "favorite"
current_user.favorites << #recipe
redirect_to :back, notice: 'You favorited #{#recipe.name}'
elsif type == "unfavorite"
current_user.favorites.delete(#recipe)
redirect_to :back, notice: 'Unfavorited #{#recipe.name}'
else
# Type missing, nothing happens
redirect_to :back, notice: 'Nothing happened.'
end
end
end
In your view you can then add the respective links to favoriting and unfavoriting recipes.
<% if current_user %>
<%= link_to "favorite", favorite_recipe_path(#recipe, type: "favorite"), method: :put %>
<%= link_to "unfavorite", favorite_recipe_path(#recipe, type: "unfavorite"), method: :put %>
<% end %>
That's it. If a user clicks on the "favorite" link next to a recipe, this recipe is added to the current_user's favorites.
The Rails guides on associations are pretty comprehensives and will help you a lot when getting started.
Thanks for the guide, Thomas! It works great.
Just wanted to add that in order for your favorite method to work correctly you need to wrap the text in double quotes instead of single quotes for the string interpolation to function.
redirect_to :back, notice: 'You favorited #{#recipe.name}'
->
redirect_to :back, notice: "You favorited #{#recipe.name}"
https://rubymonk.com/learning/books/1-ruby-primer/chapters/5-strings/lessons/31-string-basics
This thread was super helpful!!
Thank you!
Don't forget to include the = in the form tags.
<% if current_user %>
<%=link_to "favorite", favorite_recipe_path(#recipe, type: "favorite"), method: :put %>
<%=link_to "unfavorite", favorite_recipe_path(#recipe, type: "unfavorite"), method: :put %>
<% end %>
The selected answer is really good however I can't post a comment and I really do have an question about above. How can you limit a user to have one favourite recipe? With the above answer a user can continue pressing favorite and many entries will be created in the database...

Create "organization" automatically when user signs up?

I'm using Devise for authentication with two custom fields added = :organization_id and :username. I also generated a scaffold for the Organization which simply consists of Name.
Currently when users sign up they can type in an Organization ID (integer) and a username (string).
Users belong_to organizations, and organizations has_many users.
Here's what my models look like (I left everything else untouched except for the files inside app/views/devise/registrations to add organization_id and username):
#user.rb
class User < ActiveRecord::Base
belongs_to :organization
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :organization_id, :username
end
#organization.rb
class Organization < ActiveRecord::Base
has_many :users
end
What I would like is for an organization to be automatically created when a user signs up without the user having to specify the organization_id.
Also, ideally the #organization.name would be exactly the same as :username
How would I go about doing this?
I've seen the Railscast on nested model forms but he's creating a question inside the survey form, and questions belong to a survey. I need to do it the other way around (create an organization inside the user form, where users belong to a survey.)
Any help and suggestions would be greatly appreciated..
class User < ActiveRecord::Base
before_create :create_organization
private
def create_organization
self.organization = Organization.create :name => self.username
end
end
The create_organization method will be executed right before the sql query which creates the user. It'll create a new organization and assign it to the user.

How to break users into managers and attendees?

First here's my current setup:
models/user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
has_many :events
end
models/event.rb
class Event < ActiveRecord::Base
belongs_to :user
end
So, this works in the sense that a user have many events that s/he can CRUD. Anyone that is not logged in can only view and list all the events.
From this initial setup, I now have another set of users that are attendees and the current users are now actually event managers instead of just a generic user. Essentially, I now have two models that are both users (since they can both log in/sign up and have the same fields) and I was thinking that event should read something like:
models/event.rb
class Event < ActiveRecord::Base
belongs_to :manager
has_many :attendees, :through => :attendance # attendance is a join model
end
but I don't want to create a separate table for managers and attendees and I was thinking of sub classing the user model but I'm not particularly sure about how to go about it. I was looking into the :as and :polymorphic parameters in AR but I'm still not quite sure on how to do it. Help?
What you are looking for is called Single Table Inheritance. Here is one resource on the subject. There are many more. Just google for "Rails Single Table Inheritance".

Rails, Mongoid, Devise User profiles

I'm new to rails (rails 3) and I am learning in practice.
I want to create a User-profile relation. I have created a User model with devise, and that is working well. Then I created a Profile model. The idea is to register an user and the show information about his profile and if the user wants, complete the profile.
But, the profile is related with posts. So, I need to create an empty profile with the register action of the user.
How can I do that?
My idea is to change the UsersController#create method to build the empty profile, but i don't know how.
My models
class User
include Mongoid::Document
devise :database_authenticatable, :registerable, :token_authenticable,
:omniauthable,
:recoverable, :rememberable, :trackable, :validatable
field :username
key :username
embeds_one :profile
attr_accessible :username,:password,:password_confirmation,:remember_me
class Profile
include Mongoid::Document
field :name
field :lastname
...
...
embedded_in :user, :inverse_of => :profiles
referenced_many :posts
Some idea?
It looks like you're only embedding one profile (embeds_one :profile). It should be user.profile, not user.profiles. Also, user.profile will return an instance of that document, not an array of many documents.
You could do something like this in your controller.
user.profile.create(... put any params here ...)
In your view you would then display
<h2>Welcome <%=#user.profile.username %></h2>
You also need to change
embedded_in :user, :inverse_of => :profiles
to
embedded_in :user, :inverse_of => :profile
To see if the user has a profile or not, just see if user.profile returns nil.