Limit/strip and extend embedded fields in document - ruby-on-rails

I have an app where the User can have multiple roles inside a Room. I want to have a relation/embedding between them where I want to have, besides the normal fields embedded from the User, one more called role. For example:
irb(main):001:0> #user
=> #<User _id: 52c9d44d72616e19bf000000, name: "ranisalt">
And...
irb(main):001:0> #room
=> #<Room _id: 52ca3a7872616e2204000000, name: "Test Room", users: [#<User _id: 52c9d44d72616e19bf000000, name: "ranisalt", role: "admin">, (...)]
(mind the 'role: "admin"')
How is it possible to add this one more field to the user inside the room?
Also, I'm using Facebook authentication, so my user has lots of data that aren't useful for the room, for example provider, oauth_token, oauth_expires_at, etc. How can I strip these fields off when embedding the user? They use most of the space and will quickly fill the space without being useful.
I use Rails 4. If needed, ask for my models, I can show them.

Is there a reason you're denormalizing rooms so aggressively? Instead of rooms holding onto users that magically acquire roles, why not have rooms embed roles that reference users?
class Room
embeds_many :roles
end
class Role
belongs_to :user
field :type
embedded_in :room
end
class User
has_many :roles
end

Nested Attributes
provide a mechanism for updating documents and their relations in a single operation, by nesting attributes in a single parameters hash. This is extremely useful when wanting to edit multiple documents within a single web form.
Please refer http://mongoid.org/en/mongoid/docs/nested_attributes.html#common
Which help you to build relationship across tables.
Thank you

Related

How to make an update on a Nested form in ruby?

