ActiveAdmin: has_many associative records are commiting changes before parent object on patch failure - ruby-on-rails

I'm having an issue with Rails ActiveAdmin (quite a novice myself still) where a has_many association (CampaignCountry) is deleting/adding records incorrectly when its select input field :countries is modified and when the parent object (Campaign) fails due to any validation errors:
↳ /Users/JAlva/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/activerecord-5.2.5/lib/active_record/log_subscriber.rb:98
CampaignCountry Destroy (0.3ms) DELETE FROM "campaign_countries" WHERE "campaign_countries"."campaign_id" = $1 AND "campaign_countries"."country_id" = $2 [["campaign_id", 53], ["country_id", 1]]
↳ /Users/JAlva/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/activerecord-5.2.5/lib/active_record/log_subscriber.rb:98
(0.6ms) COMMIT
↳ /Users/JAlva/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/activerecord-5.2.5/lib/active_record/log_subscriber.rb:98
(0.1ms) BEGIN
↳ /Users/JAlva/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/activerecord-5.2.5/lib/active_record/log_subscriber.rb:98
Campaign Exists (0.3ms) SELECT 1 AS one FROM "campaigns" WHERE "campaigns"."vanity_url" = $1 AND "campaigns"."id" != $2 LIMIT $3 [["vanity_url", "dummycampaign"], ["id", 53], ["LIMIT", 1]]
↳ /Users/JAlva/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/activerecord-5.2.5/lib/active_record/log_subscriber.rb:98
(0.1ms) ROLLBACK
as you can see, CampaignCountry delete is still occuring despite the parent objects eventual ROLLBACK in this example. Ideally, I want the CampaignCountry records to only update after the parent Campaign validations are run and it is found to be valid. Below are the association rules between them:
# Campaign model:
has_many :campaign_countries, dependent: :destroy
has_many :countries, through: :campaign_countries
# CampaignCountry model:
belongs_to :campaign, touch: true
belongs_to :country
# Campaign ActiveAdmin countries field:
permit_params ... country_ids: []
...
f.input :countries, as: :select, multiple: true, collection: Country.pluck(:alpha_code, :id)
Any help is appreciated in understanding why this is occuring as well as a potential fix. Let me know if supplemental information is needed.

Related

Create action in controller is omitting reference field

