Handling accepts_nested_attributes_for children errors - ruby-on-rails

My setup: Rails 2.3.10, Ruby 1.8.7
users_controller.rb
def character
#user = User.find(params[:id])
respond_to do |format|
if #user.update_attributes(params[:user])
format.json { render :json => #user }
else
format.json { render :json=> #user.errors.full_messages, :status => :unprocessable_entity }
end
end
end
user.rb
accepts_nested_attributes_for :characters
has_many :characters
end
character.rb
belongs_to :user
before_create :check_count
def check_count
if Characters.find(:all, :conditions => ["user_id = ?", self.user_id).count == 3
errors.add_to_base I18n.t :exceeds
false
end
end
end
In the users character method (it's a custom method), I want to create a child character only if there aren't already 3 characters for the user. My question is how to return the error message to the #user object from within check_count method, currently errors refer to the character object, not #user. Thanks in advance for your help.

After some digging around, I found the solution
user.rb
accepts_nested_attributes_for :characters, :before_add :set_parent
has_many :characters
def set_parent(character)
character.user ||= self
end
end
character.rb
belongs_to :user
before_create :check_count
def check_count
if Characters.find(:all, :conditions => ["user_id = ?", self.user_id).count == 3
self.user.errors.add_to_base I18n.t :exceeds
false
end
end
end
Hope this helps someone else.

Related

How do I create a page without a model?

I'm working on an app which has many 'Activities'. Each 'Activity' has many 'Ranks'. I'd like each 'Activity' to have a page called grading, where the user can see a list of all of that activity's ranks and conveniently update them. I imagine the URL would be something like http://localhost:3000/activities/21/grading
I'm already using http://localhost:3000/activities/21/edit for its intended purpose.
I don't need a model for gradings, as I don't need to save any grading records.
I know exactly what to put in the view, I'm just unsure what to add to the controller and routes files. Other people have worked on this app but I'm unable to contact them.
Routes
resources :activities do
collection do
get 'scheduled_classes'
end
end
resources :ranks
end
activities_controller
class ActivitiesController < ApplicationController
def new
#activity = Activity.new
#activity.timeslots.build
#activity.ranks.build
end
def create
#activity = current_club.activities.new(activity_params)
if #activity.save
flash[:success] = "New class created!"
redirect_to activity_path(#activity)
else
render 'new'
end
end
def edit
#activity = current_club.activities.find_by(id: params[:id])
#active_ranks = #activity.ranks.where(active: true)
if !#activity.active?
redirect_to activities_path
else
#activity.timeslots.build
end
end
def update
#activity = current_club.activities.find_by(id: params[:id])
if #activity.update_attributes(activity_params)
flash[:success] = "Class updated!"
redirect_to edit_activity_path(#activity)
else
render 'edit'
end
end
def show
#activity = current_club.activities.find_by(id: params[:id])
#active_ranks = #activity.ranks.where(active: true)
if #activity.nil?
redirect_to root_url
elsif !#activity.active?
redirect_to activities_path
end
end
def index
#activities = current_club.activities.all
end
def destroy
#activity = current_club.activities.find_by(id: params[:id])
if #activity.nil?
redirect_to root_url
else
#activity.destroy
flash[:success] = "Class deleted"
redirect_to activities_path
end
end
end
private
def activity_params
params.require(:activity).permit(:name, :active,
:timeslots_attributes => [:id,
:time_start,
:time_end,
:day,
:active,
:schedule],
:ranks_attributes => [:id,
:name,
:position,
:active])
end
end
activity
class Activity < ApplicationRecord
belongs_to :club
has_many :timeslots, dependent: :destroy
accepts_nested_attributes_for :timeslots,:allow_destroy => true
has_many :ranks, dependent: :destroy
has_many :attendances, dependent: :destroy
accepts_nested_attributes_for :ranks
validates :club_id, presence: true
validates :name, presence: true, length: { maximum: 50 }
end
Your routes don't need to have an associated model or resource.
resources :activities do
collection do
get 'scheduled_classes'
end
member do
get :grading
end
end
will match to activities#grading
See https://guides.rubyonrails.org/routing.html#adding-member-routes for more info.
As you want to add a route on a particular activity, you should add member route on the activity like below,
resources :activities do
collection do
get 'scheduled_classes'
end
get :grading, on: :member
end
Apart from this, you have to add method in ActivitiesController for this route like below,
def grading
#activity = Activity.find_by(id: params[:id])
# do more here
end
In view files, you can create grading.html.erb under activities resources and put your view code there.

DB rolls back on create action

I'm trying to create a form with a series of checks to prevent duplicates during the simultaneous creation of three model records: one for the parent (assuming it doesn't exist), one for its child (assuming it doesn't exist), and one for a join table between the child and the User (to allow the User to have their own copy of the Song object).
In the current state of the code, The checks seemingly pass, but
the server logs show ROLLBACK, and nothing gets saved
to the database EXCEPT the parent object (artist).
When I try to use the ids of the object, I get the error undefined method id for nil:NilClass, or "couldn't find object without an ID".
The following code is in my controller:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_by(name: params[:artist][:name].strip.titleize) #look for the artist
#song = Song.find_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize)
if #artist.present? && #song.present?
#user_song = current_user.user_songs.find(#song_id)
if #user_song.present?
render html: "THIS SONG IS ALREADY IN YOUR PLAYLIST"
render action: :new
else
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
end
elsif #artist.present? && !#song.present?
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
elsif !#artist.present?
#artist = Artist.create(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
else
render html: "SOMETHING WENT WRONG. CONTACT ME TO LET ME KNOW IF YOU SEE THIS MESSAGE"
end
end
def index
#songs = Song.all
end
def new
#artist = Artist.new
#artist.songs.build
#user_song = UserSong.new(user_id: current_user.id, song_id: #song_id)
end
def show
#song_id = params["song_id"]
#song = Song.find(params[:id])
end
def destroy
UserSong.where(:song_id => params[:id]).first.destroy
flash[:success] = "The song has been from your playlist"
redirect_to root_path
end
def edit
#song = Song.find(params[:id])
#artist = Artist.find(#song.artist_id)
end
def update
end
private
def set_artist
#artist = Artist.find(params[:id])
end
def artist_params
params.require(:artist).permit(:name, songs_attributes: [:id, :title, :lyrics])
end
def set_song
#song = Song.find(params["song_id"])
end
end
The models:
class Artist < ApplicationRecord
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class Song < ApplicationRecord
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class UserSong < ApplicationRecord
belongs_to :song
belongs_to :user
end
Sorry if I haven't abstracted enough. Not really sure how, given that there's no error message, just a rollback (without any validations present in any of the controllers).
Thanks to #coreyward and his pointing out of the fat-model skinny-controller lemma (never knew that was a thing), I was able to cut the code down and arrive at a solution immediately. In my models, I used validates_uniqueness_of and scope in order to prevent duplication of records. In my controller, I used find_or_create_by to seal the deal.
To whom it may concern, the final code is as follows:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_or_create_by(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.find_or_create_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize) do |song|
song.lyrics = params[:artist][:songs_attributes]["0"][:lyrics].strip
end
#user_song = current_user.user_songs.find_or_create_by(song_id: #song.id) do |user_id|
user_id.user_id = current_user.id
end
redirect_to root_path
end
class Song < ApplicationRecord
validates_uniqueness_of :title, scope: :artist_id
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class Artist < ApplicationRecord
validates_uniqueness_of :name
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class UserSong < ApplicationRecord
validates_uniqueness_of :song_id, scope: :user_id
belongs_to :song
belongs_to :user
end

Rails: am I nesting incorrectly?

Hi I'm currently working on my first project, and am trying to build the functionality first before doing the login/sessions. I'm trying to create a picture album website, where users have many albums (that contain many pictures), and album access is shared among friends. However, I'm noticing that after my albums#create
http://localhost:3000/users/18/albums/new (no problem here)
I am redirected to albums#show:
http://localhost:3000/albums/20 (problem!!)
shouldn't there be a user_id in the URL as well?? Or does it not have a user_id attached to the URL because it belongs to multiple users? Here are my routes:
Pholder::Application.routes.draw do
resources :users do
resources :albums
end
resources :albums do
resources :pictures
end
root :to => "users#index"
Here are my models in case:
user model
class User < ActiveRecord::Base
has_secure_password
attr_accessible :email, :name, :password, :password_confirmation
validates_presence_of :password, :on => :create
validates_format_of :name, :with => /[A-Za-z]+/, :on => :create
validates_format_of :email, :with => /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
validates_length_of :password, :minimum => 5, :on => :create
has_many :user_albums
has_many :albums, :through => :user_albums
accepts_nested_attributes_for :albums
end
album model
class Album < ActiveRecord::Base
attr_accessible :avatar, :name, :description
has_many :user_albums
has_many :users, :through => :user_albums
has_many :photos
end
photo album
class Photo < ActiveRecord::Base
belongs_to :album
end
albums controller
class AlbumsController < ApplicationController
def index
#albums = Albums.all
respond_to do |format|
format.html
format.json { render json: #albums }
end
end
def show
#albums = Album.all
#album = Album.find(params[:id])
#photo = Photo.new
end
def update
end
def edit
end
def create
# #user = User.find(params[:albums][:user_id])
# #users = User.all
#album = Album.new(params[:album])
# #album.user_id = #user.id
respond_to do |format|
if #album.save
format.html { redirect_to #album, notice: 'Album was successfully created.' }
format.json { render json: #album, status: :created, location: #album}
else
format.html { render action: "new" }
format.json { render json: #album.errors, status: :unprocessable_entity }
end
end
end
def new
#user = User.find(params[:user_id])
#album = Album.new
end
def destroy
end
end
Let me know if you need any other files.
The line redirect_to #album makes you redirect to the show action of the #album in question.
Changing this piece of code to something like redirect_to users_path will make the app redirect to the index action of users_controller and so on.
It depends on whatever behavior you want after the save.
Reading this should be helpful too: http://guides.rubyonrails.org/routing.html

NameError with a Many to Many association

I'm trying to add a Join/Unjoin button to user created Events, similar to a Follow/Unfollow button for Users.
I'm not sure what to define #rsvps as in the event#show
NameError in Events#show
undefined local variable or method `event' for #<#:0x007f9dfaf9d978>
show.html.erb
<%= link_to "Join Event", rsvps_path(:event_id => event), :method => :post %>
events_controller.rb
def show
#event = Event.find(params[:id])
#user = current_user
##rsvp = ???? something here ????
end
rsvps_controller.rb
class RsvpsController < ApplicationController
before_filter :signed_in_user
def create
#rsvp = current_user.rsvps.build(:event_id => params[:event_id])
if #rsvp.save
flash[:notice] = "Joined event."
redirect_to root_url
else
flash[:error] = "Unable to join event."
redirect_to root_url
end
end
def destroy
#rsvp = current_user.rsvps.find(params[:id])
#rsvp.destroy
flash[:notice] = "Unjoin Event."
redirect_to current_user
end
end
Here are the models
rsvp.rb
class Rsvp < ActiveRecord::Base
attr_accessible :event_id, :user_id
belongs_to :user
belongs_to :event
end
user.rb
has_many :rsvps
has_many :events, through: :rsvps, dependent: :destroy
event.rb
belongs_to :user
has_many :rsvps
has_many :users, through: :rsvps, dependent: :destroy
Your undefined local variable or method error seems to be coming from trying to pass :event_id => event to your controller through rsvp_path. Instead you should just be passing the event object like so
<%= link_to "Join Event", rsvps_path(event), :method => :post %>
the line #event = Event.find(params[:id]) in your controller will take care of figuring out what event you passed to it.
I think this code would be more rails-ish.
# user.rb
has_many :users_events
has_many :events, through: :users_events
# event.rb
has_many :users_events
has_many :users, through: :users_events
# users_event.rb
belongs_to :user
belongs_to :event
ActiveRecord do everything else. 8)
For example user.events and event.users methods.
Join and unjoin to user actions may be processed by events controller. Update method can look like this
# events_controller.rb
def update
respond_to do |format|
#event = Event.find(params[:id])
#event.users << current_user if params[:action] == 'join'
#event.users.delete(current_user) if params[:action] == 'unjoin'
if #event.update_attributes(params[:event])
format.html { redirect_to #event, notice: 'Event was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #event.errors, status: :unprocessable_entity }
end
end
end
A little bit messy, but I hope the idea is clear.

Rails many to many token fields help

I am having that problem that my model dont want to save. I have a token field input for tags.
I have followed this guide for the token input: http://railscasts.com/episodes/258-token-fields
I get this error when I try to create a new konkurrancer:
NoMethodError in Admin/konkurrancersController#create
undefined method `class_name' for nil:NilClass
Rails.root: C:/Rails/konkurranceportalen
Application Trace | Framework Trace | Full Trace
app/models/konkurrancer.rb:15:in `tag_tokens='
app/controllers/admin/konkurrancers_controller.rb:48:in `new'
app/controllers/admin/konkurrancers_controller.rb:48:in `create'
http://pastie.org/1834194
Request
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"yo7wcAQl81tx3zZpPP44ENPYzYRZLpgyYKY+HK3yFKM=",
"konkurrancer"=>{"name"=>"Vind en rejse",
"banner2"=>"asdasd",
"tracking"=>"sadasd",
"vaerdi"=>"12222",
"tid"=>"1 min",
"tag_tokens"=>"1",
"bedom"=>"2",
"kategori_id"=>"9",
"form"=>"Nyhedsbrev",
"partner"=>"Iqmedier",
"udtraekkes(3i)"=>"30",
"udtraekkes(2i)"=>"4",
"udtraekkes(1i)"=>"2011",
"udtraekkes(4i)"=>"08",
"udtraekkes(5i)"=>"26",
"arrangeor"=>"",
"note"=>""},
"commit"=>"Opret konkurrence"}
My konkurrancer model:
class Konkurrancer < ActiveRecord::Base
attr_accessible :name, :tag_tokens
has_many :tagsmenus
has_many :tags, :through => :tagsmenus
attr_reader :tag_tokens
def tag_tokens=(ids)
self.tag_ids = ids.split(",")
end
end
My tag model:
class Tag < ActiveRecord::Base
has_many :tagsmenus
has_many :konkurrancers, :through => :tagsmenus
has_friendly_id :name, :use_slug => true
before_save :assign_cached_slug, :unless => :cached_slug?
protected
def assign_cached_slug
self.cached_slug = self.name.gsub(/\s+/, '_').gsub(/[^\w\-]/, '')
end
end
My tagmenu model:
class Tagsmenu < ActiveRecord::Base
end
My controller:
def new
#konkurrancer = Konkurrancer.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #konkurrancer }
end
end
# POST /konkurrancers
# POST /konkurrancers.xml
def create
#konkurrancer = Konkurrancer.new(params[:konkurrancer])
respond_to do |format|
if #konkurrancer.save
format.html { redirect_to(:admin_konkurrancers, :notice => 'Konkurrancer was successfully created.') }
format.xml { render :xml => :admin_konkurrancers, :status => :created, :location => #konkurrancer }
else
format.html { render :action => "new" }
format.xml { render :xml => #konkurrancer.errors, :status => :unprocessable_entity }
end
end
end
I have created the join table and the model and also added the relation to my tag model.
Your model has some conflicting statements. You first define:
attr_accessor ... :tag_tokens
then later have:
attr_reader :tag_tokens
which is not necessary given the first line or vice versa given that later you have a deinition for the setter:
def tag_tokens(ids)
self.tag_ids = ids.split(',')
end
I don't see tag_ids defined either given it is not a column in your table. You should probably remove the attr_accessor definition for the tag_tokens and then define the tag_ids method for starters.
class Tagsmenu < ActiveRecord::Base
belongs_to :konkurrancer
belongs_to :tag
end

Resources