Restrict access to "destroy" feature in Ruby on Rails app - ruby-on-rails

Thanks in advance for your help! I have a site built in Ruby on Rails (2.3.15, 1.8.7) that lets people create itineraries to different locations and with different activities at each location. The site has a way of ensuring that the current user can only delete the itineraries associated with his account. The code in itineraries_controller.rb to do this looks something like this:
def destroy
#itinerary = Itinerary.find(params[:id])
if #itinerary.user_id == current_user.id
#itinerary.destroy
respond_to do |format|
format.html { redirect_to('/home') }
format.xml { head :ok }
end
else
redirect_to '/'
end
end
This system works well. I want to do the same thing for the activities, however. In the activities_controller, I want to ensure that the current user can only delete the activities associated with his account.
Currently, here are my four models (with unnecessary stuff stripped out):
User.rb
class User < ActiveRecord::Base
has_many :itineraries
end
Itinerary.rb
class Itinerary < ActiveRecord::Base
has_many :locations
belongs_to :user
end
Location.rb
class Location < ActiveRecord::Base
belongs_to :itinerary
has_many :activities
end
Activity.rb
class Activity < ActiveRecord::Base
belongs_to :location
end
Here is where I'm starting with activities_controller.rb. FYI, my current_user.id is created by an authenticated_system.rb, so don't worry about that part.
def destroy
#activity = Activity.find(params[:id])
if #itinerary.user_id == current_user.id
#activity.destroy
respond_to do |format|
format.html { redirect_to("/") }
format.xml { head :ok }
end
else
redirect_to '/'
end
end
I tried to connect the models using a has_many :through approach, but I didn't know how to do it for more than three models, and I wasn't sure that was the right approach anyway. Thanks again for any help you can provide!

You can simply do
if #activity.location.itinerary.user_id == current_user.id

You really only need to ask the activity for its location, then ask the location for its itinerary, than ask it for its user. It's a bit of a violation of the Law of Demeter, but it will work.
loc = #activity.location
itin = loc.itinerary
if itin.user_id = current_user.id
...
That should work.

Related

Rails - How to create multiple "matches" in one action between a user and opportunities?

