Mongoid Relations (Custom accessors) - ruby-on-rails

I am using Mongoid in my Rails app. I have a User model and I also have a Role model (think Admin, guest etc).
I want to setup my Mongoid associations such that the following code will be possible:
u = User.first
u.invited_roles # returns all roles a user has been assigned
r = Role.new
r.invitee = user # user id should be stored in the Role
r.save!
Currently, my models are set up as so:
class User
include Mongoid::Document
has_many :invited_roles, :class_name => 'Role'
end
class Role
include Mongoid::Document
has_one :invitee, :class_name => 'User'
end
Currently, when I do Role.new from the Rails console, I get an object that does not appear to have a field to store the invitee User. How do I fix this? I have seen people using inverse_of but I can't really find any tutorials or documentation on this procedure.
Thanks for the help.

Well, thinking about this --- a User will be invited to have many roles, but that role will only have one user. Now, this is not "have one" in the sense of has_one. Instead, it's defining a child relationship.
So, change the has_one to belongs_to and you're golden.
class User
include Mongoid::Document
has_many :invited_roles, :class_name => 'Role'
end
class Role
include Mongoid::Document
belongs_to :invitee, :class_name => 'User'
end

Related

Rolify scope roles to many objects and different classes Rails 6

I am trying to "extend" Rolify functionality to have some global roles such as 'Admin', 'Member', 'Guest', etc... and to be able to set up different "scopes" for each user who have a specific role.
For example, in my app i have this admin role, which is a "super role" meaning it grants access to basically everything. But i also want to be able to "scope" this role for another User, the scope will be, for example 'he will have access to all users, but only if they are from countries A, B, C and from cities X, Y, Z'. I know rolify supports different roles with different scopes, but what i want is to manage "global roles" with different scopes only for different users.
I thought about doing something like a 'Scope' model that belongs to a Role and to a User, in which i would have HABTM relationships with countries and cities, and then use that for authorization (I'm using CanCanCan). But i ran into many issues when working on this approach. It was something like:
class Scope
belongs_to :user
belongs_to :role
has_and_belongs_to_many :countries
has_and_belongs_to_many :cities
end
One of the issues i ran into was that i need to grant the role at the same time i create a scope, and if a user is 'revoked' of a role, the scope which belongs to the user and the role, needs to be destroyed. This last part i found particularly hard since 'Scope' is not related to 'users_roles' table.
Anyone has any idea on a better approach to this problem? I'm having a hard time figuring out the right way to have a role that has custom scopes for each user (basically i need something in the middle of the user and the role to define what is the user's scope with that role).
Appreciate any help I can get!
If you want to create something of your own has_and_belongs_to_many is not the answer (hint: it's almost never the right answer). Using HABTM is the akilles heel of Rolify as its assocations look like this:
class User
has_and_belongs_to_many :roles
end
class Role
has_and_belongs_to_many :users
belongs_to :resource,
polymorphic: true,
optional: true
end
This doesn't let you you query the users_roles table directly or add additional columns or logic. Fixing it has been an open issue since 2013. There are workarounds but Rolify may not be the right tool for the job here anyways.
If you want to roll your own you want to use has_many through: to setup an actual join model so you can query the join table directly and add assocations, additional columns and logic to it.
class User
has_many :user_roles
has_many :roles, through: :user_roles
end
class UserRole
belongs_to :user
belongs_to :role
belongs_to :resource,
polymorphic: true,
optional: true
validates_uniqueness_of :user_id,
scope: [:role_id, :resource_id, :resource_type]
end
class Role
validates :name, presence: true,
uniqueness: true
has_many :user_roles
has_many :roles, through: :user_roles
end
This moves the resource scoping from being per role to being per user.
While you could add additional join tables between the user_roles table and the "scoped" resources its not strictly necissary unless you want to avoid polymorphic asssocations.

Rolify make association to a user with a specific role

I want to make an association where a user has many deals and a deal belongs to a user as well as another with a role ('associate').
I am using the rolify gem to do this.
Like this:
# user.rb
has_many :deals
# deal.rb
belongs_to :user
belongs_to :associate # User with role 'associate or admin'
The first belongs to could be whatever user, doesn't matter if the user is any role it should still work, the second belongs to however should most definitely be an associate or an admin
Do you think I should use rolify for this? or should I not and just make a different model for each role?
Update
My current solution won't work because the user as an associate would need two associations, the has many deals and the has_many :client_deals. I'm not sure in the naming still.
Update 2
Max's solution works great!
This is not where you want to use Rolify's tables. Rolify creates a one-to-one assocation between roles and resources through the roles tables. Roles then have a many-to-many assocation through the users_roles table to users.
Which means it works great for cases where the association is one-to-many or many-to-many but Rolify really can't guarantee that there will ever only be one user with a particular role due to the lack of database constraints.
Even if you add validations or other application level constraints that still leaves the potential for race conditions that could be a double click away.
Instead you want to just create separate one-to-one assocations:
class Deal < ApplicationRecord
belongs_to :creator,
class_name: 'User',
inverse_of: :created_deals
belongs_to :associate,
class_name: 'User',
inverse_of: :deals_as_associate
validates :must_have_associate_role!
private
def must_have_associate_role!
# this could just as well be two separate roles...
errors.add(:associate, '^ user must be an associate!') unless associate.has_role?(:associate)
end
end
class User < ApplicationRecord
has_many :created_deals,
class_name: 'Deal'
foreign_key: :creator_id,
inverse_of: :creator
has_many :deals_as_associate,
class_name: 'Deal'
foreign_key: :associate_id,
inverse_of: :associate
end
Two models can really have an unlimited number of associations between them as long as the name of each assocation is unique and you configure it correctly with the class_name and foreign_key options.
Since this uses a single foreign key this means that ere can ever only be one and you're safeguarded against race conditions.
For associate you can use the following
belongs_to :associate, -> { includes(:roles).where(roles: {name: ['associate', 'admin'] }) }, class_name: 'User', foreign_key: 'user_id'

Rails Mountable Engine with CanCan

I'm working on a blog engine and I wan't to setup cancan. I found some ways to do it:
forem doest the thing by adding an admin field in the host user model, but having multiple roles would require many fields, and that is definitely not the way to do it
I saw some exames like this but to do it I would need to have my host app user model having has_many relations to my engine models, like this
class User < ActiveRecord::Base
attr_accessible :name
has_many Blogcms::RoleUser
has_many Blogcms::Role, :through => Blogcms::RoleUser
end
I don't really know if this is the right way to do it, but this wont work because beeing a isolated engine the engine models will be invisible
has someone tried this?
sorry for my english
EDIT
I found a way around, not having to search through the User model and just setting up the relations in my Role model
module Blogcms
class Role < ActiveRecord::Base
has_many :role_user
has_many :user, :class_name => Blogcms.user_class, :through => :role_user
attr_accessible :name
# Return all the roles for a user
def self.roles_for_user(user)
joins(:user).where('users.id' => user.id)
end
end
end
like that my roles_for_user methods returns all the roles for a user
I found a way around, not having to search through the User model and just setting up the relations in my Role model
module Blogcms
class Role < ActiveRecord::Base
has_many :role_user
has_many :user, :class_name => Blogcms.user_class, :through => :role_user
attr_accessible :name
# Return all the roles for a user
def self.roles_for_user(user)
joins(:user).where('users.id' => user.id)
end
end
end
like that my roles_for_user methods returns all the roles for a user

Rails -- whats the proper relationship model

I've got a rails application built for schools. The users in the system are parents who track their childrens’ activities (aka family members or just members). Teachers are also users and can log in to participate in activities. The Member model is the place that the application uses to find participants. So that teachers can be found they are added to the Member model and for the same reason, parents are added as well.
A teacher added to this application would have one record in each table user, user_member, and member. A record in user to allow him to login, 1 record in member so he can be searchable, and 1 in user_member to make the association. In this case finding a user’s own member record is trivial. But if I were a parent with a login and 2 children in the system, James and Brian, one would find 3 records in the Members table, one for James, one for Brian and one for myself. I need to know which member record is mine, not James or Brian.
What’s the best way to model this? I’ve considered two options 1) there’s a foreign key in the user table that points to a user’s own member_id OR 2) the user_members table has a boolean called ‘own_member’ which indicates this user_id’s member_id is the member record for the user. Is there one that would be preferable? One more rails like? And how would I make the call build/create the associations?
The current relationships are modeled like this:
class User < ActiveRecord::Base
has_many :user_members
has_many :members, :through => :user_members
end
class UserMember < ActiveRecord::Base
belongs_to :user
belongs_to :member
end
class Member < ActiveRecord::Base
has_many :user_members
has_many :user, :through => :user_members
end
It sounds like the UserMember join model describes which members a user has privileges to access. This is a different concept than tracking which Member record is associated to a particular user. It sounds like a user would have a member record, and a member would belong to a user implying the following database structure:
class User < ActiveRecord::Base
has_many :user_members
has_many :members, :through => :user_members
has_one :member
end
class UserMember < ActiveRecord::Base
belongs_to :user
belongs_to :member
end
class Member < ActiveRecord::Base
has_many :user_members
has_many :user, :through => :user_members
belongs_to :user
end
This means adding another database column to the members table for "user_id". Creating and building the member record from a User instance could be done as follows:
user = User.new
user.build_member
user.create_member
More info on building through associations here: http://ar.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
You might consider creating callbacks to automatically create a member record on creation of a user record so you can skip manually building the associated record. An example would be:
# app/models/user.rb
class User < ActiveRecord::Base
before_create :build_member
def build_member
self.build_member
end
end
More information on callbacks here: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

