Rails custom validation to ensure a Store always has an Owner - ruby-on-rails

Logically, I'm trying to demote the a Member role from Owner to User. Expected behaviour is to produce an error "Store must have at least one owner".
It's use-case is to support the promotion of other members roles to Owner permissions, but never leave the store without at least one owner. I thought to add a validation to the Member for that, but it appears that isn't sufficient as it let's an Owner get demoted to a User (leaving the store without an Owner). Moreover, a new Owner cannot be set after since store_has_owner will return false.
class Admin::MembersController < Admin::BaseController
def make_user
member = current_company.members.find(params[:id])
if member.update(role: "user")
redirect_to admin_dashboard_path, notice: "#{member.user.email} is now a user."
else
redirect_to admin_dashboard_path, alert: member.errors
end
end
end
Member Exists? (0.5ms) SELECT 1 AS one FROM "members" WHERE "members"."store_id" = $1 AND "members"."role" = $2 LIMIT $3 [["store_id", 1], ["role", 0], ["LIMIT", 1]]
15:15:12 web.1 | ↳ app/models/member.rb:21:in `store_has_owner'
15:15:12 web.1 | Member Update (0.4ms) UPDATE "members" SET "role" = $1, "updated_at" = $2 WHERE "members"."id" = $3 [["role", 1], ["updated_at", "2019-08-26 19:15:12.965431"], ["id", 1]]
15:15:12 web.1 | ↳ app/controllers/admin/members_controller.rb:39:in `make_user'
15:15:12 web.1 | (0.6ms) COMMIT
15:15:12 web.1 | ↳ app/controllers/admin/members_controller.rb:39:in `make_user'
15:15:12 web.1 | Redirected to http://lvh.me:5000/admin/dashboard
15:15:12 web.1 | Completed 302 Found in 24ms (ActiveRecord: 3.8ms | Allocations: 9037)
Store
class Store < ApplicationRecord
has_many :members
has_many :users, through: :members
end
Member
class Member < ApplicationRecord
belongs_to :store
belongs_to :user
enum role: [ :owner, :user ]
validate :store_has_owner
def owner?
self.role == "owner"
end
def store_has_owner
errors.add(:member, "Store must have at least one owner.") unless store.members.owner.exists?
end
end
User
class User < ApplicationRecord
# Automatically remove the associated `members` join records
has_many :members, dependent: :destroy
has_many :stores, through: :members
end

unless clause should be
store.members.any? { |m| (m == self ? self : m).owner? }
There we're finding any member (m) in the members collection which owner? method returns true, i.e. Store has at least one Owner.
There is one caveat though. Accessing store.members causes selecting this relation from database and, because changed role of self is uncommitted yet, it returns that self is an Owner and this check don't work as expected. To avoid this we substitute m with self when they are equal, so m.owner? became self.owner? and returns false as expected.

Here are a few solutions I can think of:
1) This validation does not belongs to the member, but to the store. The store cannot be valid without an owner. Move it to the store and then add validates_associated :store to user.rb.
#store.rb
validate :has_owner
def has_owner
errors.add(:base, "Store must have at least one owner.") unless members.owner.any?
end
This should take care of the problem and is cleaner. Another solution follows. A bit dirtier but...
2) You can deal with this in the workflow of reducing a Member from Owner to User, by making sure there is another owner other than the member you are demoting. So:
# admin/members_controller.rb
def make_user
member = current_company.members.find(params[:id])
if member.update(role: "user") && current_company.has_other_owner_than(member)?
redirect_to admin_dashboard_path, notice: "#{member.user.email} is now a user."
else
redirect_to admin_dashboard_path, alert: member.errors
end
end
# store.rb
def has_other_owner_than?(member)
members.where(role: :owner).not(id: member.id).any? # Rails 5
# or
# members.where("role = ? AND id != ?", Member.roles[:owner], member.id).any? # Rails 4.2
end

Related

Strange has_many Association Behavior in Rails 4

I've got two tables, User and Allergy. These are connected via another table, UserAllergy. The models are as would be expected:
class User
has_many :user_allergies
has_many :allergies, through: :user_allergies
end
class UserAllergy
belongs_to :user
belongs_to :allergy
end
class Allergy
has_many :user_allergies
has_many :users, through :user_allergies
end
What I'm confused about is creating allergies from a multiple-valued collection_select in my User form.
I have the following field:
<%= f.collection_select :allergy_ids,
Allergy.all,
:id,
:name,
{},
{ class: 'form-control', multiple: true }
%>
This correctly inserts a key into my params like so if I selected the Allergies with ids 1 and 2:
{ user: { id: "1", allergy_ids: ["", "1", "2"] } }
When I create the user instantiated with #user = User.new( my_params ), the weird behavior occurs. Instead of inserting the provided allergy_ids into the join table, Rails does a query to get all current user_allergies for the user, then deletes all of the current user_allergies:
Started PATCH "/employees/regular_user" for 127.0.0.1 at 2015-06-18 22:08:30 -0400
Processing by UsersController#update as HTML
Parameters: {"utf8"=>"✓", "user"=>{ "allergy_ids"=>["", "1", "2", "3"]}, "button"=>"", "id"=>"regular_user"}
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
(0.1ms) begin transaction
Allergy Load (0.1ms) SELECT "allergies".* FROM "allergies" INNER JOIN "user_allergies" ON "allergies"."id" = "user_allergies"."allergy_id" WHERE "user_allergies"."user_id" = ? [["user_id", 1]]
SQL (0.1ms) DELETE FROM "user_allergies" WHERE "user_allergies"."user_id" = ? AND "user_allergies"."allergy_id" = 1 [["user_id", 1]]
(27.4ms) commit transaction
Redirected to http://localhost:3000/employees/regular_user
Completed 302 Found in 32ms (ActiveRecord: 27.8ms)
Anyone knows what gives, or what I need to do to create allergies implicitly? I've tried accepts_nested_attributes_for and changing around the form to use fields_for.
So, I went and looked at code of mine that does a similar function. Here's what my create method looks like. This is creating a Student with assignment to Student Groups in a school setting (I didn't use "class" since Ruby wouldn't like that).
def create
#student = Student.new(student_params)
if #student.save
#student.student_groups = StudentGroup.where(id: params[:student][:student_group_ids])
flash[:success] = "Student was successfully created."
redirect_to #student
else
render 'new', notice: "Your student could not be created."
end
end
I completely ignore the Student Group IDs when creating the student_params, since I'm not using them for mass assignment.
Yes, one extra line of code. I'd be really interested to hear if there's a way to accomplish this via mass assignment.
You're missing one part of the puzzle which is the relation from Allergy to User.
class Allergy
has_many :user_allergies
has_many :users, through: :user_allergies
end
Just give the following code a try-
params.require(:user).permit(___, ____, {allergy_ids: []}, ____, ____)

Canard gem - how should I programatically make an admin user?

I'm using the Canard gem (CanCan + RoleModel) for authentication on Ruby on Rails, but I'm having some issues with making an initial user admin. An Account has Users. When an Account is created, there's an Owner User who has billing details. I'd like that Owner to have the :admin role.
class SomewhereElseInSystem
...
account = Account.register(hash) # Company Admin submits form with
# details of account, owning user,
# and a load more objects we don't
# show here.
end
class Account < ActiveRecord::Base
has_many :users, dependent: :destroy, include: [:profile]
has_one :owner, class_name: 'User', conditions: { owner: true }
accepts_nested_attributes_for :owner
def self.register(details)
...
hash = { name: details[:client_name],
domain: details[:subdomain],
plan: details[:plan],
owner_attributes: {
name: details[:owner_name],
password: details[:owner_password],
password_confirmation: details[:owner_password],
email: details[:owner_email] } }
account = Account.create hash
...
end
end
class User < ActiveRecord::Base
belongs_to :account
acts_as_user roles: [:saas_admin, :admin, :manager]
# owner is a boolean DB field
before_validation :owners_are_admins
protected
def owners_are_admins
return unless owner
self.roles << :admin
end
end
However, I'm finding that when I make an account the associated owner is not an admin, and has no roles set. I've tried several variations on the above and am convinced that I'm missing something basic. Help?
Update:
If I go into the console, I can have great fun seeing 4 non-admins instead of 1 admin and 3 non-admins via the scopes, but a different answer via admin? and the actual fields.
[46] subscriptions » account.users.count
(0.5ms) SELECT COUNT(*) FROM "users" WHERE "users"."account_id" = 1
=> 4
[47] subscriptions » account.users.admins.count
(0.4ms) SELECT COUNT(*) FROM "users" WHERE "users"."account_id" = 1 AND ("users"."roles_mask" & 2 > 0)
[48] subscriptions » account.users.non_admins.count
[48] subscriptions » account.users.non_admins.count
(0.6ms) SELECT COUNT(*) FROM "users" WHERE "users"."account_id" = 1 AND ("users"."roles_mask" & 2 = 0 or "users"."roles_mask" is null)
=> 4
[49] subscriptions » account.users.map(&:admin?)
=> [
[0] false,
[1] false,
[2] false,
[3] true
]
[51] subscriptions » account.users.select{ |user| user.roles_mask & 2 > 0 }.count 0;34# Same as query above
=> 1
But better yet, if I do
account.users.admins # 0
owner = account.owner
owner.roles << :admin # Should be a no-op
owner.save!
account.user.admins # 1!
Clearly my callback isn't doing the equivalent of the << in the console after all. Which I don't understand.

Destroy has_many through association in Rails 4 Engine cannot find the column

I'm working on a Rails 4 engine and I have a simple has_many through association that is being created correctly but seems to break when I try to remove the association.
I'm using Rails 4, Formtastic and Cocoon. Here are my models and associations.
# Role
module Kohcms
class Role < ActiveRecord::Base
has_many :permission_roles, dependent: :destroy
has_many :permissions, through: :permission_roles
accepts_nested_attributes_for :permissions, reject_if: proc { |attributes| attributes[:subject_class].blank? }, allow_destroy: true
end
end
# Permission
module Kohcms
class Permission < ActiveRecord::Base
has_many :permission_roles, dependent: :destroy
has_many :roles, through: :permission_roles
end
end
# Permission Role Join Model/Table
module Kohcms
class PermissionRole < ActiveRecord::Base
belongs_to :role
belongs_to :permission
end
end
When I add the new association, it works fine. But when I delete it, I get this error:
ActiveRecord::StatementInvalid in Kohcms::Admin::RolesController#update
Mysql2::Error: Unknown column 'kohcms_permission_roles.' in 'where clause': DELETE FROM `kohcms_permission_roles` WHERE `kohcms_permission_roles`.`` = NULL
Here is an output:
Started PATCH "/admin/roles/4" for 127.0.0.1 at 2013-07-20 14:46:11 -0500
Processing by Kohcms::Admin::RolesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"Dnj2RDxlK7XJTf6NZLgmuIQCDOVfjhWjsN1mCPpHIn4=", "commit"=>"Update Role", "role"=>{"title"=>"asdfadsfadf", "permissions_attributes"=>{"0"=>{"_destroy"=>"1", "name"=>"asdf", "subject_class"=>"ActiveRecord::SchemaMigration", "subject_id"=>"", "action"=>"All", "id"=>"16"}}, "full_access"=>"0", "canlock"=>"0", "user_ids"=>[""]}, "id"=>"4"}
Kohcms::User Load (0.3ms) SELECT `kohcms_users`.* FROM `kohcms_users` WHERE `kohcms_users`.`id` = 1 ORDER BY `kohcms_users`.`id` ASC LIMIT 1
Kohcms::Role Load (0.3ms) SELECT `kohcms_roles`.* FROM `kohcms_roles` WHERE `kohcms_roles`.`id` = 1 ORDER BY `kohcms_roles`.`id` ASC LIMIT 1
Kohcms::Role Load (0.2ms) SELECT `kohcms_roles`.* FROM `kohcms_roles` WHERE `kohcms_roles`.`id` = 4 LIMIT 1
(0.1ms) BEGIN
Kohcms::User Load (0.3ms) SELECT `kohcms_users`.* FROM `kohcms_users` WHERE `kohcms_users`.`role_id` = 4
Kohcms::Permission Load (0.3ms) SELECT `kohcms_permissions`.* FROM `kohcms_permissions` INNER JOIN `kohcms_permission_roles` ON `kohcms_permissions`.`id` = `kohcms_permission_roles`.`permission_id` WHERE `kohcms_permission_roles`.`role_id` = 4 AND `kohcms_permissions`.`id` IN (16)
Kohcms::Role Exists (0.3ms) SELECT 1 AS one FROM `kohcms_roles` WHERE (`kohcms_roles`.`title` = BINARY 'asdfadsfadf' AND `kohcms_roles`.`id` != 4) LIMIT 1
Kohcms::PermissionRole Load (0.2ms) SELECT `kohcms_permission_roles`.* FROM `kohcms_permission_roles` WHERE `kohcms_permission_roles`.`role_id` = 4 AND `kohcms_permission_roles`.`permission_id` = 16
SQL (0.3ms) DELETE FROM `kohcms_permission_roles` WHERE `kohcms_permission_roles`.`` = NULL
Mysql2::Error: Unknown column 'kohcms_permission_roles.' in 'where clause': DELETE FROM `kohcms_permission_roles` WHERE `kohcms_permission_roles`.`` = NULL
(0.1ms) ROLLBACK
Completed 500 Internal Server Error in 11ms
Thanks in advance!
EDIT
Here is the update method. It's a shared method so my Roles and Permissions controllers inherit from another controller with this:
def update
#parent = parent_model
#parents = parent_models
#model = fetch_model
#model = pre_update(#model)
if #model.errors.empty? and #model.update_attributes(permitted_params)
message = "#{#model.class.name.demodulize.titlecase} was successfully updated."
# allows for some basic controler specific functionality without redefining the create method
succeeding_update(#model)
respond_to do |format|
format.html {
if params[:redirect_to].present?
redirect_to params[:redirect_to], notice: message
else
redirect_to edit_model_link(#model), notice: message
end
}
format.json {
render_json_model_response #model, message, 'updated'
}
end
else
flash[:error] = 'There was an error, please try again.'
respond_to do |format|
format.html {
if params[:redirect_to].present?
redirect_to params[:redirect_to], notice: message
else
redirect_to edit_model_link #model
end
}
format.json { render_json_response :error, :notice => 'There was an error, please try again' }
end
end
end
You need to add an ID column to your PermissionRole table. I updated my association from HABTM (where you don't need an ID), and found that it was causing exactly the same problem.
This migration should do (obviously generate migration & change to your specs):
add_column :image_products, :id, :primary_key
This should resolve your issue if the relevant dependent: :destory elements are in place

Optimize eager loading in Rails

Rails 3.2. I have the following:
# city.rb
class City < ActiveRecord::Base
has_many :zones, :dependent => :destroy
end
# zone.rb
class Zone < ActiveRecord::Base
belongs_to :city
has_many :zone_shops, :dependent => :destroy
has_many :shops, :through => :zone_shops
end
# zone_shop.rb
class ZoneShop < ActiveRecord::Base
belongs_to :zone
belongs_to :shop
end
# shop.rb
class Shop < ActiveRecord::Base
end
# cities_controller.rb
def show
#trip = City.find(params[:id], :include => [:user, :zones => [:shops]])
#zones = #trip.zones.order("position")
# List out all shops for the trip
shops_list_array = []
#zones.each do |zone, i|
zone.shops.each do |shop|
shops_list_array << shop.name
end
end
#shops_list = shops_list_array.join(', ')
end
# development.log
City Load (0.3ms) SELECT `cities`.* FROM `cities` WHERE `cities`.`id` = 1 LIMIT 1
Zone Load (0.3ms) SELECT `zones`.* FROM `zones` WHERE `zones`.`trip_id` IN (1) ORDER BY position asc
ZoneShop Load (0.3ms) SELECT `zone_shops`.* FROM `zone_shops` WHERE `zone_shops`.`zone_id` IN (26, 23, 22) ORDER BY position asc
Shop Load (0.5ms) SELECT `shops`.* FROM `shops` WHERE `shops`.`id` IN (8, 7, 1, 9)
Zone Load (0.5ms) SELECT `zones`.* FROM `zones` WHERE `zones`.`trip_id` = 1 ORDER BY position asc, position
Shop Load (0.5ms) SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 26
Shop Load (0.6ms) SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 23
Shop Load (0.4ms) SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 22
Notice in my log, the last 3 lines with shops with ID 26, 23, 22 are redundant. How should I rewrite my cities_controller.rb to reduce the query to the system?
Many thanks.
#zones = #trip.zones.includes(:shops).order("position")
This eager-loads the shops association and should elimitate the n+1 query problem caused by zone.shops.each
For more information, have a look at the Ruby on Rails Guide section 12 on Eager Loading associations, which was also linked by #Benjamin M
I'd suggest that this
zone.shops.each do |shop|
shops_list_array << shop.name
end
produces the 3 last lines of your log. This means: You currently have only one zone inside your database. If you put more zones in there, you will get a lot more Zone Load entries in log.
The problem obviously is Rails' each method, which triggers the lazy loading:
#zones.each do |zone, i|
...
The solution depends on your needs, but I'd suggest that you read everything about Rails' eager loading feature. (There's exactly your problem: The each thing). Look it up here: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
It's pretty easy, short and straightforward :)

syntax for ROR associations

Apologies in advance for another newbie question, but the ROR syntax is just not clicking with me, I can't get my head around the shortcuts and conventions (despite reading a couple of books already!) -
I effectively copied this from a book, but I"m trying to work out what is build, create etc?
#cart = current_cart
product = Catalog::Product.find(params[:product_id])
Rails.logger.debug { "Value in cart id " + #cart.id.to_s }
#checkout_line_item = #cart.line_items.build(product: product)
respond_to do |format|
if #checkout_line_item.save...
The output from log is this:
Processing by Checkout::LineItemsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"9NH+xgDPTf/iN7RCdPd8H9rAIqWsSVB/f/rIT++Kk7M=", "product_id"=>"7"}
Created a line item
(0.1ms) BEGIN
SQL (2.0ms) INSERT INTO `checkout_carts` (`created_at`, `discounts`, `grand_total`, `loyalty_points`, `order_date`, `subtotal`, `timestamps`, `total_tax`, `updated_at`) VALUES ('2012-08-21 11:06:15', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2012-08-21 11:06:15')
(0.2ms) COMMIT
Catalog::Product Load (0.2ms) SELECT `products`.* FROM `products` WHERE `products`.`id` = 7 LIMIT 1
Value in cart id 8
Completed 500 Internal Server Error in 5ms
NoMethodError (undefined method `save' for nil:NilClass):
app/controllers/checkout/line_items_controller.rb:55:in `block in create'
app/controllers/checkout/line_items_controller.rb:54:in `create'
I'm guessing the problem lies with the build syntax where it builds the checkout line item, or possibly I've set up the has_many associations wrong. Is this enough for someone to help me troubleshoot? Or should I post the model declarations?
Update with models:
class Checkout::LineItem < ActiveRecord::Base
attr_accessible :customer_update_date, :inventory_status, :line_item_color, :line_item_description, :line_item_size, :line_item_tagline, :line_item_total, :quantity, :sku_id, :style_id, :tax, :tax_code, :timestamps, :unit_price, :product
belongs_to :cart
belongs_to :product, :class_name => 'Catalog::Product'
end
class Checkout::Cart < ActiveRecord::Base
attr_accessible :discounts, :grand_total, :loyalty_points, :order_date, :subtotal, :timestamps, :total_tax
has_many :line_items, dependent: :destroy
end
module Catalog
class Product < ActiveRecord::Base
attr_accessible :assoc_product,:product_id, :merch_associations, :aux_description, :buyable, :long_description, :name, :on_special, :part_number, :release_date, :short_description, :withdraw_date, :occasion
<<clipped for brevity>>
has_many :line_items, :class_name => 'Checkout::LineItem'
...
end
Can't answer my own question, but I think I got the answer:
It looks like I needed to add the cart to the build call...
this appears to have worked (I think, there's another blocking problem, but I can sort that one):
#cart = current_cart
product = Catalog::Product.find(params[:product_id])
Rails.logger.debug { "Value in cart id " + #cart.id.to_s }
#checkout_line_item = #cart.line_items.build(product: product, cart: #cart)
Build essentially creates and reserves space for a blank object. This allows you to create an associated object without saving it. The only use case I've experienced this with is a nested form where I had multiple dates for an event, so I used 5.times.build to create 5 empty date associations.

Resources