simplified 'logical' models to cleanly access highly normalized database - ruby-on-rails

I know that one way to add/edit/delete (nested) records using a form is by using :accepts_nested_attributes_for: in the the corresponding models. However, when this nesting extends to about 4 levels (because of the normalization of the database), and I want to display all of these levels for editing on the website, this method seems to be rather cumbersome (and ugly).
I was wondering whether there is a way to define 'super' models with getter and setter methods that allow me to edit the necessary data in one place . As a simplified example, consider:
class Person < ActiveRecord::Base
attr_accessible :name, :age
has_one :address
end
class Address < ActiveRecord::Base
attr_accessible :street, :zip, :country
belongs_to :person
end
I would like to show/edit/update/etc the name, age, street, zip, country in one form. It's clear how to do this using accepts_nested_attributes_for. But I would like to have a class, say, PersonalInformation, that combines the fields name, age, street, zip, country from both classes by passing in the id from Person. I would then like to use this class as an interface for the website.

Something like a Form Object as described here:
http://robots.thoughtbot.com/activemodel-form-objects
I've been trying various implementations and haven't found the perfect solution but they do simplify "bringing a bunch of models together". The codeclimate blog also touches on it (item #3) at:
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models
The codeclimate post uses an older method of including the ActiveModel modules (no need to include them individually unless you want to now) but the concept is the same.

I would recommend the simple form gem. I modified an example from their docs to reflect your models that does exactly what you want:
Models:
class Person < ActiveRecord::Base
attr_accessible :name, :age
has_one :address
end
class Address < ActiveRecord::Base
attr_accessible :street, :zip, :country
belongs_to :person
end
View:
<%= simple_form_for #person do |f| %>
<%= f.input :name %>
<%= f.input :age %>
<%= f.association :street %>
<%= f.association :zip %>
<%= f.association :country %>
<%= f.button :submit %>
<% end %>

You could use virtual attributes on the Person model and put custom assignment logic within the getters/setters for each of the editable Address attributes.
class Person < ActiveRecord::Base
attr_accessible :name, :age
has_one :address
def street=(new_street)
# ...
end
end
This would likely end up being more complex in the long run.

Related

rails drop down menu association without db table ....

Respected ppl ...
i have these models :
class SanctionedPost < ActiveRecord::Base
attr_accessible :hospital_id, :sanctioned_posts, :designation_id
column :district_id
belongs_to:designation
belongs_to:hospital
belongs_to:district
belongs_to:division
end
class Hospital < ActiveRecord::Base
attr_accessible :beds, :fax_no, :hospital_name, :phone_no, :district_id, :institution_type_id, :location_id, :division_id, :block_id, :hospital_type_id, :IsAdministrativeLocation, :IsTribal, :latitude, :longitude
belongs_to:district
belongs_to:division
belongs_to:block
has_many:sanctioned_posts
end
class Division < ActiveRecord::Base
attr_accessible :division_name, :state_id
has_many:health_dept_locations
belongs_to:state
has_many:districts
has_many:hospitals
has_many:sanctioned_posts
validates_associated :districts
end
I want to be able to create dropdowns in my sanctioned_posts for such that divisions narrows down the districts which narrows down the blocks which narrows down to hospitals ...
(so that i dont have to select from a million hospitals ...) ...
I have tried everything from http://railscasts.com/episodes/88-dynamic-select-menus-revised and http://railscasts.com/episodes/193-tableless-model still to no avail ...
====================================
im using simple form ... and this allowed me to accomplish a similar task for the hospitals create form ...
<%= f.association :division,label_method: :division_name, value_method: :id, include_blank: false%>
<%= f.input :district_id do %>
<%= f.grouped_collection_select :district_id, Division.order(:division_name), :districts, :division_name, :id, :district_name, include_blank: true, label: 'District' %>
<% end %>
<%= f.input :block_id do %>
<%= f.grouped_collection_select :block_id, District.order(:district_name), :blocks, :district_name, :id, :block_name, include_blank: true, label: 'Block' %>
<% end %>
(+ the accompanying coffeescript... )
But in my current scenario im not able to do the same ...since im not storing division,district,block id in the sanctioned_posts model .... but i believe the hospital_id would be somewhat helpful in this context ....
Thnx very much in advance :) ...
Regards
Why not use CoffeeScript and JQuery to do REST requests on the index method and you can configure your routes accordingly?
Example: States have schools, schools have students
States have_many schools, schools have_many students
Select first state of Illinois. Illinois has ID of 11
get request to /states/11/schools.json
SchoolsController < ApplicationController
responds_to :json
def index
states = State.find(params[:id})
respond_with states.schools
end
end