I'm looking for the easiest and the most clever way to create interest_id(match) in one-click.
Here is my MVC :
user.rb
class User < ApplicationRecord
has_many :interests, through: :opportunities
end
interest.rb
class Interest < ApplicationRecord
belongs_to :opportunity
belongs_to :user
end
opportunity.rb
class Opportunity < ApplicationRecord
has_many :interests
end
InterestsController.rb
def create
#user = current_user
#opportunities = Opportunity.all
#interest = Interest.new(interest_params)
if #interest.save!
redirect_to user_interests_path, notice: 'it works'
else
render :new, notice:"it doesn't work"
end
end
def interest_params
params.permit(
:user_id,
:opportunity_id)
end
user/show
<%= link_to "Match", user_interests_path(#user), class:"btn btn-primary", :method => :post %>
For now, I can't pass opportunities (nil). Could you please advise me about the easiest way to create interests? (New on RoR for 6 months).
Many thanks for your help.
If I understand correctly your relation schema, the Interest is the join record associating a User to (eventually) many Opportunity, and vice-versa (many-to-many relationship).
With that being said (and please correct me if I am wrong), you can do the following to achieve what you want:
# in user/show
<% #opportunities.each do |opportunity| %>
<%=
link_to "Match opportunity #{opportunity.id}",
user_interests_path(#user, opportunity_id: opportunity.id),
class: "btn btn-primary",
method: :post
%>
<% end %>
# in interests_controller
def create
if current_user.interests.create(opportunity_id: opportunity_id_param)
redirect_to user_interests_path, notice: 'it works'
else
render :new, notice: "it doesn't work"
end
end
private
def opportunity_id_param
params.require(:opportunity_id)
end
This suggested code:
requires the opportunity_id param for the interests#create action
use current_user to automatically set the user_id on the Interest model, so the end-users can't send a user_id that are not theirs (if they could, then each user could create interest for other users without their agreement... security flaw)
On a side note, I strongly advise you to not select all existing Opportunity record and display it on your page: it does not scale. Someday, you will end up with hundreds of Opportunity records, making this list too big from a User Experience perspective.
I suggest a smarter approach, for example some kind of ordering + limit: max of 10 records ordered by "most interest", which can be accomplished by the following:
# in controller
#popular_opportunities = Opportunity
.joins('LEFT JOINS interests ON interests.opportunity_id = opportunities.id')
.order('count(interests.*) DESC, opportunities.id')
.limit(10)
And then in the view, simply use #populator_opportunities instead of #opportunities.
Other options, like pagination, are also efficient in this case but IMO relevant ordering is the minimum.
First, you need to pass the ids of the opportunities you want to create interest some way, the best is a form, with checkboxes like MrShemek said, or a multi select dropdown.
I think you probably made some mistakes in User and Opportunity with the has_many and belong_to part:
class User < ApplicationRecord
has_many :interests
has_many :opportunities, through: :interests
# interest is the one that links user and opportunity, it has the references for both user and opportunities
end
class Opportunity < ApplicationRecord
has_many :interests
has_many :users, through: :interests
end
then in controller you could do
def create
#user = current_user
#opportunities = Opportunity.all
#user.opportunity_ids = interest_params[:opportunity_ids] # it will create the interrests automatically for the given ids (because the relations of has_many through)
if #user.save!
redirect_to user_interests_path, notice: 'it works'
else
render :new, notice:"it doesn't work"
end
end

How to update has_many :through table?

This is a follow up question from one of my earlier questions: How to Model doctor and patient relation
I am fairly new to rails, I am making an appointment booking system between patient and doctor, I have made the relationships, set up authentication using devise. I have 3 models:
class Doctor < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Patient < ApplicationRecord
has_many :appointments
has_many :doctors, through:appointments
end
class Appointment < ApplicationRecord
#table_columns: id | start_time| end_time| doctor_id| patient_id|slot_taken|
belongs_to :patient
belongs_to :doctor
end
I have created the appointments controller: Some actions are below:
#current_doctor comes from devise. Have created two separate models for doctor and patients using devise
def create
#appointment = current_doctor.appointments.build(appointment_params)
respond_to do |format|
if #appointment.save
format.html { redirect_to appointments_path, notice: 'Appointment was successfully created.' }
else
format.html { render :new }
end
end
end
def update
respond_to do |format|
if #appointment.update(appointment_params)
format.html { redirect_to appointments_path, notice: 'Appointment was successfully updated.' }
else
format.html { render :edit }
end
end
end
private
def appointment_params
params.require(:meeting).permit(start_time, end_time)
end
As you can see, the doctor can create the time slots of when he is available, and can edit,update and destroy the same for him/herself and when he creates one the appointment table gets updated with doctor_id, start_time and end_time.
Now, what should I do to add the patient_id and change slot_taken to true (default: false) to it? When the patient books the available slot, the appointments table should be updated with patient_id and slot_taken value. How should I update the appointments table and where should I put the code for the same?
First of all, some advice: I don't think two user models it's a good idea. Think it would be best have one User devise model, and that user have an Role associated for it, so you control the access type by that. If you have to add more type of users to that system, creating one model for each one can be troubling.
About the appointments, question, in a roughly way, if you have an doctor instance, and you wanna make an appointment for some patient at s start_time and e end_time, you can do something like:
doctor.appointments.find_by(start_time: s, end_time: e).update(patient_id: your_patient_id)
About where to put that, depends on how your system works. Is the client that assigns himself? Or some administrator does that?
Anyway, think you should have another action/view for doing that assignment, so you can have a form where the user can input that information, and when he sends the form, you handle it and do the update in some way like a showed before.
Hope this helps, good luck

Creation of data in a :through relation

I'll ask my question first:
Will this code logically work and it is the right thing to do (best practices perspective)? First off, it looks strange having a user being passed to a static subscription method
User and magazine have a many to many relationship through subscriptions (defined below). Also you can see, I've used through joins instead of the has and belongs to many so that we can define a subscription model.
after creating a user they need to have default subscriptions. Following the single responsibility principle, I don't think a user should have to know what default magazines to subscribe to. So how, after a user has been created can I create default subscriptions. The user.likes_sports? user.likes_music? should define which subscriptions methods we want.
Am I on the right track? I don't have anyone to review my code, any code suggestions highly appreciated.
class User < ActiveRecord::Base
after_create create_default_subscriptions
has_many :magazines, :through => :subscriptions
has_many :subscriptions
def create_default_subscriptions
if self.likes_sports?
Subscription.create_sports_subscription(self)
end
end
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :magazine
#status field defined in migration
def self.create_sports_subscription(user)
Magazine.where("category = 'sports'").find_each do |magazine|
user.subscriptions << Subscription.create(:user => user, :magazine => magazine, :status=>"not delivered")
end
end
.
.
end
class Magazine < ActiveRecord::Base
has_many :users, :through => :subscriptions
has_many :subscriptions
end
The code is too coupled in my view. This can get out of hand really easily.
The right way to do this in my view would be to create a new service/form that takes care of creating the user for you
class UserCreationService
def perform
begin
create_user
# we should change this to only rescue exceptions like: ActiveRecord::RecordInvalid or so.
rescue => e
false
end
end
private
def create_user
user = nil
# wrapping all in a transaction makes the code faster
# if any of the steps fail, the whole user creation will fail
User.transaction do
user = User.create
create_subscriptions!(user)
end
user
end
def create_subscriptions!(user)
# your logic here
end
end
Then call the code in your controller like so:
def create
#user = UserCreationService.new.perform
if #user
redirect_to root_path, notice: "success"
else
redirect_to root_path, notice: "erererererooooor"
end
end

Changing Data from an other model

I am wondering what kind of query should I accept to allow my data to be updated. My models consists of client, interest, and a manager
Clients his has follow
id
name
email
password
Interest
id
description
manager
customer_id
interest_id
created_at
The goal of the manager his not to override old data in interest but just keep adding a new interest and refering to it.
The relationship his has follow
class Client < ActiveRecord::Base
has_many :music_interest_managers
has_many :music_interests, through => :music_interest_managers
end
class MusicInterest < ActiveRecord::Base
has_many :music_interest_managers
has_many :clients, through => :music_interest_managers
end
class MusicInterestManager < ActiveRecord::Base
belongs_to :music_interests
belongs_to :client
end
Now to update the data from the customer controller i am not sure how would i do this
This is what i am thinking about:
#client = Client.find(params[:id])
#manager = #client.manager.build(params[:manager])
#interest = #interest.manager.build(params[:interest])
Does this make sense? or i am dead wrong?
Update:
def update
#client = Client.find(params[:id])
#interest = #client.music_interests.build(params[:interest])
if #client.update_attributes(params[:client])
flash[:success] = "Profile updated"
#sign_in #client
redirect_to #client
else
render 'edit'
end
end
Or should i render a model view from interest to then apply the change?
#client = Client.find(params[:id])
#interest = #client.music_interests.build(params[:interest])
should work - try it out in the console!

How to associate two foreign key to a post on create

I have 3 models as below and would like to associate two foreign key to a post on create (user_id (from User model) and review_id (from Review model) to Goal model. I manage to associate the user_id using 'current_user' to goals on create by using the solution given in the link below but not sure on how to go about getting this done as well for review_id.
Thanks.
Devise how to associate current user to post?
My models:
class User < ActiveRecord::Base
has_many :reviews
has_many :periods, :through => :reviews
has_many :goals
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :period
has_many :goals
end
class Goal < ActiveRecord::Base
belongs_to :user
belongs_to :review
end
My goals_controller.rb
def create
#goal = current_user.goals.build(params[:goal])
respond_to do |format|
if #goal.save
format.html { redirect_to #goal, notice: 'Goal was successfully created.' }
format.json { render json: #goal, status: :created, location: #goal }
else
format.html { render action: "new" }
format.json { render json: #goal.errors, status: :unprocessable_entity }
end
end
end
Cheers,
Azren
While I don't understand what the goal is. One general way I usually use is to model the relationships to something you are familiar with or you can easily find good examples. For example, think about your use case as user-question-answer models. Same as your use case, user has many questions and many answers. question has many answers and belongs to user, and answer belongs to user and question.
So things become easy, right? Let's see how stackoverflow implements this. You can check the html code of a comment box, and the form action is something like (/questions/10186415/answer/submit). Here 10186415 is the question id, it's passed to the server side, so when an answer is created, this question id can be used and related.
Back to your case, the goal form should know what review it's for. The review id could be a hidden field, or part of the submit url.

Resources