I have a problem with my rails application when i create a customer for an environment, the action itself if omitting the parameter environment_id in the insert statment.
This is my customer.rb model:
class Customer < ActiveRecord::Base
belongs_to :environment, inverse_of: :customers
has_many :all_services, class_name: 'Service'
has_many :services, inverse_of: :customer
has_paper_trail ignore: %i[created_at updated_at]
end
This is my environment.rb model:
class Environment < ActiveRecord::Base
has_many :all_customers, class_name: 'Customer', dependent: :destroy
has_many :customers, inverse_of: :environment
has_many :all_services, class_name: 'Service', dependent: :destroy
has_many :services, inverse_of: :environment
has_many :all_versions, class_name: 'Version', dependent: :destroy
has_many :versions, inverse_of: :environment
has_many :all_role_permissions, class_name: 'RolePermission', dependent: :destroy
has_many :role_permissions, inverse_of: :environment
has_paper_trail ignore: %i[created_at updated_at]
end
This is the customer_controller.rb create action:
def create
if customer_params.permitted?
render json: Customer.create!(customer_params), status: :ok
else
render json: { message: Api::V1::INVALID_PARAMETERS }, status: :bad_request
end
end
def customer_params
params.require(:customer).permit(:environment_id, :full_name, :document_type, :document_value, :customer_type)
end
And this is the server log:
Started POST "/api/v1/customers" for 127.0.0.1 at 2021-06-02 18:18:44 +0100
Processing by Api::V1::CustomersController#create as HTML
Parameters: {"full_name"=>"cvbcv", "document_type"=>"bcvbc", "document_value"=>"vbcvbcv", "customer_type"=>"bcvbcvb", "environment_id"=>1, "customer"=>{"full_name"=>"cvbcv", "document_type"=>"bcvbc", "document_value"=>"vbcvbcv", "customer_type"=>"bcvbcvb", "environment_id"=>1}}
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."uid" = $1 LIMIT $2 [["uid", "test1#test.cl"], ["LIMIT", 1]]
(0.5ms) BEGIN
↳ app/services/api/customers.rb:20:in `create_customer'
Environment Load (0.5ms) SELECT "environments".* FROM "environments" WHERE "environments"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/services/api/customers.rb:20:in `create_customer'
Customer Create (1.4ms) INSERT INTO "customers" ("full_name", "document_type", "document_value", "customer_type", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["full_name", "cvbcv"], ["document_type", "bcvbc"], ["document_value", "vbcvbcv"], ["customer_type", "bcvbcvb"], ["created_at", "2021-06-02 17:18:44.378509"], ["updated_at", "2021-06-02 17:18:44.378509"]]
↳ app/services/api/customers.rb:20:in `create_customer'
(1.2ms) ROLLBACK
↳ app/services/api/customers.rb:20:in `create_customer'
Completed 500 Internal Server Error in 22ms (ActiveRecord: 4.3ms | Allocations: 6384)
ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "environment_id" violates not-null constraint
DETAIL: Failing row contains (28, cvbcv, bcvbc, vbcvbcv, bcvbcvb, null, 2021-06-02 17:18:44.378509, 2021-06-02 17:18:44.378509).
):
I tried everything i can possible know, i triple checked the migrations and schema.
I also checked active record gem version from previous projects and everything is ok.
One thing that might be relevant, this that the project have a front-end in Angular.
Rails validates for Environment presence in the database because Customer belongs_to to it.
Query below in your log is trying to load Environment with ID=1:
Environment Load (0.5ms) SELECT "environments".* FROM "environments" WHERE "environments"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
After this Rails try to INSERT new Customer without Environment reference because it seems that Environment with ID=1 doesn't exist in the database.
multiple ways of solving it.
problem: the environment_id is nil and already as not nil defined in the postgres DB.
you can add a required: true to the association, which rails then checks for existence. in your case a validation error would throw.
you can add a validates :enviroment_id, presence: true which would do the same
you merge the id into the params
example to always merge the environment_id into the params
def customer_params
params.require(:customer).permit(:full_name, :document_type, :document_value, :customer_type).merge(environment_id: GET_ME_THE_ENV_ID)
end

Rails destroys existing has_one child before raising validation error

I have a model relationship like this:
class Parent < ApplicationRecord
has_one :child, dependent: destroy
end
class Child < ApplicationRecord
belongs_to :parent
end
The foreign key is in the childs table:
t.belongs_to :parent, index: { unique: true }, foreign_key: true
In the childs_controller, I have a create method from the route POST /parents/<id>/childs:
def create
#parent = Parent.find(params[:id])
#child = #parent.create_child! params[:child]
render json: #child, status: 200
end
When I first POST data to this controller, the child model is created accordingly. When I POST child data to the controller again, it throws a validation error, since the parent ID is already taken. This is expected. However, the existing child is destroyed at the same time. This means that when I POST data for a third time, a new child is created.
The corresponding log where the child is deleted looks like this:
Child Exists? (0.3ms) SELECT 1 AS one FROM "childs" WHERE "childs"."parent_id" = $1 LIMIT $2 [["parent_id", 1], ["LIMIT", 1]]
(0.2ms) ROLLBACK TO SAVEPOINT active_record_3
Child Load (0.3ms) SELECT "childs".* FROM "childs" WHERE "childs"."parent_id" = $1 LIMIT $2 [["parent_id", 1], ["LIMIT", 1]]
(0.2ms) SAVEPOINT active_record_3
Child Destroy (0.2ms) DELETE FROM "childs" WHERE "childs"."id" = $1 [["id", 1]]
(0.2ms) RELEASE SAVEPOINT active_record_3
ERROR: Validation failed: Parent has already been taken
I can work around this by wrapping the creation in a transaction:
Parent.transaction do
#child = #parent.create_child! params[:child]
end
But I don't understand why this suddently works.
So:
Why is the existing child model destroyed when trying to create a new one (in a has one relationship) is invalid?
Why does wrapping it in a transaction prevent that behavior?

