RoR: Save information from 3 models at same time - ruby-on-rails

I am trying to make it so that when I save an answer, I also save the prop_id that is associated with that answer.
I have a nested route relationship so that each prop (stands for proposition or bet) has a an associated answer like this: http://localhost:3000/props/1/answers/new.
Right now, when I save an answer, I save the answer choice and the user_id who created the answer. I need to save also the prop that is associated with the answer.
Answers Controller:
class AnswersController < ApplicationController
attr_accessor :user, :answer
def index
end
def new
#prop = Prop.find(params[:prop_id])
#user = User.find(session[:user_id])
#answer = Answer.new
end
def create
#prop = Prop.find(params[:prop_id])
#user = User.find(session[:user_id])
#answer = #user.answers.create(answer_params)
if #answer.save
redirect_to root_path
else
render 'new'
end
end
def show
#answer = Answer.find params[:id]
end
end
private
def answer_params
params.require(:answer).permit(:choice, :id, :prop_id)
end
Answer Model
class Answer < ActiveRecord::Base
belongs_to :prop
belongs_to :created_by, :class_name => "User", :foreign_key => "created_by"
has_many :users
end
Prop Model
class Prop < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :answers
end
User Model
class User < ActiveRecord::Base
has_many :props
has_many :answers
has_many :created_answers, :class_name => "Answer", :foreign_key => "created_by"
before_save { self.email = email.downcase }
validates :username, presence: true, uniqueness: {case_sensitive: false}, length: {minimum: 3, maximum: 25}
has_secure_password
end

Just modify your code a little bit, and it will work:
def create
#user = User.find(session[:user_id])
#prop = #user.props.find_by(id: params[:prop_id])
#answer = #user.answers.build(answer_params)
#answer.prop = #prop
# Modify #user, #prop or #answer here
# This will save #user, #prop & #answer
if #user.save
redirect_to root_path
else
render 'new'
end
end

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 User trying to follow 2 Events, but instead follows one event twice

