STI with a Single Controller - ruby-on-rails

I have several models (User, Goal) and my Goal model has several subtypes (Exercise, Yoga, etc.)
A user can have many goals, but only one of each type.
class User < ActiveRecord::Base
has_many :goals, dependent: :destroy
has_one :exercise
has_one :water
has_one :yoga
def set_default_role
self.role ||= :user
end
end
and
class Goal < ActiveRecord::Base
self.inheritance_column = :description
belongs_to :user
validates :user_id, presence: true
validates :description, presence: true
end
where a subclass of goal is just something like this
class Exercise < Goal
belongs_to :user
end
I want to create all types of goals in my goal controller and have it set up so that localhostL:3000/waters/new will be my "water goal-type" creation page. I'm having trouble correctly setting it up though so that description is automatically set (my STI column) because I also want to build it through my user (so user_id is also set).
my goals controller currently looks like this...
def create
#goal = current_user.goals.build
???? code to set goal.description = subtype that the request is coming from
respond_to do |format|
if #goal.save
format.html { redirect_to #goal, notice: 'Goal was successfully created.' }
format.json { render action: 'show', status: :created, location: #goal }
else
format.html { render action: 'new' }
format.json { render json: #goal.errors, status: :unprocessable_entity }
end
end
end
I'm pretty new to rails so a little bit confused. Also using Rails 4.0

In that case you'd need to communicate the type to create to the controller somehow.
This is most easily established by including a hidden field in the form which has a value of the type you want to create.
Assuming your block variable for the form is f
<%= f.hidden_field :type, :value => "Excercise" %>
Then you can build a goal like this:
current_user.goals.build(type: params[:type])
You can easily see how this can be very dangerous, you should always be doubly careful when using user submitted data.
To guard against a malicious user, you could set a constant in your controller
AcceptedModels = ["Excercise", "Yoga", "Water"]
def accepted_type
(AcceptedModels & params[:type]).first
end
If you also want to hide your internal structure, you could use a hash and send any identifier
AcceptedModels = {"0": "Excercise", "1": "Yoga", "2": "Water"}
def accepted_type
AcceptedModels.fetch(params[:type], nil)
end
in either case you can then build your goal like this:
current_user.goals.build(type: accepted_type)
The biggest downside with this is that you will have to keep the AcceptedModels updated whenever you add/remove more goals

Related

update_attributes & update do not call validations on parent association

So i have the following problem in my codebase.
In database there is data that doesn't satisfy the validations. (its not right I know, and will be fixed but the question remains why rails works like it does)
Calling save in create action will run validations on the parent object and wont save anything, however, calling the update or update_attributes in update action will not and the object will be saved with invalid parent.
Model:
class RussianDoll < ActiveRecord::Base
validates :name, :description, presence: true
has_and_belongs_to_many :parents,
class_name: 'RussianDoll',
join_table: :russian_dolls_russian_dolls,
foreign_key: :child_id,
association_foreign_key: :parent_id
has_and_belongs_to_many :children,
class_name: 'RussianDoll',
join_table: :russian_dolls_russian_dolls,
foreign_key: :parent_id,
association_foreign_key: :child_id
end
Controller:
class RussianDollsController < ApplicationController
def create
rd_params = russian_doll_params
rd_params[:parents] = RussianDoll.where(id: params[:parent_ids])
#russian_doll = RussianDoll.new(rd_params)
respond_to do |format|
if #russian_doll.save
format.html { render 'foo' }
format.xml { render 'bar' }
else
format.html { render action: 'new' }
format.xml { render xml: #russian_doll.errors }
end
end
end
def update
#russian_doll = RussianDoll.find(params[:id])
rd_params = russian_doll_params
rd_params[:parents] = RussianDoll.where(id: params[:parent_ids])
respond_to do |format|
if #russian_doll.update(rd_params)
format.html { render 'foo' }
format.xml { render 'bar' }
else
format.html { render action: 'edit' }
format.xml { render xml: #russian_doll.errors }
end
end
end
private
def russian_doll_params
params.require(:russian_doll).permit(:name, :description)
end
end
When you try to create a new RussianDoll (with name AND description) it wont save if the RussianDoll parent you are trying to associate it with doesnt have description set in database.
However if you create it without any parents, and then try to add a parent through the update action, then you will be able to set association to a RussianDoll entry that doesnt have description set (invalid entry).
As far as I know update should run all validations, same as the save action does but it doesn't seem to do that in this case.
Why do save validations work, but update ones don't?
Any help is appreciated!
[EDIT]
So I finally managed to find the root cause...
In create action we first create the #russian_doll with already set invalid parent in its association, so if we run .valid? on the object it will fail.
However in the update action, our #russian_doll is being first found in database, and before the .update(rd_params) object itself is not being changed, so running .valid? will return true, and update will manage to connect edited russian_doll with invalid parent.
What still bugs me, is that second time the update is called, #russian_doll will have invalid parent from a first update, but for some reason .valid? is still returning true, instead of failing.
It is like validations work for all nested entries only on object creation...

Trouble creating a new record in Rails with Select Boxes and Foreign Keys

Ruby on Rails newbie here.
TL;DR: When trying to create a new record, I get an error message saying my foreign keys must exist. I'm using Select Boxes on my view. Need help.
I am creating a requests management system for our own support staff here in our company. So far, I have generated a scaffold for Issues and 2 models, one for Category and another for Subcategory.
So far, the relationship I came up with is designed like this:
Issue belongs to Category and Subcategory
Subcategory belongs to Category.
This means that a user may have a problem with his keyboard, for which he will create an Issue. This issues would belong the the subcategory named "Peripherals", which would in turn, belongs to a broader "Hardware" category.
I still need to implement Ajax to fetch data for my Select Boxes, but while testing it out in a simpler scenario, I couldn't create my Issues from the "New Issue" view. I come across an error message saying that Category and Subcategory must exist. I've reviewed what I've written so far, but couldn't find my mistake.
Here is the code for my create method in the Issues controller
def create
#issue = Issue.new(issue_params)
respond_to do |format|
if #issue.save
format.html { redirect_to root_path, notice: 'Issue was successfully created.' }
else
#categories = Category.enabled
#subcategories = Subcategory.enabled
format.html { render :new }
end
end
end
Here go my Models
Issue Model:
class Issue < ApplicationRecord
belongs_to :category
belongs_to :subcategory
end
Category Model:
class Category < ApplicationRecord
has_many :subcategories
has_many :issues
enum status: { disabled: 0, enabled: 1 }
after_initialize :set_defaults
def self.enabled
where(status: 'enabled')
end
def set_defaults
self.status ||= 1
end
end
Subcategory Model
class Subcategory < ApplicationRecord
belongs_to :category
has_many :issues
enum status: { disabled: 0, enabled: 1 }
def self.enabled
where(status: 'enabled')
end
after_initialize :set_defaults
def set_defaults
self.status ||= 1
end
end
And finally, here are the parameters passed to the controller:
Processing by IssuesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"tzKDayYfEbEwTaOFup/N9kQ+8tr9c0P5otV2B0boKGrgyv+HkQaEvYJ6ZMQeR+8XgCnhJR6PosVcx0jPJpqBEA==", "category_id"=>"1", "subcategory_id"=>"1", "issue"=>{"description"=>"Replace broken keyboard.", "status"=>"1", "criticality_level"=>"1"}, "commit"=>"Create Issue"}
I was able to create an Issue via Rails Console, though.
Could anyone give me a hint on how to solve this? Thanks guys!
modify create action as follows
def create
#category = Category.find(params[:category_id])
#issue = #category.issues.build(issue_params)
respond_to do |format|
if #issue.save
format.html { redirect_to root_path, notice: 'Issue was successfully created.' }
else
#categories = Category.enabled
#subcategories = Subcategory.enabled
format.html { render :new }
end
end
end

Rails custom validate - uniqueness of a property across models

I'm stuck at defining a custom validation method that's purpose is to verify uniqueness of a property across two models
I realize this is bad code, but i wanted to get the test passing before refactor
here is a model with the custom validation to check another model property, error undefined local variable or method `params' (be gentle I'm still trying to figure out RoR)
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models
def uniqueness_of_a_slug_across_models
#sprocket = Sprocket.where(slug: params[:widget_slug]).first
if #sprocket.present?
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
end
end
You don't have access to params in a model. It belongs to controller and view. What you could do is to call custom method in widgets controller (instead of regular save) in order to pass params to a model:
class WidgetsController < ActionController::Base
def create
#widget = Widget.new(widget_params)
if #widget.save_with_slug_validation(params)
redirect_to widgets_path
else
render :new
end
end
end
and define it:
class Widget < ActiveRecord::Base
# ...
def save_with_slug_validation(params)
sprocket = Sprocket.find_by(slug: params[:widget_slug])
if sprocket
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
save
end
end
I didn't test it but it should work.
P.S. Rails 4 style is used.
UPD
I should have tested it, sorry. Please use another approach.
Widgets controller:
# POST /widgets
# POST /widgets.json
def create
#widget = widget.new(widget_params)
#widget.has_sprocket! if Sprocket.find_by(slug: params[:widget_slug])
respond_to do |format|
if #widget.save
format.html { redirect_to [:admin, #widget], notice: 'widget was successfully created.' }
format.json { render action: 'show', status: :created, location: #widget }
else
format.html { render action: 'new' }
format.json { render json: #widget.errors, status: :unprocessable_entity }
end
end
end
Widget model:
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models, if: 'has_sprocket?'
def uniqueness_of_a_slug_across_models
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
def has_sprocket!
#has_sprocket = true
end
def has_sprocket?
!!#has_sprocket
end
end
It would be better to move has_sprocket! and has_sprocket? methods and maybe validation itself to Slugable concern.

Rails object does not return has_many subclass object

I've got below setup in my Ticket.rb model
has_many :note
And in note.rb model
belongs_to :ticket
But I cannot seem to get note id or description using below code:
def show
#ticket = Ticket.find(params[:id])
#note_id = #ticket.note.id
respond_to do |format|
format.html
format.json { render json: #ticket }
end
end
It doesn't seem to detect note class at all when I call #ticket.node
I also have note table populated with a row that has ticket_id aswell
What could be wrong?
Ticket.note is a has many. It behaves like an array.
You either need to change it to has_one :note, or use #ticket.note.first.id. If you're not sure if the note exists for a given ticket, you could try #ticket.note.first.try(:id).

Subform for parent object

So I've been holding off putting a question on here because I don't want to bother the community with stupid questions, but I'm going to ask for help now anyway.
I'm quite new to Ruby on Rails, and as you've probably read from the title, I'm having trouble with my subform. More specifically, with assigning the parent object to a client object. I'm building a system for my work in where employees can register repairs (mobile phones) and keep track of them. I'm building the client object with #repair = Repair.new, which works fine, but when I try to set the Client with #repair = Client.new, the :client_id on the repair stays null.
Here's my repair.rb: (some fields are in Dutch, please ignore that)
class Repair < ActiveRecord::Base
attr_accessible :imei, :klantnaam, :telefoon, :intake, :branch_id, :id, :client_id
attr_accessible :merk, :type, :batterij, :lader, :headset, :batterijklep, :carkit, :schade_toestel, :schade_scherm, :bon, :datum_bon, :klacht, :prijsindicatie
belongs_to :branch
belongs_to :client
accepts_nested_attributes_for :client
end
client.rb:
class Client < ActiveRecord::Base
attr_accessible :email, :firstname, :lastname, :number, :phone, :postalcode
has_many :repairs
end
repairs_controller.rb: (I've left the irrelevant methods out, I was getting tired of the 4 spaces :P)
class RepairsController < ApplicationController
# GET /repairs/new
# GET /repairs/new.json
def new
#repair = Repair.new
#repair.client = Client.new
if request.remote_ip == "xx.xx.xx.xx"
#repair.branch = Branch.where(:name => "Xxxxxxx").first
end
#repair.intake = Time.now
respond_to do |format|
format.html # new.html.erb
format.json { render json: #repair }
end
end
# POST /repairs
# POST /repairs.json
def create
#repair = Repair.new(params[:repair])
respond_to do |format|
if #repair.save
format.html { redirect_to #repair, notice: 'Repair was successfully created.' }
format.json { render json: #repair, status: :created, location: #repair }
else
format.html { render action: "new" }
format.json { render json: #repair.errors, status: :unprocessable_entity }
end
end
end
end
And this is the JSON I get from /repair/new.json:
{"batterij":null,"batterijklep":null,"bon":null,"branch_id":null,"carkit":null,"client_id":null,"created_at":null,"datum_bon":null,"headset":null,"id":null,"imei":null,"intake":"2013-02-01T23:29:10Z","klacht":null,"klantnaam":null,"lader":null,"merk":null,"pickup":null,"prijsindicatie":null,"schade_scherm":null,"schade_toestel":null,"telefoon":null,"updated_at":null}
By the way, the branch assignment works flawlessly... (It's null now because I'm not on the IP I specified in the new method)
Please help me out... :-(
Robin
Solved it!!
The code above all works flawlessly, the problem was a <% instead of <%= in my view, which made my subform not show up. Duhh.

Resources