Rails: Uniqueness of two attributes in join table causing 500 error - ruby-on-rails-3

I have the following models, which basically are trying to mean that a professor has knowledge of many subjects for a particular level. The subjects are fixed, so there will be no new subjects created, there will be just "related" to a professor through the knowledge join table.
class Subject < ActiveRecord::Base
# Self Associations
has_many :subcategories, :class_name => "Subject"
belongs_to :category, :class_name => "Subject",:foreign_key => "parent_id"
# Associations
has_many :knowledges
has_many :professors, :through => :knowledges
end
class Professor < ActiveRecord::Base
# Associations
has_many :knowledges
has_many :subjects, :through => :knowledges
...
end
class Knowledge < ActiveRecord::Base
# Associations
belongs_to :professor
belongs_to :subject
has_one :level
attr_accessible :subject_id, :professor_id
validates :subject_id, :uniqueness => { :scope => :professor_id }
end
I want to have a form that will let a professor to add a subject to his account, and I decided to have a form for a knowledge (as I want to be able to insert a level too).
It looks like this:
<%= simple_form_for #knowledge,:url => professor_knowledges_path, :html => { :class => 'form-horizontal' } do |f| %>
<div class="control-group select optional">
<%= label_tag "Subject Type", nil, :class => "select optional control-label"%>
<div class="controls">
<%= select_tag "Parent Subject", options_from_collection_for_select(#parent_subjects, "id", "name"), :id => "knowledge_parent_subject" %>
</div>
</div>
<%= f.input :subject_id, :collection => #subjects, :label => "Subject" %>
<%= f.input :level %>
<%= f.button :submit, t('add_form'),:class => 'btn-primary' %>
<% end %>
And in the create action of the Knowledges controller I have this:
def create
#knowledge = Knowledge.create(:professor_id => current_professor.id, :subject_id => params[:knowledge][:subject_id])
end
I would like/expect to get an ActiveRecord saying that this knowledge can't be inserted because there is a uniqueness violation, but nops, I just see a 500 in the logs and a rollback, but it seems the execution goes on. So my question is: What am I doing wrong, or how I could improve this modeling situation? I believe the form needs to be related to the join model as I want to have fields of that model on it...But maybe I am wrong, and I could do in an easy/cleaner way.
EDIT:
As asked in one of the comments, here is the log of the submission of the form and the 500 error right after the rollback:
Started POST "/professors/1/knowledges" for 127.0.0.1 at 2012-07-01 00:45:39 -0700
Processing by KnowledgesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"4JVyxWnIh37kyBwLwLGTHk/znsI1c5wrJvaWjKKT5tM=", "Parent Subject"=>"1", "knowledge"=>{"subject_id"=>"1"}, "commit"=>"Añadir", "professor_id"=>"1"}
Professor Load (0.4ms) SELECT `professors`.* FROM `professors` WHERE `professors`.`id` = 1 LIMIT 1
Completed 500 Internal Server Error in 4ms
I added some conditions in the create action, like this:
def create
#knowledge = Knowledge.new(:professor_id => current_professor.id, :subject_id => params[:knowledge][:subject_id])
if #knowledge.save
flash[:notice] = "Success..."
redirect_to professor_path(current_professor)
else
render :action => 'new'
end
end
And this actually shows the following right after the 500:
Completed 500 Internal Server Error in 6ms
ActiveRecord::RecordInvalid (Validation failed: Subject has already been taken):
I wonder why the exception is raised instead of just adding the errors into the object and let me manage that situation. Isn't what the following line should be doing?
validates :subject_id, :uniqueness => { :scope => :professor_id }

That error means you are trying to insert duplicate subject_id / professor_id pairs on that table. Most often happens when either the subject_id or professor_id is null.
Are you sure the controller is getting the correct parameters? I would check the logs to make sure the inserts are what you would expect.