How can a record belong_to a single user, but also have multiple "secondary" users?

In my app, I have a User model and a Project model. A user has_many assignments and each project belongs_to a user. But along with each project having an owner, the user who created it, I would like the owner be able to share it with others (so that the project gets shown on the other users' account along with their own). I imagine having to use has_many :through, and setting up a projects_users table with a user_id and a project_id. And I guess this would be the end result?
Project.first.user
# The creator of the project
=> #<User id: 1, name: 'andrew', etc...>
Project.first.users
# The users that the creator chose to share it with
=> [#<User id: 2 ...>, #<User id: 3 ...>]
I've been working on this a bit, and I created a SharedProject model with a user_id and project_id column:
class SharedProject < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
I would like to call both user.projects and user.shared_projects, but I don't know how I would get shared_projects to return project records instead of records from the shared_projects table. I can't do has_many :projects, :through => :shared_projects since then I wouldn't be able to return projects that the user has created.
class User < ActiveRecord::Base
has_many :projects # Calling user.projects returns the projects that user created
has_many :shared_projects # How to get user.shared_projects to return project records?
end
Here's how you can add an owner field to your Project model and then and owned projects collection to your User model. You can use what you already have for the shared projects part.
class User < ActiveRecord::Base
has_many :owned_projects, :class_name => 'Project', :foreign_key => 'owner_id'
end
class Project < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
end
You'll need to add an owner_id column to your projects table.
Just of the top of my head, I think you might want to set up a ProjectShare model.
The ProjectShare model will belong_to user (the sharer), belong_to project (the project being shared) and has_many user_shared_with (just a different classname for user model)
This way you could see who exactly has shared what with who.
I think you'd could accomplish the same thing with the has_many :through situation by just naming your models accordingly.

Resources