In my application, I have a entity called User, that has one Talent. A talent is a kind a user can be on my system (a model, a photographer, a videomaker, a client)
#user.rb
class User < ApplicationRecord
has_one :talent, dependent: :destroy
A Talent belongs to a user, and have many TalentAbilities.
#talent.rb
class Talent < ApplicationRecord
belongs_to :user
has_many :talent_talent_abilities, dependent: :destroy
has_many :talent_abilities, through: :talent_talent_abilities
So, we can create, for example, a TalentAbility that belongs to a Talent. Like this:
TalentType.create!([
{name: "Model", is_model: true, is_photo: false, is_makeup: false, is_hair: false},
{name: "Photographer", is_model: false, is_photo: true, is_makeup: false, is_hair: false
])
TalentAbility.create!([
{name: "Scuba diving", requires_description: false, talent_type_id: 1}
{name: "River rafting", requires_description: false, talent_type_id: 1},
I want to add a View, where the user, based on your TalentType, edit his profile, with a lot of checkboxes where he can click for example, "Scuba Diving -> true" . "River rafting -> false".
My question is: How is the better way to update those relationships this on my controller? First remove all the TalentTalentAbility that belongs to this talent, and then add all based on my form?
#profiles_controler.rb
def update
##delete all the entities
TalentTalentAbility.where(talent: u.talent).each do |tt|
tt.destroy
end
##some code to add the new relationships
end
Thanks
The problem with the accepted answer:
Say you have a user with 5 talents and he chooses that he's learned a sixth one. If you go the way of calling .destroy_all, this is what will happen:
5 separate, sequential DELETE statements sent to DB
6 separate, sequential INSERT statements sent to DB
The correct solution for such problem is to render the checkboxes with their respective ids and make use of _destroy (read more here) attribute.
In short, if you get a hash (you may need to clean it up after you get it from your form, I'm sorry, but I don't have anything on hand to give you a A-Z example) containing something like this:
..., talent_talent_abilities_attributes: [
{id: 1, _destroy: true},
{id: nil, talent_ability_id: 1}
{id: 2, talent_ability_id: 2, some_attribute: "value"}
]
And you save your user, ActiveRecord will:
Issue one DELETE statement for first item.
Issue one INSERT statement for second item.
Depending on whether your records are loaded and whether something changed for ability with id: 2, it might or might not issue an UPDATE statement.
I suggest you tinker a bit in rails console with creating a User and updating his abilities manually using the nested attribute array (which again, please read more about at the aforementioned link) and observing how many SQL queries it generates. If you have any further questions, leave a comment under this or create a separate, more specific question.
I've done something similar in the past and it's worked really well. I would move it out to the model though, so you'd have something like this.
class User < ActiveRecord::Base
ActiveModel::Dirty # if you need to track changes and only delete on attrs changed
before_save :update_talents
# ...
private
def update_talents
talents.destroy_all if talents_changed?
end
end
Then the new talents will be saved with the form data. Also, that way your controller won't be needlessly messy.
You should be careful, though, to ensure that that's the behaviour you want. You could add in a check to only update if the talents are being changed with ActiveModel::Dirty, too. Might wanna check the syntax on the *_changed method for associations.
Caveat
I tend to advocate this approach because my nested form used JS positioning to re-order items therein. There might be a saner approach.

How should I structure two types of Roles in a Rails application?

I am working on a Ruby on Rails application that has two kinds of "Roles".
One will be where a user has multiple roles, such as "Admin", "Team Lead", etc. these will be defined in the seeds file and not generated by the user.
The other will be generated by a User and assigned to other users, such as "Chef", "Waiter", etc.
They are both similar in that they only have a name column. The former will be used with an authorization tool such as CanCanCan. I currently plan to allow users and roles to have many of the other using a has_many :through relationship.
class Role
has_many :user_roles
has_many :users, through: :user_roles
end
class User
has_many :user_roles
has_many :roles, through: :user_roles
end
class UserRole
belongs_to :user
belongs_to :role
end
My questions are:
Should the latter ("Chef", "Waiter", etc) be put in the same table? Separate tables?
Should I use some kind of inheritance?
What's a good practice for this situation?
I plan to use the term "Role" in the user interface for the latter, showing what "Roles" a user has. The former I guess is more about what privileges they have within the application.
If you go away from the technical side of roles and authentication, and try to describe the "things" from a more business oriented approach you make the distinction clearer for yourself.
What I understand is: You have a definition for a user of your application that is used to describe what authorization this user has, e.g. an "admin" has more rights than an "editor" or "community manager".
You also want these users of your application to be able to create names that are associated with (other?) users. Theses names have nothing to do with authorization, as I understood.
Maybe these names are more like tags, that people can assign?
I would keep both separated, as it shouldn't be able for a user to create a role, or modify existing roles, that could grant them access to admin features.
If you want to look at a tagging gem, I could recommend this one: https://github.com/mbleigh/acts-as-taggable-on I used this for several years and while it has its drawbacks, it's reliable.
I'd suggest having a look at rolify before rolling your own solution, as it will have solved some things that you'll have to reimplement / discover later (e.g. role queries, avoiding N+1 queries etc). It also integrates well with can?, and should work well for the authorisation part of your question.
Whilst it's not impossible to allow users to create new roles (by throwing an input form on top of Role.create), this starts to get messy, as you need to track which ones are for authorisation and which ones informative (and user created).
Since the two groups of things are for different purposes, I wholeheartedly agree with this other answer that it's cleaner to separate the user-generated entities, and look to implement them as tags. You may display all the "roles" together in certain views, but that doesn't mean that it makes sense to store them within a single table.
Side-note: if you do end up rolling your own solution, consider using HABTM here. The join table will still be created, but you won't have to manage the join table model. E.g.
has_and_belongs_to_many :users, join_table: :users_roles
Since you only have a limited number of roles, you could use a bitmask and store directly on the user model as a simple integer.
See this Railscasts for more information. That would be the most efficient, database and association wise, way to do this although perhaps not the simplest to understand. The only real restriction is that you can't alter the array of values you check against, only append to it.
Good luck!
I would create one more model, Permission, where you can create a list of all the permissions you want to manage under any given role.
Then have a many to many between Permissions and Roles.
From a UserRole instance then you will be able to list the permissions, and in the future you could add additional permissions to roles buy just running inserts in a couple of tables
Roles: Onwer, Chef, Cook, Waiter
Permission: can_cook, can_buy_wine, can_manage, can_charge_custome
Owner: can_manage, can_buy_wine, can_charge_customer
Chef: can_cook, can_manage
Waiter: can_charge_customer
Is would be a good start and you can evolve the role functionality to whatever your needs are without an external dependency.
Also, You can go just using Users table and adding role column as integer and give them a role code in default 0 integer.
#app/helpers/roles_helper.rb
module RolesHelper
#roles = {
'Default' => 0,
'Waiter' => 10,
'Chef' => 20,
'Superadmin' => 30
}
class << self
def list_roles
#roles.map{|k,v| [k,v] }
end
def code(str)
return #roles[str]
end
def value(id)
return #roles.key(id)
end
end
def require_default_users
unless current_user && current_user.role >= RolesHelper.code('Waiter')
redirect_to root_url(host: request.domain)
end
end
def require_superadmin_users
unless current_user && current_user.role >= RolesHelper.code('Superadmin')
redirect_to courses_path
end
end
end
access in controllers
sample:
class Admin::AdminController < ApplicationController
include RolesHelper
def sample_action_method
require_default_users #if non admin user redirect ...
puts "All Roles: #{RolesHelper.list_roles}"
puts "Value: #{RolesHelper.value(30)}"
puts "Code: #{RolesHelper.code('Superuser')}"
end
end

How to use `first_or_initialize` step with `accepts_nested_attributes_for` - Mongoid

I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.
Basically, I'd like to do something like:
# override authors_attributes
def authors_attributes(attrs)
attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
super(attrs)
end
But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).
I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).
Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...
The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.
With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.
This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.
If I have a model Paper:
class Paper
include Mongoid::Document
embeds_many :authors
accepts_nested_attributes_for :authors
field :title, type: String
end
And a model Author embedded in Paper:
class Author
include Mongoid::Document
embedded_in :paper, inverse_of: :authors
field :name, type: String
end
I can do this in the console:
> paper = Paper.create(title: "My Paper")
> paper.authors_attributes = [ {name: "Raviolicode"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]
> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]
As you can see, I can update and add authors in the same authors_attributes hash.
For more information see Mongoid nested_attributes article
I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:
accepts_nested_attributes_for :authors, reject_if: :check_author
def check_author(attrs)
if existing = Author.where(label: attrs['label']).first
self.authors << existing
true
else
false
end
end
This still seems like a hack, but it works in Mongoid as well...