Rails 3, many-to-many form using accepts_nested_attributes_for, how do I set up correctly?

I have a many-to-many relationship between Recipes and Ingredients. I am trying to build a form that allows me to add an ingredient to a recipe.
(Variants of this question have been asked repeatedly, I have spent hours on this, but am fundamentally confused by what accepts_nested_attributes_for does.)
Before you get scared by all the code below I hope you'll see it's really a basic question. Here are the non-scary details...
Errors
When I display a form to create a recipe, I am getting the error "uninitialized constant Recipe::IngredientsRecipe", pointing to a line in my form partial
18: <%= f.fields_for :ingredients do |i| %>
If I change this line to make "ingredients" singular
<%= f.fields_for :ingredient do |i| %>
then the form displays, but when I save I get a mass assignment error Can't mass-assign protected attributes: ingredient.
Models (in 3 files, named accordingly)
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredient_id
has_many :ingredients, :through => :ingredients_recipes
has_many :ingredients_recipes
accepts_nested_attributes_for :ingredients
accepts_nested_attributes_for :ingredients_recipes
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
has_many :ingredients_recipes
has_many :recipes, :through => :ingredients_recipes
accepts_nested_attributes_for :recipes
accepts_nested_attributes_for :ingredients_recipes
end
class IngredientsRecipes < ActiveRecord::Base
belongs_to :ingredient
belongs_to :recipe
attr_accessible :ingredient_id, :recipe_id
accepts_nested_attributes_for :recipes
accepts_nested_attributes_for :ingredients
end
Controllers
As RESTful resources generated by rails generate scaffold
And, because the plural of "recipe" is irregular, inflections.rb
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'recipe', 'recipes'
end
View (recipes/_form.html.erb)
<%= form_for(#recipe) do |f| %>
<div class="field">
<%= f.label :name, "Recipe" %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :ingredients do |i| %>
<div class="field">
<%= i.label :name, "Ingredient" %><br />
<%= i.collection_select :ingredient_id, Ingredient.all, :id, :name %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Environment
Rails 3.2.9
ruby 1.9.3
Some things tried
If I change the view f.fields_for :ingredient then the form loads (it finds Recipe::IngredientRecipe correctly, but then when I save, I get a mass-assignment error as noted above. Here's the log
Started POST "/recipes" for 127.0.0.1 at 2012-11-20 16:50:37 -0500
Processing by RecipesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/fMS6ua0atk7qcXwGy7NHQtuOnJqDzoW5P3uN9oHWT4=", "recipe"=>{"name"=>"Stewed Tomatoes", "ingredient"=>{"ingredient_id"=>"1"}}, "commit"=>"Create Recipe"}
Completed 500 Internal Server Error in 2ms
ActiveModel::MassAssignmentSecurity::Error (Can't mass-assign protected attributes: ingredient):
app/controllers/recipes_controller.rb:43:in `new'
app/controllers/recipes_controller.rb:43:in `create'
and the failing lines in the controller is simply
#recipe = Recipe.new(params[:recipe])
So the params being passed, including the nested attributes, are incorrect in some way. But I have tried lots of variants that fix-one-break-another. What am I failing to understand?
Thanks to clues from all, I have found what was wrong with my approach. Here's how I solved it.
I had originally tried with a simple HABTM many-to-many relationship, where the join table was named following standard Rails convention: ingredients_recipes. Then I realized that in a way, accepts_nested_attributes_for is designed for a 1-to-many relationship. So I converted to using has_many_through, creating a model IngredientsRecipes.
That name was the core problem, because Rails needs to be able to convert from plural to singular when using build to create form elements. This caused it to look for the non-existant class Recipe::IngredientsRecipe. When I changed my form so it used fields_for :ingredient the form displayed, but still failed to save with a mass assignment error. It even failed when I added :ingredients_attributes to attr_accessible. It still failed when I added #recipe.ingredients.build to RecipesController#new.
Changing the model to a singular form was the final key to resolve the problem. IngredientsRecipe would have worked, but I chose RecipeIngredients, as it makes more sense.
So to summarize:
can't use accepts_nested_attributes_for with has_and_belongs_to_many; need has_many with through option. (Thanks #kien_thanh)
adding accepts_nested_attributes_for creates a accessor that must be added to attr_accessible in the form <plural-foreign-model>_attributes, e.g. in Recipe I added attr_accessible :name, :ingredients_attributes (Thanks #beerlington)
before displaying the form in the new method of the controller, must call build on the foreign model after creating a new instance, as in 3.times { #recipe.ingredients.build }. This results in HTML having names like recipe[ingredients_attributes][0][name] (Thanks #bravenewweb)
join model must be singular, as with all models. (All me :-).
If you inspect the form that is generated, you'll notice that the nested fields have a name like "ingredients_attributes". The reason you're getting the mass-assignment error is because you need to add these fields to the attr_accessible declaration.
Something like this should fix it (you'll need to doublecheck the field names):
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
#...
end
Update: There's a similar answer here
Leave the call as
<%= f.fields_for :ingredients do |i| %>
But before that do
<% #recipe.ingredients.build %>
Im guessing that will allow your form to be created the right way, but there are likely other errors with your models, I can look # it more in detail when I have more time if its still not working, but:
As far as what accepts_nested_attributes_for does, when you pass in a correctly formatted params hash to the Model.new or Model.create or Model.update, it allows those attributes on the related model to be saved if they are in the params hash. In addition though, you do need to make the attributes accessible if they are unaccessible in the parent model as stated by beerlington.
I think you just need set up a one-to-many association, one recipe has many ingredients and one ingredient belongs to one recipe, so your model look like:
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
has_many :ingredients
accepts_nested_attributes_for :ingredients
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
belongs_to :recipe
end
You are built right form, so I don't write it again here. Now in your new and create controller will be like this:
def new
#recipe = Recipe.new
# This is create just one select field on form
#recipe.ingredients.build
# Create two select field on form
2.times { #recipe.ingredients.build }
# If you keep code above for new method, now you create 3 select field
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
...
else
...
end
end
How does params[:recipe] look like? If you just have one select field, maybe like this:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 } ] } }
If you have 2 ingredient select field:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 }, { id: 2 } ] } }

