In RoR, whenever you create a nested resource, is there to set attributes during creation of a resource with a parent association, within the model?
I have this Role model that may belong_to and have_many other roles.
employee = Role.find_by_slug :employee
employee.role
=> nil
employee.roles
=> [...more roles...]
waitress = employee.roles.create(slug: :waitress)
=> #<Role id...
waitress.role
=> #<Role slug: 'employee'...
waitress.roles
=> []
The role model has a boolean attribute of subtype. Whenever I create a role from an existing role, I'd like for subtype to be set to true.
employee.subtype
=> false
And waitress would look like this:
waitress.subtype
=> true
Whenever I create a role from an existing role, I'd like for subtype to be set to true.
#app/models/Role.rb
class Role < ActiveRecord::Base
belongs_to :role
has_many :roles
validate :role_exists, if: "role_id.present?"
before_create :set_subtype, if: "role_id.present?"
private
def set_subtype
self.subtype = true
end
def role_exists
errors.add(:role_id, "Invalid") unless Role.exists? role_id
end
end
The above will require another db request; it's only for create & it will happen when the model is invoked (IE you can call it whatever you like when you need it).
--
An alternative to this would be to use acts_as_tree or a similar hierarchy gem.
AAT adds a parent_id column in your db, to which it will then append a series of instance methods you can call (parent, child, etc).
This would permit you to get rid of the has_many :roles, and replace it with a children instance method:
#app/models/role.rb
class Role < ActiveRecord::Base
acts_as_tree order: "slug"
#no need to have "subtype" column or has_many :roles etc
end
root = Role.create slug: "employee"
child1 = root.children.create slug: "waitress"
subchild1 = child1.children.create slug: "VIP_only"
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1
According to your description, a given Role is considered a subtype if it has no parent role. In this case, simply add the following method to Role:
def subtype?
!self.role.nil?
end
The following changes did the trick for me:
from:
has_many :roles
to:
has_many :roles do
def create(*args, &block)
args[0][:subtype] = true
super(*args, &block)
end
end
Related
User and Organization have a many-to-many association through Relationship. The Relationship model includes several boolean variables about the relationship, such as moderator (true/false) and member (true/false). Also, I added a boolean called default that sets the default organization.
I require a validation that if (and only if) a user is a member of one or more organizations (member == true), one (and exactly 1) of these organizations has to have default == true.
So basically this means that if a user is member of multiple organizations, one of these organizations needs to be the default ánd if the user is a member of multiple organizations such a default organization has to exist.
How to write this validation? My current validation generates the following error upon seeding:
PG::SyntaxError: ERROR: syntax error at or near "default"
LINE 1: ...ERE (user_id = 1) AND (member = 't' and default = ...
^
: SELECT COUNT(*) FROM "relationships" WHERE (user_id = 1) AND (member = 't' and default = 't')
My implementation in the Relationship model:
validate :default
private
def default
#relationships = Relationship.where('user_id = ?', self.user_id)
#members = #relationships.where('member = ?', true)
#defaults = #members.where('default = ?', true)
# If more than 1 organization has been set as default for user
if #defaults.count > 1
#defaults.drop(0).each do |invalid|
invalid.update_columns(default: false)
end
end
# If user is member but has no default organization yet
if !#defaults.any? && #members.any?
#members.first.update_columns(default: true)
end
end
Update On the looks of it, I understand I shouldn't model it this way, and instead should use a has_one belongs_to relationship as #DavidAldridge suggests in his answer. But I don't understand how to model this relationship (see my comment below the answer). Any advice is very much appreciated.
The reason for this being difficult is that your data model is incorrect. The identity of a user's default organisation is an attribute of the user, not of the relationship, because there can be only one default per user. If you had a primary, secondary, tertiary organisation, then that would be an attribute of the relationship.
Instead of placing a "relationship is default for user" attribute on the Relationship, place a "default_relationship_id" attribute on the User so it ...
belongs_to :default_relationship
... and ...
has_one :default_organisation, :through => :default_relationship
This guarantees that:
Only one organisation can be the default for the user
There has to be a relationship between the user and its default organisation
You can also use :dependent => :nullify on the inverse association of :default_relationship, and easily test whether an individual relationship is the default based on whether:
self == user.default_relationship.
So something like:
class User << ActiveRecord::Base
has_many :relationships, :inverse_of => :user, :dependent => :destroy
has_many :organisations, :through => :relationships, :dependent => :destroy
belongs_to :default_relationship, :class_name => "Relationship", :foreign_key => :default_relationship_id, :inverse_of => :default_for_user
has_one :default_organisation, :through => :default_relationship, :source => :organisation
class Relationship << ActiveRecord::Base
belongs_to :user , :inverse_of => :relationships
belongs_to :organisation, :inverse_of => :relationships
has_one :default_for_user, :class_name => "User", :foreign_key => :default_relationship_id, :inverse_of => :default_relationship, :dependent => :nullify
class Organisation << ActiveRecord::Base
has_many :relationships, :inverse_of => :organisation, :dependent => :destroy
has_many :users , :through => :relationships
has_many :default_for_users, :through => :relationships, :source => :default_for_user
Hence you can do such simple matters as:
#user = User.find(34)
#user.default_organisation
Default organisation is also easily eager-loaded (not that it couldn't be otherwise, but no scope is required to do so).
#Brad Werth's correct that your validate method would work better as a callback.
I'd recommend something like this in your Relationship model:
before_save :set_default
private
def set_default
self.default = true unless self.user.relationships.where(member: true, default: true).any?
end
This should enforce that a user's relationship is set to default if none of the user's other relationships already are.
Change default to is_default (as pointed out by another user in comments, default is postgres keyword). Create separate migration for this. (Or you could quote it everywhere if you prefer to leave it be as it is.)
Then, there are two points.
First, why you need to check for single is_default organization every time? You just need to migrate your current data set, and then keep it consistent.
To migrate your current data set, create migration and write something like this there:
def self.up
invalid_defaults = Relationship.
where(member: true, is_default: true).
group(:user_id).
having("COUNT(*) > 1")
invalid_defaults.each do |relationship|
this_user_relationships = relationship.user.relationships.where(member: true, is_default: true)
this_user_relationships.where.not(id: this_user_relationships.first.id).update_all(is_default: false)
end
end
Just make sure to run this migration in off-peak hours, as it could take considerable amount of time to finish. Alternatevely, you can just run that code snippet from the server console itself (just test in in development environment beforehand, of course).
Then, use callback (as rightfully suggested by another commenter) to set the default organization when the record is updated
before_save :set_default
private
def set_default
relationships = Relationship.where(user_id: self.user_id)
members = relationships.where(member: true)
defaults = members.where(is_default: true)
# No need to migrate records in-place
# Change #any? to #exists?, to check existance via SQL, without actually fetching all the records
if !defaults.exists? && members.exists?
# Choosing the earliest record
members.first.update_columns(is_default: true)
end
end
To take the case into account where Organization is being edited, callback to organization should be added as well:
class Organization
before_save :unset_default
after_commit :set_default
private
# Just quque is_default for update...
def remember_and_unset_default
if self.is_default_changed? && self.is_default
#default_was_set = true
self.is_default = false
end
end
# And now update it in a multi-thread safe way: let the database handle multiple queries being sent at once,
# and let only one of them to actually complete, keeping base in always consistent state
def set_default
if #default_was_set
self.class.
# update this record...
where(id: self.id).
# but only if there is still ZERO default organizations for this user
# (thread-safety will be handled by database)
where(
"id IN (SELECT id FROM organizations WHERE member = ?, is_default = ?, user_id = ? GROUP BY user_id HAVING COUNT(*)=0)",
true, true, self.user_id
)
end
end
This has something to do with my last quesion about unsaved objects, but now it is more about a specific problem how to use rails.
The models I have are:
class User < ActiveRecord::Base
has_many :project_participations
has_many :projects, through: :project_participations, inverse_of: :users
end
class ProjectParticipation < ActiveRecord::Base
belongs_to :user
belongs_to :project
enum role: { member: 0, manager: 1 }
end
class Project < ActiveRecord::Base
has_many :project_participations
has_many :users, through: :project_participations, inverse_of: :projects
accepts_nested_attributes_for :project_participations
end
With this models, when I create a new project I can do it by a form (fields_for etc) and then I can call update_attributes in the controller. So if I have users in the database already, I can do this:
u = Users.create # save one user in database (so we have at least one saved user)
p = Project.new
# add the user to the project as a manager
# the attributes could come from a form with `.fields_for :project_participations`
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> true
This works fine until I want to do something with the users of a project. For example I want add a validations that there must be at least one user for a project:
class Project < ActiveRecord::Base
...
validates :users, presence: true # there must be at least one user in a project
...
end
This now gives:
u = Users.create
p = Project.new
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> false
p.errors
=> #<ActiveModel::Errors:... #base=#<Project id: nil>, #messages={:users=>["can't be blank"]}>
p.users
=> #<ActiveRecord::Associations::CollectionProxy []>
p.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: 1, project_id: nil>]>
So on unsaved projects the .users is empty. This already bugs me (see my last quesion about unsaved objects). But in this case I can of course now work around this by doing validates :project_participations, presence: true instead of validates :users, presence: true and it should mean the same.
But this would mean I should never use the .users method (in any helper, model, view, ...) unless I am totally sure that I work with a saved object. Which in fact renders the .users method unusable (like it does with the validation of user`s presence).
If I call update_attributes like this, the validations works and it saves:
p.update_attributes(users: [u])
With this it creates the project_participation by itself so p.users works as expected. But here I cannot set any data like role for project_participation of that user.
So my questions are: Can I make the .users method work whether or not the object is saved (I think not)? But then, how can I add users to a unsaved project as a manager/member and work with the unsaved project?
I hope my problem is clear.
I think I understand you question, and you're correct in assuming that you cannot use the .users method whether or not the project model is saved. The reason for this is that in defining an association in Project (ie. has_many :users, through: :project_participations, inverse_of: :projects) you're telling rails to read the users attribute out of the database via the project_participations join table and when you haven't saved the project you have nothing to read out of the database.
In order to add a User to your project in a particular role you will need to create a new ProjectParticipation model which you will then associate to your project. If you then remove the users association and write your own users method you should be able to access your collection of users regardless of whether or not the project has been saved.
class Project < ActiveRecord::Base
has_many :project_participations
...
def users
project_participations.collect { |pp| pp.user }
end
end
Then something like:
u = Users.create
p = Project.new
pp = ProjectParticipation.new({user: u, project: p, role: 1})
p.project_participations << pp
p.users
Hopefully that helps.
I've inherited quite a weird table layout:
callbacks
id, note, user
admin
id, name, password
In callbacks, the user is set to the name of the admin rather than the actual ID. Now I need to be able to call callbacks.user and have rails lookup the admin with that name and then bind it to that record.
I have a model for admin that is called users
How would I go about that?
You can override the default methods.
def user
User.find_by_name(user_name)
end
def user=(obj)
self.user_name = obj.name
end
def user_name
self[:user]
end
def user_name=(name)
self[:user] = name
end
Other option , to make it work with belongs_to, there is primary_key option but need to have a different name than the attribute user
# Callback.rb
belongs_to :user_model , :class => "User", :foreign_key => :user, :primary_key => :name
# User.rb
has_one :callback , :foreign_key => :user, :primary_key => :name
I have the following script which demonstrates me that has_many roles= attribute always works in a persistent manner.
My questions are:
1) What is the reason behind this behavior: why has_many attributes are persisted right at the moment when they've been set? Why this difference from regular attributes behavior (name in the following script) ?
2) Can I write my custom roles= setter so I could use fx assign_attributes for a bunch of models attributes (including roles=) without roles association to be persisted? I would appreciate an example if it is possible in Rails > 3.2 ?
Here is the script:
gem 'rails', '>=3.2.0' # change as required
gem 'sqlite3'
require 'active_record'
require 'logger'
puts "Active Record #{ActiveRecord::VERSION::STRING}"
ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => ':memory:'
)
ActiveRecord::Schema.define do
create_table :users, :force => true do |t|
t.string :name
end
create_table :user_roles, :force => true do |t|
t.integer :user_id
t.integer :role_id
end
create_table :roles, :force => true do |t|
t.string :name
end
end
# Create the minimal set of models to reproduce the bug
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
end
r = Role.create(:name => 'admin')
u = User.create
# roles= persists its value, name= does not
u.assign_attributes({ :roles => [r], :name => 'Stanislaw' })
# The same behavior is produced by:
# u.attributes=
# u.roles=
puts "name attribute: #{u.name}"
puts "many roles #{u.roles}"
u.reload
puts "name attribute: #{u.name}"
puts "many roles #{u.roles}" # I see admin role and I want to achieve behavior that I would not see it
Associations are not the same as attributes. For example with a has_many association all you are doing when you assign is setting the foreign key on the belongs_to side.
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
p = Post.create
u = User.create
u.posts << p # this line will simply update p.user_id with u.id
In your example with the join table assigning a role to a user will create a UserRole record and with the user_id/role_id records set. This happens because you declared the has_many :through
As for preventing this behavior, you could use a virtual attribute that stores the unpersisted roles until you save the record, then create the associations.
class User < ActiveRecord::Base
attr_accessor :unpersisted_roles
attr_accessible :unpersisted_roles
after_save :assign_roles
def assign_roles
self.roles << #unpersisted_roles if defined(#unpersisted_roles)
end
end
r = Role.create
u = User.create
u.attributes = {:unpersisted_roles => [r]}
u.save # roles get persisted here
This is only a simple example, actual code might need to be more complicated or require diving deeper into AR's interface to get it working without too many side effects.
If you could give some insight as to why your wanting to not persist the association I might be able to suggest a more specific course of action.
Update
In reference to Issue #3 with some comments where changes were made.
module SimpleRoles
module Many
module Persistence
class << self
def included base
base.class_eval %{
has_many :user_roles
has_many :roles, :through => :user_roles
# Add a callback to persist the roles
after_create :persist_roles
}
end
end
def roles
# Apply unpersisted roles in case we want to access them before saving
super.map(&:name).map(&:to_sym) + (#unpersisted_roles || [])
end
def roles= *rolez
rolez.to_symbols!.flatten!
# if we're already persisted then go ahead and save
# otherwise stash them in an ivar array
if persisted?
super retrieve_roles(rolez)
else
#unpersisted_roles = rolez
end
end
private
# Our callback method that sets the roles, this will
# work since persisted? is true when this runs.
def persist_roles
self.roles = #unpersisted_roles
end
def retrieve_roles rolez
raise "Not a valid role!" if (rolez - config.valid_roles).size > 0
rolez.map do |rolle|
begin
Role.find_by_name! rolle.to_s
rescue
raise "Couldn't find Role for #{rolle}. Maybe you need to re-run migrations?"
end
end
end
def config
SimpleRoles::Configuration
end
end
end
end
We struggled as well with this by-design behaviour of persisting on validation. We have a confirm step, before the actual save but after validation. If a user wants to cancel the action or clicks away the confirm modal, we still had the saved changes.
Our solution: use a param for the validation part of update, put the validation within a transaction and raise a rollback if we are not saving.
def update
#user = User.find_by(key: params[:id])
User.transaction do
#user.assign_attributes(user_params)
if params[:only_validate] == 'true'
#user.valid?
raise ActiveRecord::Rollback
else
#user.save
end
end
render json: #user
end
Hope it will be helpful for others with same approach.
I have the following join table that works:
class CreateRolesUsers < ActiveRecord::Migration
def self.up
create_table :roles_users,:id => false do |t|
t.integer :role_id, :null => false
t.integer :user_id, :null => false
end
end
def self.down
drop_table :roles_users
end
end
But I don't know how to load by default some data (syntax), I try that:
roleUser = RoleUser.create(:role_id => 2,:user_id => 1)
roleUser.save!
It does not work, is it RoleUser... or something else to use? RolesUser...etc.
That's provided that you have a model named RolesUser.
If you have a habtm association, the model is probably not there.
One way of loading roles could be;
user = User.create :name => 'John'
role = Role.create :name => 'admin'
user.roles << role
From what I understand, your user can have many roles, right? If so..
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
Then you can do
user = User.create :name => 'John'
role = Role.create :name => 'admin'
roles_users = RolesUser.create :user => user, :role => role
The has_and_belongs_to_many assoiation creates a join table with both FK. If you need extra data in the join table, you will need to use has_may :through instead of has_and_belongs_to_many.
I strongly recommend reading the guide on ActiveRecord associations. Good luck!
You're almost there. The table name is a plural form of the model name. The table is named RolesUsers, so the model is named RolesUser.
Also, I think you would prefer to use the new method if you're going to call save! after the fact. Calling create automatically saves the record.
rolesUser = RolesUser.new(role_id => 2,:user_id => 1)
rolesUser.save!
Recommended reading: http://api.rubyonrails.org/classes/ActiveRecord/Base.html
First when you have association has_and_belongs_to_many you actualy don't have model, you only have database table with plural name combined from models that are participating in association, for example
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
You don't get model named UserRoles, or UsersRoles, or UsersRole or any kind of model, this only makes some methods that you can use on User instance or Role instance to find for 1 user all of his roles, or to find all users with some role ant etc.
This works, that rails will look for database table with name roles_users (it looks for table that is combined with both model names in plural, ordering by alphabetical order, thats why its roles_users and not users_roles).
For particular user you can add roles or predefine existing ones, example:
# find one user
user = User.first
# get collection of roles
roles_c = Role.where("some conditional statement to find roles")
# set user roles to found collection
user.roles = roles_c
# save changes
user.save
This way you will get records in roles_users table with user_id of user and for every role in roles_c collection there will be record, for example:
# if user.id is 1
# and in roles_c you have 3 roles with id_s 2,5 and 34
# in roles_users table there will be created 3 records with
user_id => 1, role_id => 2
user_id => 1, role_id => 5
user_id => 1, role_id => 34
Other way is to add some roles,
user = User.first
roles_c = Role.where('conditional statement')
user.roles << roles_c
user.save