can't write unknown attribute "parent_id", has_one and belongs_to

i'm using rails 4 and was wondering if anyone could find what's wrong in my code.
I have project model and I created a team model that has a belongs_to - has_one relation with project.
project model:
class CrmProject < ActiveRecord::Base
has_one :crm_team
team model
class CrmTeam < ActiveRecord::Base
belongs_to :crm_project
accepts_nested_attributes_for :crm_project
belongs_to :crm_section
belongs_to :manager, class_name: "User"
has_many :users
accepts_nested_attributes_for :users
end
When submitting the form to create a new team i get this error:
ActiveModel::MissingAttributeError in CrmTeamsController#create
can't write unknown attribute `crm_team_id`
and log from server :
Parameters: {"crm_team"=>{"crm_project"=>"5", "manager"=>"3", "user_ids"=>["", "2"]}, "co
mmit"=>"Create Crm team"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT 1
[["id", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE ("users"."email" = 'email#gmail.com' AND "users"."
id" != 2) LIMIT 1
SQL (0.2ms) INSERT INTO "crm_teams" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2020-03-1
1 07:29:19.735115"], ["updated_at", "2020-03-11 07:29:19.735115"]]
(0.2ms) rollback transaction
Completed 500 Internal Server Error in 9ms (ActiveRecord: 1.0ms)
When you use a has_one relation like this
has_one :crm_team
Rails expects you to add crm_team_id to your crm_project, in order to understand which crm_team is related with crm_project object. Adding it going to solve your problem.
In my opinion defining opposite like this much better in logic. Because in future these teams can have multiple projects.
class CrmProject < ActiveRecord::Base
belongs_to :crm_team
class CrmTeam < ActiveRecord::Base
***has_one/has_many(pick one)*** :crm_project
accepts_nested_attributes_for :crm_project
belongs_to :crm_section
belongs_to :manager, class_name: "User"
has_many :users
accepts_nested_attributes_for :users
end

Rails Cancancan define ability on 3 association

Using Rails 6 and CanCanCan. Here are my models:
class Shop < ApplicationRecord
has_many :shop_reviews, dependent: :destroy
end
class ShopReview < ApplicationRecord
belongs_to :shop, counter_cache: true
belongs_to :user_profile, counter_cache: true
end
class User < ApplicationRecord
has_one :user_profile, dependent: :destroy
has_many :shop_reviews, through: :user_profile
end
class UserProfile < ApplicationRecord
belongs_to :user
has_many :shop_reviews
end
ShopReview table:
id, shop_id, user_profile_id, review
Association:
1. A shop can have many reviews
2. Only one User Profile can have one review on a shop
Abilities I want to define:
1. User Profile can edit, update, destroy his own review of a shop
2. User Profile cannot create a new review if a review of his exist on that shop
What I tried:
class Ability
include CanCan::Ability
def initialize(user)
can :read, :all
if user.present?
can [:update, :destroy], ShopReview, user_profile_id: user.user_profile.id
can :create, ShopReview do
!ShopReview.exists?(user_profile_id: user.user_profile.id, shop_id: :shop_id)
end
end
end
end
But I can't seem to pass in the shop_id on visiting /shops/11/shop_reviews/new. Here's the log:
Started GET "/shops/11/shop_reviews/new" for 127.0.0.1 at 2020-01-04 00:23:10 +0800
Processing by ShopReviewsController#new as HTML
Parameters: {"shop_id"=>"11"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 9], ["LIMIT", 1]]
UserProfile Load (0.2ms) SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = $1 LIMIT $2 [["user_id", 9], ["LIMIT", 1]]
↳ app/models/ability.rb:9:in `initialize'
ShopReview Exists? (0.3ms) SELECT 1 AS one FROM "shop_reviews" WHERE "shop_reviews"."user_profile_id" = $1 AND "shop_reviews"."shop_id" = $2 LIMIT $3 [["user_profile_id", 8], ["shop_id", nil], ["LIMIT", 1]]
↳ app/models/ability.rb:11:in `block in initialize'
Shop Load (0.2ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" = $1 ORDER BY "shops"."created_at" DESC LIMIT $2 [["id", 11], ["LIMIT", 1]]
You should use gem pundit to create authorizations in your app. Pundit is easy to scale than gem cancancan. Because pundit allows your app to create authorization on each model. You can search more about two gems and choose what is most suitable for you.

Rails has_many :through dependent :destroy behaving very oddly

User:
class User < ActiveRecord::Base
attr_accessible :email, :username, :password, :password_confirmation, :remember_me
has_many :tasks_users, :dependent => :destroy, :conditions => {:is_owner => true}
has_many :tasks, :through => :tasks_users, :source => :task
Task:
class Task < ActiveRecord::Base
include RankedModel
ranks :sort_order
acts_as_taggable
has_many :tasks_users
has_many :users, :through => :tasks_users
TasksUser:
class TasksUser < ActiveRecord::Base
attr_accessible :is_owner
belongs_to :user
belongs_to :task
validates_uniqueness_of :user_id, :scope => [:user_id, :task_id]
end
They key here is dependent destroy.
Whenever I try to destroy my user, which should destroy the join model, I end up getting this odd sql error:
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 17]]
(0.1ms) begin transaction
ActsAsTaggableOn::Tagging Load (0.1ms) SELECT "taggings".* FROM "taggings" WHERE "taggings"."tagger_id" = 17 AND "taggings"."tagger_type" = 'User'
TasksUser Load (0.1ms) SELECT "tasks_users".* FROM "tasks_users" WHERE "tasks_users"."user_id" = 17 AND "tasks_users"."is_owner" = 't'
Could not log "sql.active_record" event. NoMethodError: undefined method `name' for nil:NilClass
**SQLite3::SQLException: no such column: tasks_users.: DELETE FROM "tasks_users" WHERE "tasks_users"."" = ?**
(0.1ms) rollback transaction
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: tasks_users.: DELETE FROM "tasks_users" WHERE "tasks_users"."" = ?
from /home/steveq/.rvm/gems/ruby-1.9.3-p194#rails32/gems/sqlite3-1.3.7/lib/sqlite3/database.rb:91:in `initialize'
The line with the double asterisk is the line in question - it appears to be searching for a record in "tasks_users"."".
If all I do is change :conditions => {:is_owner => false}, the sql executes without a problem:
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 17]]
(0.1ms) begin transaction
ActsAsTaggableOn::Tagging Load (0.2ms) SELECT "taggings".* FROM "taggings" WHERE "taggings"."tagger_id" = 17 AND "taggings"."tagger_type" = 'User'
TasksUser Load (0.1ms) SELECT "tasks_users".* FROM "tasks_users" WHERE "tasks_users"."user_id" = 17 AND "tasks_users"."is_owner" = 'f'
List Load (0.2ms) SELECT "lists".* FROM "lists" WHERE "lists"."owner_id" = 17
Relationship Load (0.1ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."user_id" = 17
SQL (0.3ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 17]]
(299.4ms) commit transaction
Any ideas about what's happening here, and why having the condition of is_owner change from true to false allows the query and delete statement to execute?
Thanks
Ugh - Hopefully I can save someone else from banging their heads against this wall for an hour.
Problem was how I specified the :conditions.
It needed to be put in double quotes:
has_many :tasks_users, :dependent => :destroy, :conditions => "is_owner = 'true'"
That was it.

Resources