How do I avoid chains of method calls like #invoice.customer.address.street etc?

I have some models and have views code that look like the following
class Address < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :address
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :customer
end
This code shows a simple invoice structure, with a customer who has a single address.
The view code to display the address lines for the invoice would be as follows:
<%= #invoice.customer.name %>
<%= #invoice.customer.address.street %>
<%= #invoice.customer.address.city %>,
<%= #invoice.customer.address.state %>
<%= #invoice.customer.address.zip_code %>
above view code is not ideal. For proper encapsulation, the
invoice should not reach across the customer object to the street attribute of the
address object. Because if, for example, in the future your application were to change
so that a customer has both a billing address and a shipping address, every place in
your code that reached across these objects to retrieve the street would break and
would need to change. How can I avoid this problem ?
A simples solution would be the customer have a method that will return its main address, like:
class Customer < ActiveRecord::Base
def main_address
self.address
end
end
If you access its address only by this method, when it has more than one address it is necessary just change the main_address method to do whatever you want to.
Edit 1:
Another option would be use the delegate as suggested by #soundar
class Invoice < ActiveRecord::Base
belongs_to :customer
delegate :address, :to => :customer
end
You can use the delegate feature to reduce of sequence of function calls.
class Invoice < ActiveRecord::Base
belongs_to :customer
delegate :address, :to => :customer
end
<%= #invoice.customer.name %>
<%= #invoice.address.street %>
<%= #invoice.address.city %>,
<%= #invoice.address.state %>
<%= #invoice.address.zip_code %>
To avoid the problem just describe, it's important to follow the Law of Demeter, also known as the Principle of Least Knowledge.
To follow the Law of Demeter, you could rewrite the code above as follows:
class Address < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :address
has_many :invoices
def street
address.street
end
def city
address.city
end
def state
address.state
end
end
class Invoice < ActiveRecord::Base
belongs_to :customer
def customer_name
customer.name
end
def customer_street
customer.street
end
def customer_state
customer.state
end
end
And you could change the view code to the following:
<%= #invoice.customer_name %>
<%= #invoice.customer_street %>
<%= #invoice.customer_city %>
<%= #invoice.customer_state %>
In above code, public interface on Invoice has been polluted by methods that arguably have nothing to do with the rest of your interface for invoices. This is a general disadvantage of Law of Demeter, and it it not particularly specific to Ruby on Rails.
Now, The method is the class-level delegate method. This method provides a shortcut for indicating that one or more methods that will be created on your object are actually provided by a related object. Using this delegate method, you can rewrite your example like this:
class Address < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :address
has_many :invoices
delegate :street, :city, :state, :to => :address
end
class Invoice < ActiveRecord::Base
belongs_to :customer
delegate :name, :street, :city, :state, :to => :customer, :prefix => true
end
In this situation, you don't have to change your view code, the methods are exposed just as they were before:
<%= #invoice.customer_name %>
<%= #invoice.customer_street %>
<%= #invoice.customer_city %>
<%= #invoice.customer_state %>