I don't have enough reputation to comment...my answer is more some things to try than a definitive answer, sorry.
It looks like the save is failing due to validation errors. You can try to handle those in your 'else' block. The following will give you a description of all validation errors (useful for debugging).
#knowledge.errors.full_messages
You haven't shown what is happening in the 'new' action. I suspect this is where the errors are occurring.
Does the same issue occur (i.e. the validation problem) in the console? If so, try cleaning out your databases (beware - the following will erase & rebuild all your databases).
rake db:drop:all db:create:all db:migrate db:test:prepare
Also, if you haven't already, add an index to your migration for Knowledge to prevent duplicates being added to the db. e.g.
add_index :knowledges, [ :professor_id, :subject_id ], unique: true

Related

Rails - Saving in a table and update/creating in another one

I am creating a small app to manage my own online portfolio but I can't solve an issue with update.
The main table of my database is works, then there's the tables authors and clients where I set the fields authorName and clientName as unique. Authors and clients can have multiple works, but a work can only have one of each.
In the form where I create works I have a field for the authorName and another one for clientName: if the author doesn't exist in the authors table it gets created, otherwise the existing one gets linked to the work. Same thing with the client. To achieve this I am using first_or_initialize and it works perfectly. Problems start when I try to use the same method inside the update action. These are my models:
Work Model
class Work < ApplicationRecord
has_one :description
belongs_to :author
belongs_to :client
accepts_nested_attributes_for :client
accepts_nested_attributes_for :author
scope :active, lambda {where(:isActive => true)}
scope :descOrder, lambda {order(:date => :desc)}
scope :cover, lambda {where(:isCover => true)}
end
Author Model
class Author < ApplicationRecord
has_many :works
end
Client Model
class Client < ApplicationRecord
has_many :works
end
Quick edit: the models as you see them are exactly as they are in my app. No code has been removed.
This is the form in my view:
<%= form_for(#work, :url => { :controller => "projects", :action => "update"} ) do |f| %>
<%= f.label("title") %>
<%= f.text_field(:title) %>
<%= f.fields_for(:author) do |author| %>
<%= author.label("author") %>
<%= author.text_field(:authorName) %>
<% end %>
<%= f.fields_for(:client) do |client| %>
<%= client.label("client") %>
<%= client.text_field(:clientName) %>
<% end %>
<%= f.label("date") %>
<%= f.date_field(:date) %>
<%= f.submit("update") %>
<% end %>
And this is how I am handling it in the controller:
def edit
#work = Work.find(params[:id])
if #work.client.nil?
#work.build_client
end
if #work.author.nil?
#work.build_author
end
end
def update
#work = Work.find(params[:id])
#work.client = Client.where(clientName: work_params["client_attributes"]["clientName"]).first_or_initialize
#work.author = Author.where(authorName: work_params["author_attributes"]["authorName"]).first_or_initialize
if #work.update(work_params)
flash[:notice] = "work: #{#work.title} updated successfully."
redirect_to(project_path(#work))
else
redirect_to new_project_path
end
end
private
def work_params
params.require(:work).permit(:title, :date, client_attributes: [:id, :clientName], author_attributes: [:id, :authorName])
end
This is the error that I get:
Started PATCH "/projects/21" for ::1 at 2019-10-14 20:00:27 -0700
Processing by ProjectsController#update as HTML
Parameters: {"authenticity_token"=>"rw3X7VJg8CDnOibniv1jKHTVTGp7pjE4ep6xHpHy0Zp8Xv/0uQd6y5xqq629M2FOOQNoYyOAXH//w5/VoeNEOA==", "work"=>{"title"=>"Progetto1", "author_attributes"=>{"authorName"=>"Autore1", "id"=>"34"}, "client_attributes"=>{"clientName"=>"Cliente4", "id"=>"30"}, "date"=>""}, "commit"=>"update", "id"=>"21"}
Work Load (0.7ms) SELECT `works`.* FROM `works` WHERE `works`.`id` = 21 LIMIT 1
↳ app/controllers/projects_controller.rb:42:in `update'
Client Load (0.6ms) SELECT `clients`.* FROM `clients` WHERE `clients`.`clientName` = 'Cliente4' ORDER BY `clients`.`id` ASC LIMIT 1
↳ app/controllers/projects_controller.rb:44:in `update'
Author Load (0.6ms) SELECT `authors`.* FROM `authors` WHERE `authors`.`authorName` = 'Autore1' ORDER BY `authors`.`id` ASC LIMIT 1
↳ app/controllers/projects_controller.rb:45:in `update'
Completed 404 Not Found in 15ms (ActiveRecord: 1.9ms | Allocations: 3805)
ActiveRecord::RecordNotFound (Couldn't find Client with ID=30 for Work with ID=21):
app/controllers/projects_controller.rb:47:in `update'
Even though those record do exist in the database (with those IDs that you see in the error) and their foreign keys are correctly stored in the works table (I checked in mysql).
What I am expecting to achieve is the same behaviour of the new action (that I described in the beginning of my post).
How can I solve this? Thank you!
Small update: if I change first_or_initialize with first_or_create it does create the author (or client), if not existing, but in the same time it still gives me the same error.
After dozens of tests I eventually came up with a sort of solution - it's not "elegant" and probably it's not the best, but at least it works without interruptions:
def edit
#work = Work.find(params[:id])
if #work.client.nil?
#work.build_client
end
if #work.author.nil?
#work.build_author
end
end
def update
#work = Work.find(params[:id])
#work.client = Client.where(clientName: work_params["client_attributes"]["clientName"]).first_or_create
#work.author = Author.where(authorName: work_params["author_attributes"]["authorName"]).first_or_create
if #work.update(update_params)
flash[:notice] = "work: #{#work.title} updated successfully."
redirect_to(project_path(#work))
else
redirect_to new_project_path
end
end
private
def work_params
params.require(:work).permit(:title, :date, client_attributes: [:id, :clientName], author_attributes: [:id, :authorName])
end
def update_params
params.require(:work).permit(:title, :date)
end
Being that first_or_initialize works smoothly with save method, but it gets stuck - at least in my code - with update, I replaced it with first_or_create.
This time, however, I am using another private method update_params which simply ignores :client_attributes and :authors_attributes.
This results in an Unpermitted parameters: :author_attributes, :client_attributes error, as expected, but at least everything goes through and gets updated.
Honestly I am not quite sure that I totally understood why it's working. But it does.
Anyway, if anyone have a better solution to this problem I am totally open to improve it.

Rails: nested_form gem remove not working but add works

My problem is somewhat similar to question nested_form gem add works but remove fails...why?.
I have a product edit page, where sub-categories of products are linked in product_sub_categories. To assign sub-categories to product, I used nested attributes for product_sub_categories. So, product can have more than one sub_categories.
In product model,
has_many :product_sub_categories
has_many :sub_categories, :through => :product_sub_categories
accepts_nested_attributes_for :product_sub_categories, :allow_destroy => true
And in product edit view:
<%= f.fields_for :product_sub_categories do |product_sub_category| %>
<%= product_sub_category.collection_select :sub_category_id, #sub_categories, :id, :sub_category, {:include_blank => 'Select a Sub Category'} %>
<%= product_sub_category.link_to_remove "Remove", :class => "subcatlink" %>
<% end %>
Code works well for adding sub-categories. But fails when I remove sub-category. Log gives:
"product_sub_categories_attributes"=>{"0"=>{"sub_category_id"=>"1", "_destroy"=>"false", "id"=>"9"}, "1"=>{"sub_category_id"=>"1", "_destroy"=>"1", "id"=>"17"}},
ProductSubCategory Load (0.2ms)[0m [1mSELECT `product_sub_categories`.* FROM `product_sub_categories` WHERE `product_sub_categories`.`product_id` = 8 AND `product_sub_categories`.`id` IN (9, 17)
Though, i click on Remove, it just passes _destroy="1", but doesn't destroy sub-category.
Can anybody tell the solution?
Update:
Extremely sorry for my stupid mistake. I didnt see code properly. In the model i duplicated
accepts_nested_attributes_for :product_sub_categories
without :allow_destroy => true. When i removed it, code worked properly.
You need to add dependent destroy clause next to association, and it will destroy product's sub categories.
has_many :product_sub_categories, :dependent => :destroy

rails 3 how to associate a new object without mass-assign error

I came back to the relatively "old book" Head First rails, which was published for Rails 2.3.
Now, going back again through those samples and using Rails 3 I came up with some questions.
Let's say that I'm adapting the sample for coconut airways and instead of flights and seats, I have a project and tasks.
The page shows a project description and below a list of tasks associated to that project. so far so good. now below that there is a form to create new task. This task needs a Task object and the project_id. here is when things do not work as before.
if you want to do it like the old style you will type:
<%= render :partial => "new_task",
:locals => {:task => Task.new(#project.id)} %>
well, this is showing the mass-assign error.
Then I tried to pass both as parameter:
<%= render :partial => "new_task",
:locals => {:task => Task.new, :project_id => #project.id} %>
and assign it in the partial
<%= f.hidden_field :project_id, :value => project_id %>
any hint?
EDITED:
class Task < ActiveRecord::Base
belongs_to :project
attr_accessible :title
end
class Project < ActiveRecord::Base
has_many :tasks
attr_accessible :description, :title
end
If you change your model's attr_accessible you can include these assignments to be made. For more information about attr_accessible and mass assignment see: Ruby on Rails API

mongoid save embedded documents

I'm trying to build up on the following tutorial from railscast:
http://railscasts.com/episodes/196-nested-model-form-part-1
I'm trying to make everything work with mongodb and mongoid.
the scenario is:
I want to creates events linked to a location. Each events (dance class) contains many lessons.
So I thought that an embedded relationship would be perfect.
Here are my models
model Lesson
class Lesson
include Mongoid::Document
include Mongoid::Slug
field :name, :type => String
embedded_in :event
slug :name
end
model Event
class Event
include Mongoid::Document
include Mongoid::Slug
include Mongoid::Timestamps
include Mongoid::MultiParameterAttributes
field :name, :type => String
field :description, :type => String
field :date, :type => DateTime
validates_presence_of :name
has_one :venue
referenced_in :venue
embeds_many :lessons
slug :name
end
model Venue
class Venue
include Mongoid::Document
include Mongoid::Slug
include Mongoid::Timestamps
include Mongoid::MultiParameterAttributes
field :name, :type => String
field :location, :type => String
validates_presence_of :name, :location
belongs_to :event
slug :name
end
event controller
def create
#event = Event.new(params[:event])
if #event.save
flash[:notice] = 'Event was successfully created.'
end
respond_with(#Event, :location => events_url)
end
def update
# #event = Event.find(params[:id])
#event = Event.find_by_slug(params[:id])
if #event.update_attributes(params[:event])
flash[:notice] = "Event was succesfully updated"
end
respond_with(#event)
end
Then I have my Event view where I can create events and link it to a Venue. But I'd like to be abe to create the lessons from the Event view/model.
so I used the fields_for to generate a field linked to the Lessons model.
= form_for #event do |f|
.field
= f.label :name
%br/
= f.text_field :name
.field
= f.label :description
%br/
= f.text_area :description
.field
= f.label :venue_id
%br/
= f.collection_select :venue_id, Venue.all, :id, :name
.field
= f.label :date
%br/
= f.datetime_select :date
%h3 Add a Class
= f.fields_for :lessons do |builder|
= render "lesson_fields", :f => builder
.actions
= f.submit 'Save'
When I create or edit a new event I get an error message:
undefined method `extract_id' for "test":String
But the request parameter message on the error page shows my lessons value in the Event document.
"lessons"=>{"name"=>"test name lesson"}
When I remove the fields_for line, everything works fine. But then i don't know how to save the value for the nested documents.
I have same problem with embeds_many, but when i try change to has_many. It works!. Maybe you can try too.
can you post the exact code you use to create the Event, including parameters?
which version of Mongoid and Rails are you using?
First thing I noticed is that the following parameter hash does not match your Lessons model:
"lessons"=>{"content"=>"test name lesson"} # this looks wrong
this should be:
"lessons"=>{"name" => "test name lesson"}
Looks like your lessons form has the wrong label for the text input field .. it should be :name , not :content
To dry things up, you might want to try if the 'nested_form' gem works for you:
after installing the gem, use the nested_form_for instead of form_for in your view.
Check here for a more detailed description:
How can I handle this type of multi level forms in rails
See:
https://github.com/ryanb/nested_form (it's also referenced in the RailsCast you mentioned)
You also might want to check this:
field_for and nested form with mongoid
The conclusion of this story is...
I removed everything related to mongoid_slug and it started to work.
I then put everything back as it was to try to find out how to make it work with mongoid_slug and it just worked, like out of the box.
:(
Please include the following code in model event.rb
**accepts_nested_attributes_for :lessons**
This will fix your problem

New record not being saved with any values

I started the question differently, about a collection_select, but I found out that is not the problem.
This particular model won't save any data at all. It just ignores the values in the parameters. I can only save new records with NULL values (except for the timestamp fields).
See my comment for my latest try to fix it.
I have generated a few models with the handy scaffold command. Now I have tried to change a textbox to a collection_select for linking the new entity to the correct related one.
Using rails 3.1RC4 (hopefully this is not a bug).
In the _form.html.erb I use the following code:
<div class="field">
<%= f.label :category_id %><br />
<%= f.collection_select(:category_id, Admin::Category.all, :id, :name) %>
</div>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
...all other items...
<div class="actions">
<%= f.submit %>
</div>
After I click the submit button I receive error messages. It says that the name and permalink do not comply to the validation. I don't understand however, because in the logfiles I find this:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"my token is here", "admin_branche"=>{"category_id"=>"3", "name"=>"Verzekeraars", "permalink"=>"verzekeraars", "visible"=>"1"}, "commit"=>"Create Branche"}
To me it seems that the params contain all the needed values.
For the sake of completeness I will post my create method and model below.
So far I have tried switching back and forth between collection_select and f.coll... with no success. The current setup seems most appropriate to me, based on the logs.
I have also googled a lot, but haven't been able to find the answer. Question 2280106 on this site looks the same, but it had to do with attr_accessible which I have commented out in the model (I restarted the server afterwards and retried, just to be sure).
Help is much appreciated!
branche.rb:
class Admin::Branche < ActiveRecord::Base
# attr_accessible :name, :permalink
#relationships
has_many :courses, :as => :parent
belongs_to :category
#validations
validates :name, :presence => true, :length => {:maximum => 255}
validates :permalink, :presence => true, :length => { :within => 4..25 }
end
create action in the controller:
def create
#admin_branch = Admin::Branche.new(params[:admin_branch])
respond_to do |format|
if #admin_branch.save
format.html { redirect_to #admin_branch, notice: 'Branche was successfully created.' }
format.json { render json: #admin_branch, status: :created, location: #admin_branch }
else
format.html { render action: "new" }
format.json { render json: #admin_branch.errors, status: :unprocessable_entity }
end
end
end
In the controller, you're doing this:
#admin_branch = Admin::Branche.new(params[:admin_branch])
You should do this:
#admin_branch = Admin::Branche.new(params[:admin_branche])
If you look at the request parameters, the attributes are under "admin_branche", not "admin_branch".
I think that should solve your problems, if not, please let us know.
If you have problems with the generated inflections, you can completely customize them in the config/initializers/inflections.rb
just add something like this:
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'branch', 'branches'
end