Here is the progression of what we are trying to do and what is actually happening:
User creates Event with id=1
User creates Event with id=2
User clicks "Attend" for Event 1---> User is attending Event 1
User clicks "Attend" on Event 2 ---> User is not attending Event 2, but is attending Event 1 a second time
Any insight onto why this may be happening would be greatly appreciated.
User.rb model
class User < ActiveRecord::Base
has_many :friends, class_name: "Friend", foreign_key: "follower_id",
dependent: :destroy
has_many :fellows, class_name: "Friend", foreign_key: "followed_id", dependent: :destroy
has_many :following, through: :friends, source: :followed
has_many :followers, through: :fellows, source: :follower
has_many :relationships, class_name: "Relationship", foreign_key: "user_id"
has_many :relations, class_name: "Relationship", foreign_key: "event_id"
has_many :coming, through: :relationships, source: :user
has_many :going, through: :relations, source: :event
has_attached_file :photo
validates_attachment_content_type :photo, :content_type => ["image/jpg", "image/jpeg", "image/png", "image/gif"]
def self.omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name
user.dog_name = "A"
user.dog_breed = "A"
user.dog_gender = "Unknown"
user.dog_age = "0"
user.image = auth.info.image
user.save!
end
end
validates :name, presence: true
validates :uid, presence: true
validates_length_of :dog_name, minimum: 0, maximum: 30, message: 'cannot have more than 30 characters'
validates_format_of :dog_name, :with => /\A[a-zA-Z-']+\z/, message: "can only contain letters, ' and -"
validates_length_of :dog_breed, minimum: 0, maximum: 30, message: 'cannot have more than 30 characters'
validates_format_of :dog_breed, :with => /\A[a-zA-Z-']+\z/, message: "can only contain letters, ' and -"
# Follows a user.
def follow(other_profile)
friends.create(followed_id: other_profile.id)
end
# Unfollows a user.
def unfollow(other_profile)
friends.find_by(followed_id: other_profile.id).destroy
end
# Returns true if the current user is following the other user.
def following?(other_profile)
following.include?(other_profile)
end
def coming(event)
relationships.create(event_id: event.id)
end
def unattend(event)
relationships.find_by(event_id: event.id).destroy
end
def attending?(event)
going.include?(event)
end
# Profile will follow an event
end
event.rb model
class Event < ActiveRecord::Base
has_many :relationships, class_name: "Relationship", foreign_key: "user_id"
has_many :relations, class_name: "Relationship", foreign_key: "event_id"
has_many :coming, through: :relationships, source: :user
has_many :going, through: :relations, source: :event
validates :event_name, presence: true
validates :place, presence: true
validates_length_of :event_name, minimum: 1, maximum: 30, message: "Event Name must be at least 1 character and less than 30 characters"
validates_length_of :place, minimum: 1, maximum: 50, message: "Event location must be at least 1 character and less than 50 characters"
#validates_format_of :event, :with => /\A[a-zA-Z0-9-']+\z/
def going(user)
relations.create(user_id: user.id)
end
def uncome(user)
relations.find_by(user_id: user.id).destroy
end
def coming?(user)
coming.include?(user)
end
end
events_controller.rb
class EventsController < ApplicationController
before_filter :requireLogin
def requireLogin
if session[:user_id] == nil
redirect_to "/"
end
end
def index
#events = Event.all
end
def main
#event = Event.find(params[:id])
#current_user ||= User.find(session[:user_id]) if session[:user_id]
#creator = User.find(#event.createdByID)
end
def new
#event = Event.new
#current_user ||= User.find(session[:user_id]) if session[:user_id]
#event.createdByID = #current_user.id
end
def coming
#title = "coming"
#event = Event.find([:id])
#events = #event.coming.paginate(page: params[:page])
render 'show'
end
def create
#event = Event.new(event_params)
#event.createdByID = Rails.application.config.currUserID
if #event.save
redirect_to '/event'
else
render 'new'
end
end
def edit
#event = Event.find(params[:id])
if #event.update_attributes(event_params)
redirect_to action: "show"
# Handle a successful update.
else
render 'edit'
end
end
def destroy
#event = Event.find(params[:id]).destroy
redirect_to(events_path)
end
private
def event_params
params.require(:event).permit(:event_name, :place, :time_of)
end
end
Relationships controller
class RelationshipsController < ApplicationController
def create
event = Event.find(params[:event_id])
current_user.coming(event)
redirect_to '/event'
end
def destroy
event = Relationship.find(params[:id]).event
current_user.unattend(event)
redirect_to '/event'
end
def main
#event = Event.find(params[:id])
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
_attend.html.erb
<%= form_for(#current_user.relationships.build) do |f| %>
<div><%= hidden_field_tag :event_id, #event.id %></div>
<%= f.submit "Attend", class: "btn btn-primary" %>
<% end %>
_attend_form.html.erb
<% unless current_user = nil %>
<div id="attend_form">
<% if #event.coming?(#current_user) %>
<%= render 'leave' %>
<% else %>
<%= render 'attend' %>
<% end %>
</div>
<% end %>
Where _attend_form.html.erb is rendered? What controller and action?
In RelationshipsController#main you have:
#event = Event.find(params[:id])
But in create action it's:
event = Event.find(params[:event_id])
Check that you pass correct param for event id in main action.

how to access params in a before_destroy callback in Rails

How do I write a before_destroy callback for the following controller:
class RelationshipsController < ApplicationController
....
def destroy
#user = Relationship.find(params[:id]).followed
current_user.unfollow(#user)
respond_to do |format|
format.html { redirect_to #user }
format.js
end
end
end
class Relationship < ActiveRecord::Base
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
before_destroy :delete_car_permissions
private
def delete_car_permissions
car_ids = followed.car_ids
Permission.where("thing_id IN (?) AND user_id = ?", car_ids, follower).delete_all
end
end
The delete_car_permissions doesn't work since I cannot access params in the model!
You do not need params[:id] to identify the Relationship, because before_destroy callback runs on instance, not on class.
It is enough to have:
def delete_car_permissions
car_ids = followed.car_ids
Permission.where("thing_id IN (?) AND user_id = ?", car_ids, follower).delete_all
end

CanCan for employees and users

I have a Ticket model, an Employee model, and a User model.
Users and Employees can create tickets, but employees also have tickets assigned to them. So user_id refers to the creator of the ticket, and employee_id refers to the assigned employee (I am not sure if this the best way or not).
Ticket.rb
class Ticket < ActiveRecord::Base
before_save :default_values
after_commit :close_solved
after_commit :close_canceled
before_create :assign_state
attr_accessible :description, :title, :employee_department_id, :user_id, :first_name, :last_name , :email, :state_id, :employee_id, :ticket_state, :assign_state
belongs_to :employee_department
belongs_to :user
belongs_to :state
belongs_to :employee
has_many :replies
def default_values
self.state_id = 3 if self.state_id.nil?
end
def to_label
ticket_state.to_s
end
def close_solved
if self.ticket_state == "solved"
self.update_column(:ticket_state, "closed (solved)")
self.save!
end
end
def close_canceled
if self.ticket_state == "canceled"
self.update_column(:ticket_state, "closed (canceled)")
self.save!
end
end
def assign_state
if self.employee_id.nil?
self.assign_state = "un-assigned"
else
self.assign_state = "assigned"
end
end
Ticket.all.each do |ticket|
if ticket.ticket_state.blank?
ticket.ticket_state = 'open'
end
ticket.save
end
end
Employee.rb
class Employee < ActiveRecord::Base
# attr_accessible :title, :body
after_create :add_to_users
attr_accessible :employee_number, :joining_date, :first_name, :middle_name, :last_name,
:gender, :job_title, :employee_department_id, :qualification, :experience_detail,
:experience_year, :experience_month, :status_description, :date_of_birth, :marital_status,
:children_count, :father_name, :mother_name, :husband_name, :blood_group, :nationality_id,
:home_address_line1, :home_address_line2, :home_city, :home_state, :home_pin_code,
:office_address_line1, :office_address_line2, :office_city, :office_state, :office_pin_code,
:office_phone1, :office_phone2, :mobile_phone, :home_phone, :email, :fax, :user_id, :school_id,
:employee_category_id, :employee_position_id, :reporting_manager_id, :employee_grade_id,
:office_country_id, :home_country_id
belongs_to :employee_department
belongs_to :employee_category
belongs_to :employee_position
belongs_to :employee_grade
belongs_to :nationality, class_name: 'Country'
belongs_to :reporting_manager, class_name: "Employee"
belongs_to :school
belongs_to :user
has_many :tickets
def add_to_users
new_user = User.new
new_user.user_name = self.first_name
new_user.first_name = self.first_name
new_user.last_name = self.last_name
new_user.email = self.email
new_user.password = "123456"
new_user.password_confirmation = "123456"
new_user.user_type_id = 2
new_user.save
t = Employee.find(self.id)
t.user_id = new_user.id
t.save
end
def to_label
full_name = first_name + " " + last_name
end
def full_name
full_name = first_name + " " + last_name
end
end
User.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :validatable,:confirmable and :omniauthable
devise :database_authenticatable, :registerable, :recoverable, :rememberable,
:trackable, :lockable, :timeoutable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :user_name, :first_name, :last_name, :password, :password_confirmation, :remember_me,
:role_ids, :current_password, :user_type
attr_accessor :current_password
# attr_accessible :title, :body
has_many :assignments
has_many :roles, :through => :assignments
has_many :articles
has_many :comments
has_many :students
has_many :guardians
has_many :employees
has_many :tickets
has_many :permissions
accepts_nested_attributes_for :tickets
def has_role?(role_sym)
roles.any? { |r| r.role_name.underscore.to_sym == role_sym }
end
end
Ability.rb
class Ability
include CanCan::Ability
def initialize(user)
#user = user || User.new
if user.has_role? :administrator
can :manage, :all
end
if user.has_role? :admission_manager
can :manage, Student
end
if user.has_role? :news_manager
can :manage, Article
end
if user.has_role? :ticket_manager
can :manage, Ticket
end
if user.has_role? :student_viewer
can :read, Student
end
if user.has_role? :news_viewer
can :read, Article
end
if user.has_role? :ticket_viewer #he should be able to create tickets and see what he has created.
can :create, Ticket
can :read, Ticket
end
end
end
Ticket_controller.rb
class TicketsController < ApplicationController
load_and_authorize_resource
def index
#tickets = Ticket.all
#tickets_grid = initialize_grid(Ticket, :include => [{:user => :user_type}, :employee_department, :state])
end
def show
#ticket = Ticket.find(params[:id])
#reply = #ticket.replies.build # this for comments on ticket
#state = State.all # this for a model called State which describe the priority of the ticket (Emergency / High / Normal )
end
def new
#ticket = Ticket.new
end
def create
#ticket = Ticket.new(params[:ticket])
if #ticket.save
flash[:notice] = 'Support ticket request created.'
redirect_to #ticket
else
flash[:error] = 'An error occurred please try again!'
redirect_to '/dashboard'
end
end
def edit
#ticket = Ticket.find(params[:id])
end
def update
#ticket = Ticket.find(params[:id])
if #ticket.update_attributes(params[:ticket])
flash[:notice] = 'Successfuly updated.'
redirect_to tickets_path
else
flash[:error] = 'An error occurred please try again!'
render #ticket
end
end
end
I need to allow Employees to be able to manage their assigned tickets, and I need the creator of the ticket to see only the tickets he created.
How can I do this using CanCan? I'm open to other suggestions, if it cannot be done with CanCan.
For users to be able to read the tickets they've created, you just need to add a condition on the ability (see below). You can use the same condition on the :create ability and cancan will pre-fill those attributes for you when it builds a new object for the #new or #create actions.
# app/models/ticket.rb
class Ticket < ActiveRecord::Base
# <snip>
belongs_to :user
belongs_to :employee
# <snip>
end
# app/models/user.rb
class User < ActiveRecord::Base
has_one :employee
end
# app/models/ability.rb
class Ability
# <snip>
if user.has_role? :ticket_viewer
can :create, Ticket
can :read, Ticket, :user_id => user.id
end
if user.employee # && any other necessary conditions
can :create, Ticket
can :read, Ticket, :employee_id => user.employee.id
end
end
# app/controllers/tickets_controller.rb
controller TicketsController < ApplicationController
load_and_authorize_resource
def index
# #tickets = Ticket.accessible_by(current_ability) # cancan's
# load_and_authorize resource will take care of loading ticket(s) for
# all controller actions, so I've commented them out
#tickets_grid = initialize_grid(#tickets, :include => [{:user => :user_type}, :employee_department, :state])
end
def show
# #ticket = Ticket.find(params[:id])
#reply = #ticket.replies.build # this for comments on ticket
#state = State.all # this for a model called State which describe the priority of the ticket (Emergency / High / Normal )
end
def new
# #ticket = Ticket.new
end
def create
# #ticket = Ticket.new(params[:ticket])
if #ticket.save
flash[:notice] = 'Support ticket request created.'
redirect_to #ticket
else
flash[:error] = 'An error occurred please try again!'
redirect_to '/dashboard'
end
end
def edit
# #ticket = Ticket.find(params[:id])
end
def update
# #ticket = Ticket.find(params[:id])
if #ticket.update_attributes(params[:ticket])
flash[:notice] = 'Successfuly updated.'
redirect_to tickets_path
else
flash[:error] = 'An error occurred please try again!'
render #ticket
end
end
end
This is fairly simple to achieve using CanCan. Here's a quick example using a modified subsection of the ability file you included:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
# Full access if you're the assigned employee
if user.has_role? :support_manager
can :manage, Ticket do |ticket|
ticket.try(employee) == user
end
end
# Read access only if you created the ticket
can :read, Ticket do |ticket|
ticket.try(user) == user
end
end
end
you will see all the tickets because in index action inside your controller you call:
#tickets = Ticket.all
you can try this:
#tickets = Ticket.accessible_by(current_ability)
by using this method current_user will see all tickets that current_user has access to
Update
you can define specific access in the accessible_method
#tickets = Ticket.accessible_by(current_ability, :manage)
the rest is how you define the access
https://github.com/ryanb/cancan/wiki/Fetching-Records
Example on define the access:
if user.has_role? :ticket_manager
can :manage, Ticket, employee: {:user_id => user.id}
end

Resources