Is there an easier way of creating/choosing related data with ActiveAdmin?

Imagine I have the following models:
class Translation < ActiveRecord::Base
has_many :localizations
end
class Localization < ActiveRecord::Base
belongs_to :translation
end
If I do this in ActiveAdmin:
ActiveAdmin.register Localization do
form do |f|
f.input :word
f.input :content
end
end
The association for word will only allow me to choose from existing words. However, I'd like to have the option of creating a new word on the fly. I thought it may be useful to accept nested attributes in the localization model ( but then, I will only have the option of creating a Word, not selecting from existing ones ). How can I solve this problem?
I think you can try using virtual attribute for this
Example(not tested)
class Localization < ActiveRecord::Base
attr_accessor :new_word #virtual attribute
attr_accessible :word_id, :content, :new_word
belongs_to :translation
before_save do
unless #new_word.blank?
self.word = Word.create({:name => #new_word})
end
end
end
The main idea is to create and store new Word instance before saving localization and use it instead of word_id from drop-down.
ActiveAdmin.register Localization do
form do |f|
f.input :word
f.input :content
f.input :new_word, :as => :string
end
end
There is great rails-cast about virtual attributes http://railscasts.com/episodes/167-more-on-virtual-attributes

Using class variable

Beginner here, learning ruby on rails by jumping into a project. This question is probably pure ruby and has nothing to do with rails. I also want to note that I'm using Active_Admin.
I have the following 2 classes: Owner and Phone.
class Owner < ActiveRecord::Base
attr_accessible :email, :password,
has_many :phones
end
class Phone < ActiveRecord::Base
attr_accessible :owner_id :model
belongs_to :owner
end
How would I go accessing the owners email from within the phones model, for example:
form do |f|
f.inputs "Phone Details" do
f.input :model
f.input :owner_id # This is where I want the email, not the owners id.
end
f.buttons
end
It looks like I should review the pickaxe book and refine my ruby before jumping into rails.
Thanks
to have both the owner and the phone in one form, your form should look something like this:
form_for #phone do |phone_form|
phone_form.label :model
phone_form.text_field :model
fields_for #phone.owner do |owner_fields|
owner_fields.label :email
owner_fields.text_field :email
If you use this method, make sure you can update the Owner from the Phone model by setting accepts_nested_attributes_for :owner on you Phone model.
In this case you should use nested form. Usage of nested form is explained very well in this railscast.

Resources