RoR: Different user roles for each new created record?

I want to make a record management system. The system will have 4 different user roles: Admin, Viewer, Editor and Reviewer.
While the first two are easy to implement using gems such as cancan and declarative authorization, the other two are not so simple.
Basically each new record is created by an Admin (only an Admin can create new records), and should have its own separate Editor and Reviewer roles. That is, a user can be assigned many different roles on different records but not others, so a user might be assigned Editor roles for Record A and C but not B etc.
Editor: can make changes to the record, and will have access to specific methods in the controller such as edit etc.
Reviewer: will be able to review (view the changes) made to the record and either approve it or submit comments and reject.
Viewer: Can only view the most recent approved version of each record.
Are there any ways of handling such record-specific user roles?
This can be accomplished without too much effort with the cancan gem and a block condition. A block condition checks for authorization against an instance. Assuming your Record class had an editors method that returns an array of authorized editors the cancan ability for updating a Record might look something like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
...
can :update, Record do |record|
record.editors.include?(user)
end
...
end
end
See "Block Conditions" on the CanCan wiki:
https://github.com/ryanb/cancan/wiki/Defining-Abilities
Update
Storing which users have which access to which records could be done many ways depending on your specific needs. One way might be to create a model like this to store role assignments:
class UserRecordRoles < ActiveRecord::Base
# Has three fields: role, user_id, record_id
attr_accessible :role, :user_id, :record_id
belongs_to :user_id
belongs_to :record_id
end
Now create a has_many association in the User and Record models so that all role assignments can be easily queried. An editors method might look like this:
class Record < ActiveRecord::Base
...
has_many :user_record_roles
def editors
# This is rather messy and requires lot's of DB calls...
user_record_roles.where(:role => 'editor').collect {|a| a.user}
# This would be a single DB call but I'm not sure this would work. Maybe someone else can chime in? Would look cleaner with a scope probably.
User.joins(:user_record_roles).where('user_record_roles.role = ?' => 'editor')
end
...
end
Of course there are many many ways to do this and it varies wildly depending on your needs. The idea is that CanCan can talk to your model when determining authorization which means any logic you can dream up can be represented. Hope this helps!

