I’m new to ruby on rails and I’m a bit stuck with what the best next step is in a multi-tenancy application I’m building.
Basically I want to scope resources by account_id, so I have created a method and helper called current_account in my accounts base_controller.
However, the tutorial I’m following scopes current_account by subdomain which I do not want to do. So I need a way to identify the current user’s account_id so that that I can have a resource variable #contact = current_account.contacts."all".
Do I need to make a new association between the user and account model so that I can use the current_user helper to define the current account id or is there a better way? If so, what is the best way to do this?
Background
The first user who signs up becomes the account owner. Account owners can then invite other users to the account. I'm using the devise gem. Resources are scoped by account so that only users linked to an account can see the records belonging to that account.
Base Controller
module Accounts
class BaseController < ApplicationController
def current_account
#current_account ||= ?????
end
helper_method :current_account
def owner?
current_account.owner == current_user
end
helper_method :owner?
end
end
Contacts (my resource) Controller
module Accounts
class ContactsController < Accounts::BaseController
def index
#contact = current_account.contacts.all
end
end
end
Account Model
class Account < ActiveRecord::Base
belongs_to :owner, class_name: "User"
accepts_nested_attributes_for :owner
validates :subdomain, presence: true, uniqueness: true
has_many :contacts
has_many :invitations
has_many :memberships
has_many :users, through: :memberships
end
Invitation Model
class Invitation < ActiveRecord::Base
belongs_to :account
validates :email, presence: true
end
Membership Model
class Membership < ActiveRecord::Base
belongs_to :account
belongs_to :user
end
User Model
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
Routes
Rails.application.routes.draw do
devise_for :users
scope module: "accounts" do
resources 'dashboard'
resources 'contacts'
resources :invitations, only: [:new, :create] do
member do
get :accept
patch :accepted
end
end
resources :users, only: [:index, :destroy]
end
Schema
ActiveRecord::Schema.define(version: 20170124002015) do
create_table "accounts", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "owner_id"
t.string "subdomain"
end
add_index "accounts", ["subdomain"], name: "index_accounts_on_subdomain"
create_table "contacts", force: true do |t|
t.string "first_name"
t.string "last_name"
t.string "phone"
t.string "email"
t.text "comments"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "account_id"
end
add_index "contacts", ["account_id"], name: "index_contacts_on_account_id"
create_table "invitations", force: true do |t|
t.string "email"
t.integer "account_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "token"
end
add_index "invitations", ["account_id"], name: "index_invitations_on_account_id"
add_index "invitations", ["token"], name: "index_invitations_on_token"
create_table "memberships", force: true do |t|
t.integer "account_id"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "memberships", ["account_id"], name: "index_memberships_on_account_id"
add_index "memberships", ["user_id"], name: "index_memberships_on_user_id"
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
There are two possible associations between users and accounts:
users have many accounts
users belong to an account
In the first case, the tenant cannot be set from the current_user, because it's unclear which account should be used as the current tenant. The membership table in the schema.rb indicates this is the approach taken by the tutorial you mentioned. Loading the account by subdomain helps specify which account shall be used as the current tenant.
In the second case, every user has just one account. Users get an account_id, the membership table becomes obsolete, and you can load the current tenant like so:
def current_account
#current_account ||= current_user.account
end
Do I need to make a new association between the user and account model
so that I can use the current_user helper to define the current
account id or is there a better way? If so, what is the best way to do
this?
It seems to me that you want to take the second approach, which requires that an account has_many users and a user belongs_to an account.
Related
My goal is for users to add individual games pulled from an API gem (https://github.com/games-directory/api-giantbomb) to their personal library. I want users to be able to browse other people's libraries. I have the games showing up via search along with a show page for each game.
I am running into two problems: can't add games to a user's library and can't view other people's library.
Here is my games controller:
class GamesController < ApplicationController
#search for games
def index
#games = GiantBomb::Search.new().query(params[:query]).resources('game').limit(100).fetch
end
#Shows data for individual games
def show
#game = GiantBomb::Game.detail(params[:id])
end
#Adding and removing games to a user's library
def library
type = params[:type]
#game = GiantBomb::Game
if type == "add"
current_user.library_additions << #game
redirect_to user_library_path, notice: "Game was added to your library"
elsif type == "remove"
current_user.library_additions.delete(#game)
redirect_to root_path, notice: "Game was removed from your library"
else
# Type missing, nothing happens
redirect_to game_path(#game), notice: "Looks like nothing happened. Try once more!"
end
end
private
def game_params
params.require(:game).permit(:name, :search, :query)
end
end
When I try to add a game to my library, I get "Game(#70231217467720) expected, got GiantBomb::Game which is an instance of Class(#70231150447440)". So my #game is incorrect but I am not sure what should be there instead.
Even if I could add the game to my library, I can't view other user's libraries. Here is my current controller.
class LibraryController < ApplicationController
#before_action :authenticate_user!
def index
#library_games = User.library_additions
end
end
I get 'undefined method library_additions' even though it is in the model. If I change User to current_user I can see the page, but that means users can only see their page and not others.
Here are my game, user, and library model:
class Game < ApplicationRecord
has_many :libraries
has_many :added_games, through: :libraries, source: :user
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :games
has_many :libraries
has_many :library_additions, through: :libraries, source: :game
end
class Library < ApplicationRecord
belongs_to :game
belongs_to :user
end
I made my library a join table for users and games but I am thinking I didn't do it correctly. Here is my schema:
ActiveRecord::Schema.define(version: 2020_11_19_143536) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "games", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "search"
end
create_table "libraries", force: :cascade do |t|
t.integer "user_id"
t.integer "game_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end
Am I missing a migration or do I need the rework the models and controllers?
[edit] Here are my routes, I am getting a pathing error when I try to add a game.
Rails.application.routes.draw do
devise_for :users
resources :games do
member do
put "add", to: "games#library"
put "remove", to: "games#library"
end
end
resources :library, only:[:index]
root to: 'pages#home'
get '/search', to: 'games#search', as: :search
get '/games', to: 'games#index', as: :index
get '/user/:id', to: 'user#show'
get '/user/:id/library', to: 'library#index', as: :user_library
end
Here, the error clearly states it is expecting an instance of Game not GiantBomb::Game, so you have to create one.
#game = Game.new(name: 'some name', other fields ....)
if type == "add"
current_user.library_additions << #game
About the other error you can only call association methods on an instance not on the class itself
def index
# You could get the user'id through params for example
#library_games = User.find(params[:user_id]).library_additions
end
I save the #booking with a user (called "booker"). Right after the #booking.save I can retrieve #booking.booker in the command line that display all the properties from the user (email, password, id, etc.). However After leaving the create method, impossible to retrieve it (for example from the show) : #booking.booker = nil .
I guess that commes from a mistake in my booking model : I have belongs_to and has_many_through. If the error comes from here, how to solve it without having to change all the db?
booking_controller.rb
class BookingsController < ApplicationController
before_action :set_booking, only: [:show, :edit, :update ]
before_action :set_booking_format, only: [:destroy ]
def index
end
def my_bookings
#bookings = BookingPolicy::Scope.new(current_user, Booking).scope.where(booker: current_user)
end
def show
authorize #booking
end
def new
#garden = Garden.find(params[:garden_id])
#booking = Booking.new
authorize #booking
end
def create
#garden = Garden.find params[:garden_id]
#booking = #garden.bookings.build(booker: current_user)
authorize #booking
if #booking.save
redirect_to garden_booking_path(#booking, current_user)
end
end
def update
end
private
def set_booking
#booking = Booking.find(params[:id])
end
def set_booking_format
#booking = Booking.find(params[:format])
end
def booking_params
params.require(:booking).permit(:garden_id, :booker_id, :date)
end
end
booking.rb
class Booking < ApplicationRecord
belongs_to :garden
belongs_to :booker, class_name: "User"
end
garden.rb
class Garden < ApplicationRecord
belongs_to :user
has_many :bookings, dependent: :destroy
end
user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :gardens
has_and_belongs_to_many :bookings
end
schema.rb
create_table "bookings", force: :cascade do |t|
t.date "date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "garden_id"
t.integer "booker_id"
t.index ["garden_id"], name: "index_bookings_on_garden_id"
end
create_table "gardens", force: :cascade do |t|
t.string "title"
t.text "details"
t.integer "surface"
t.text "address"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "availabilities"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "admin", default: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "bookings", "gardens"
end
In your model, user.rb:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :gardens
has_and_belongs_to_many :bookings
end
The :bookings association should be has_many. You aren't using a join table.
See: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
The belongs_to part of the habtm association is looking for a foreign key, which doesn't exist. You can retrieve #booking.booker before moving to a different controller action because you aren't hitting the database at all, you're just retrieving the instance variables' association.
My rails_admin application is having two roles namely Teacher and Student such that every user belongs_to a role. I am using the gem cancancan to manage roles.
app/models
class Role < ApplicationRecord
has_many :users
end
class User < ApplicationRecord
belongs_to :role
has_many :projects
end
class Project < ApplicationRecord
belongs_to :user
end
Project schema
create_table "projects", force: :cascade do |t|
t.string "name"
t.string "description"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_projects_on_user_id", using: :btree
end
Now, I want the project list to show all the data when role is Teacher but it should only show those projects where project.user_id == user.id when the role is Student.
That is, the final aim is to allow the role Student to see only his/her own projects thereby restricting the role Student to see all the projects where as the role Teacher should be able to see all the projects.
To get data based on current user role I made the following changes to models/ability.rb.
Under the case when user role is Student
can :read, Project, ["user_id = ?", user.id] do |project|
project.present?
end
It will compare the user_id in projects table with the user.id so, in case of role Student it will only fetch the projects created by that student.
Your controller will have an action to list projects.
Just use a before_action filter to load projects based on role.
Assuming, you are setting current_user in application's base controller and have association b/w student and project - student has_many projects.
ProjectController
before_action :load_projects, only: [:index]
def index
#projects
end
private
def load_projects
if current_user.role == "Teacher"
#projects = Project.all
elsif current_user.role == "Student"
#projects = current_user.projects
end
end
end
You can also add association between projects and teacher. And user teacher.projects if you need.
There is also another approach to achieve the functionality
create a new column called role in users table
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "role"
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
end
And in the user model
class User < ApplicationRecord
enum role: [:student, :teacher, ]
after_initialize :set_default_role, :if => :new_record?
def set_default_role
self.role ||= :student
end
end
This creates a default role as student when a new user signs up.you can change the role of the user to teacher by simply doing
go to rails console in the terminal using
rails c
u = User.find_by_id(1)
# you can change value '1' depending upon your user_id
u.role = "teacher"
u.save
Now the role of the user is teacher.
class ProjectsController < ApplicationController
if current_user.teacher?
#projects = Project.all
else
#project = current_user.projects
end
class User < ApplicationRecord
belongs_to :role
has_many :projects
def projects
if self.role.to_s == "student"
super projects
else
Project.all
end
end
end
The answers provided asume the controllers can be changed and as the questioner points he does not have access to them.
Not sure if cancancan will handle the filtering of records based on view(read) permission on the index views of rails admin with a rule like this:
can :read, Project, user_id: user.id
But on the edit view i know you can configure a has many field to scope it based on the current user like this:
edit do
field :projects do
associated_collection_scope do
current_user = bindings[:controller].current_user
proc { |scope| scope.where(user: current_user) }
end
end
end
In my app User can have many Companies and vice versa. In Accounts table id of User and id of its Company is stored.
I want to find all Users who belong to Companies, which belong to current_user. Let's assume that the current_user is like master User (not Admin, as that would be system Admin) of those companies.
How do I do this? My guess is to do it with Arel, but then how should it look in Model, Controller, View? Many thanks for any help. I'm on Rails 5.
models/user.rb
class User < ApplicationRecord
has_many :accounts, dependent: :destroy
has_many :companies, through: :accounts
models/account.rb
class Account < ApplicationRecord
belongs_to :company
belongs_to :user
accepts_nested_attributes_for :company, :user
models/company.rb
class Company < ApplicationRecord
has_many :accounts, dependent: :destroy
has_many :users, through: :accounts
accepts_nested_attributes_for :accounts, :users
My schema.rb looks like this:
create_table "accounts", force: :cascade do |t|
t.integer "company_id"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["company_id"], name: "index_accounts_on_company_id"
t.index ["user_id"], name: "index_accounts_on_user_id"
end
create_table "companies", force: :cascade do |t|
t.string "name"
t.string "legal_name"
t.string "reg_number"
t.string "address"
t.string "bank_acc"
t.string "description"
t.string "website"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "role", default: 0
t.integer "currency", default: 0
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "password_digest"
t.string "remember_digest"
t.boolean "admin", default: false
t.index ["email"], name: "index_users_on_email", unique: true
end
You can find current user's companies, and eager load users who belong to those companies
#companies = current_user.companies.includes(:users)
To list all users(may be in a view), loop through #companies and all its users
#companies.each do |company|
company.users.each do |user|
puts user.name
end
end
Or use map/collect to assign them to a variable.
#users = #companies.map(&:users).flatten
I would like to access the current user's albums by using the primary keys in the Schema below. I have User, Band, And Album models as follows...
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :bands
end
class Band < ActiveRecord::Base
has_many :albums
has_many :users
end
class Album < ActiveRecord::Base
belongs_to :band
end
Schema as follows...
create_table "albums", force: true do |t|
t.string "name"
t.string "releaseDate"
t.string "artWorkUrl"
t.integer "band_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "bands", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
end
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
end
and am trying to use the following method in the Albums controller...
def albums
#albums = current_user.bands.albums
end
Sorry, i'm sure this is a noob question. I know this should be a simple primary key access through the user --> bands --> albums, using user_id and band_id but have been unable to populate the current users albums through bands. Any insight in much appreciated.
def albums
band_ids = current_user.bands.map(&:id)
#albums = Album.where(band_id: band_ids)
end
By separating the queries you do not have the n+1 problem: What is SELECT N+1?
by using the primary keys
You should be looking at the foreign_keys (primary_keys are when you reference the same table). So what you're really asking should be how do I set up the Rails associations correctly for my models?
Here's how:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :bands
end
#app/models/band.rb
Class Band < ActiveRecord::Base
has_many :albums
belongs_to :user
end
#app/models/album.rb
Class Album < ActiveRecord::Base
belongs_to :band
end
This will allow you to call current_user.bands.first.albums
--
Notes
You have to remember the bands association will be a collection. This means you can't just call ....bands.albums. Instead, you'll need to loop through the array like this:
<% for band in current_user.bands do %>
<%= band.albums.inspect %>
<% end %>
In user class define one more association
class User < ActiveRecord::Base
has_many :albums, :through => bands
end
In your controller
#albums = current_user.albums