Signaling validation errors in assigning a virtual attribute?

This is a Rails/ActiveRecord question.
I have a model which basically has to represent events or performances. Each event has many attributions: an attribution is basically something like "In this event, Person X had Role Y".
I concluded that the best way to allow a user to edit this data is by providing a free text field which expects a structured format, which I'll call a role string:
singer: Elvis Costello, songwriter: Paul McCartney, ...
where I use autocompletion to complete on both the names of roles (singer, songwriter...) and the names of people. Both roles and people are stored in the database.
To implement this, I created a virtual attribute in the Event model:
def role_string
# assemble a role string from the associations in the model
end
def role_string=(s)
# parse a string in the above role string format,
# look up the People and Events mentioned, and update
# what's in the database
end
This is all fine. The whole thing works quite well, when the role string is well-formed and the associations given by the role string all check out.
But what if the role string is malformed? Okay, I figure, I can just use a regex together with standard validation to check the format:
validates_format_of :role_string, :with => /(\w+:\s*\w+)(,\s*\w+:\s*\w+)*/
But what if the associations implied by the role string are invalid? For example, what happens if I give the above role string, and Elvis Costello doesn't reference a valid person?
I thought, well, I could use validates_each on the attribute :role_string to look up the associations and throw an error if one of the names given doesn't match anything, for example.
My questions are two: first, I don't like this approach, since to validate the associations I would have to parse the string and look them up, which duplicates what I'd be doing in role_string= itself, except for actually saving the associations to the database.
Second, ... how would I indicate that an error's occurred in assigning to this virtual attribute?
First of all, you're attributing the Person to the Event incorrectly. You should instead pass a Person's ID to the event, rather than a string of the person's name. For instance, what if a Person with an ID of 230404 and a name of "Elvis Costello" changes his name to "Britney Spears?" Well, if that were to happen, the ID would remain the same, but the name would change. However, you would not be able to reference that person any longer.
You should set up your associations so that the foreign key references people in multiple cases:
has_one :singer, :class_name => "Person", :foreign_key => "singer_id"
has_one :songwriter, :class_name => "Person", :foreign_key => "songwriter_id"
This way, you can have multiple people associated with an Event, under different roles, and you can reference multiple attributes that Person may have. For example:
Event.first.singer.name # => "Elvis Costello"
Event.first.songwriter.name # => "Britney Spears"
You can research the available validations for associations (validates_associated), as well as whether or not an ID is present in a form (validates_presence_of). I would recommend creating your own custom validation to ensure that a Person is valid before_save. Something like (untested):
def before_save
unless Person.exists?(self.songwriter_id)
self.errors.add_to_base("Invalid songwriter. Please try again!")
return false
end
end
Also, I noticed that you're looking for a way for users to select a user which should be used for the roles in your Event. Here is what you can do in your form partial:
select(:event, :singer_id, Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => 'None' })
Hope this helps!

Resources