I have a Post Model, and a comment Model. I would like to limit the comments to 3. How would you do this?
Would it be best to create a validation? If so what would this look
like?
Would you do this in the view unless Post.comments == 3?
Would a callback make sense?
the post's comments count validation is the "Comment" model responsibility, so I'd like to suggest the following code:
class Comment < ActiveRecord::Base
belongs_to :post
before_create :limit_number
private
def limit_number
if post.comments.count >= 3
errors.add(:post, "the limit of comments is over")
end
end
end
class Post < ActiveRecord::Base
has_many :comments
end
You should always validate at the model level as well as the views.
class Comment < ActiveRecord::Base
belongs_to :post
validate :check
private
def check
if post.present?
errors.add("Post", "can not have more than 3 comments") if post.comments.size >= 3
end
end
end
class Post < ActiveRecord::Base
# other implementation...
def commentable?
comments.size <= 3
end
end
Then just call #commentable? in your views like this. You should never hard-code values in the views.
<% if #post.commentable? %>
<%= render "form" %>
<% end %>
I would make sure not to let them submit that fourth comment that you don't want to allow. Some might say you should do the check in your controller and pass a flag, but for something this simple the check in the view seems fine.
Validate it in the models. You could use validates_with as described here.
In the view, you'd be better off checking with an inequality like
unless Post.comments.length >= 3
show_form
end
That way if you have four comments for some reason (race condition or an admin posts a response after 3, etc.) the form stils won't show up.
Related
TL;DR
What is the best way to create join table entries based on a form with the attributes of a association, like a bar code or a plate number?
Detailed explanation
In this system that records movements of items between storage places, there is a has_many_and_belongs_to_many relationship between storage_movements and storage_items because items can be moved multiple times and multiple items can be moved at once.
These items are previously created and are identified by a plate number that is physically attached to the item and recorded on its creation on the application.
The problem is that I need to create storage_movements with a form where the user inputs only the plate number of the storage_item that is being moved but I cant figure it out a way to easily do this.
I have been hitting my head against this wall for some time and the only solution that I can think of is creating nested fields on the new storage_movements form for the storage_items and use specific code on the model to create, update and delete these storage_movements by explicitly querying these plate numbers and manipulating the join table entries for these actions.
Is this the correct way of handling the problem? The main issue with this solution is that I can't seem to display validation errors on the specific plates number that are wrong (I'm using simple_forms) because I don't have storage_item objects to add errors.
Below there is a snipped of the code for the form that I'm currently using. Any help is welcome :D
# views/storage_movements/_form.html.erb
<%= simple_form_for #storage_movement do |movement_form| %>
#Other form inputs
<%= movement_form.simple_fields_for :storage_items do |item_form| %>
<%= item_form.input :plate, label: "Plate number" %>
<% end %>
<% end %>
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many_and_belongs_to_many :storage_items, inverse_of: :storage_movements, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
... several callbacks and validations ...
end
# models/storage_item.rb
class StorageItem < ActiveRecord::Base
has_many_and_belongs_to_many :storage_movements, inverse_of: :storage_items
... more callbacks and validations ...
end
The controllers were the default generated ones.
This was my solution, it really "feels" wrong and the validations also are not shown like I want it to... But it was what I could come up with... Hopefully it helps someone.
I created the create_from_plates and update_from_plates methods on the model to handle the create and update and updated the actions of the controller to use them.
Note: had to switch to a has_many through association due to callback necessities.
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many :movements_items, dependent: :destroy, inverse_of: :storage_movement
has_many :storage_items, through: :movements_items, inverse_of: :allocations, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
validate :check_plates
def StorageMovement::create_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
unless items_attributes.nil?
item_plates = items_attributes.collect {|k, h| h["plate"]}
items = StorageItem.where, plate: item_plates
end
if not items_attributes.nil? and item_plates.length == items.count
new_allocation = Allocation.new attributes
movements_items.each {|i| new_allocation.items << i}
return new_allocation
else
Allocation.new mov_attributes
end
end
def update_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
if items_attributes.nil?
self.update mov_attributes
else
transaction do
unless items_attributes.nil?
items_attributes.each do |k, item_attributes|
item = StorageItem.find_by_plate(item_attributes["plate"])
if item.nil?
self.errors.add :base, "The plate #{item_attributes["plate"]} was not found"
raise ActiveRecord::Rollback
elsif item_attributes["_destroy"] == "1" or item_attributes["_destroy"] == "true"
self.movements_items.destroy item
elsif not self.items.include? item
self.movements_items << item
end
end
end
self.update attributes
end
end
end
def check_plates
movements_items.each do |i|
i.errors.add :plate, "Plate not found" if StorageItem.find_by_plate(i.plate).nil?
end
end
... other validations and callbacks ...
end
With this, the create works as I wanted, because, in case of a error, the validation adds the error to the specific item attribute. But the update does not because it has to add the error to the base of the movement, since there is no item.
Ok guys, these is my first attempt to code without any tutorials or examples, so may be I'm doing something terribly stupid.
I have a Rating modes, which parent to Party, which parent to Vote. I did't generated scaffold for vote, just model, as I suppose generally I don't need to have all standart controller or view here. I try to add simple voting system (I know about gems, but for the sake of practice wanna do it by myself)
So my Party model:
class Party < ActiveRecord::Base
belongs_to :rating
has_one :vote
end
end
Vote model:
class Vote < ActiveRecord::Base
attr_accessible :negative, :positive, :party_id
belongs_to :party
#def self.build(party_id)
#return Vote.new(:party_id=>party_id)
#end
end
Parties_controller:
def create
#rating = current_rating
#party = #rating.parties.build(:rating_id => #rating_id)
##vote = Vote.add_voting(#party.id)
#vote = #party.Vote.build(:party_id=>#party.id)
commented part is one of tries to do pretty same thing.
And if in view I ask for class:
<% #rating.parties.each do |item| %>
<p><%= item.name %></p>
<p><%= item.vote.class %></p>
<% end %>
It shows 'nilClass'
Why?
This syntax is invalid:
#party.Vote.build(:party_id => #party.id)
Use this instead:
#vote = #party.build_vote
There is no need to assign party_id. The build_vote method does this for you.
See explanation on Rails Guides.
#positive_votes = Vote.find_by_positive(params[:user_uid, :party_id])
find_by_positive expects to search the "positive" column in your db, but you are passing in a user_id and party_id. Try this:
#positive_votes = Vote.find_by_user_uid_and_party_id(params[:user_uid], params[:party_id])
I have two models, Page and PageContent.
class Page < ActiveRecord::Base
has_many :page_contents
end
class PageContent < ActiveRecord::Base
belongs_to :page
end
Page has a :css_design attribute, but I want to be able to edit that attribute from my PageContent form. Any ideas?
I see a lot of accepts_nested_attributes and fields_for advice, but they don't work, because they all seem to be for forms that are either A. Creating entire new instances of a different model from a form (for example, creating tasks from a project form), or B. Updating associated records thru the parent. I want to do the opposite- I want to update the parent's record thru associated records.
Any help is greatly appreciated! Many thanks in advance!
--Mark
UPDATE
I have added the following to my PageContent model:
def css_design
page ? page.css_design : nil
end
def css_design= (val)
if page
page.update_attribute 'css_design', val
else
#css_design = val
end
end
after_create :set_page_css_design_on_create
def set_page_css_design_on_create
self.css_design = #css_design if page && #css_design
end
And I have, in both my create and update actions:
#page_content.update_attributes params[:page_content]
But I'm getting:
NoMethodError (undefined method `css_design=' for #<Page:0x00000003a80338>):
app/models/page_content.rb:13:in `css_design='
app/controllers/page_contents_controller.rb:56:in `new'
app/controllers/page_contents_controller.rb:56:in `create'
When I go to create the page_content for the first time. I copied and pasted this stuff straight from my files, so if you see anything weird, please let me know!
Update your model with the following:
class PageContent < ActiveRecord::Base
belongs_to :page
def css_design
page ? page.css_design : nil
end
def css_design= (val)
if page
page.update_attribute 'css_design', val
else
#css_design = val
end
end
after_create :set_page_css_design_on_create
def set_page_css_design_on_create
self.css_design = #css_design if page && #css_design
end
end
Your form can now display and update the current Page's value, even though it looks like it's only handling PageContent:
<%= form_for #page_content do |f| %>
...
<%= f.text_field :css_design %>
...
<% end %>
In your controller, e.g.:
def update
...
#page_content.update_attributes params[:page_content]
...
end
If it's not too many fields, you can add attributes to the PageContent model via attr_accessor. Then update the parent after_save. Something like this:
class PageContent < ActiveRecord::Base
belongs_to :page
attr_accessor :css_design
after_save :update_parent_css
private
def update_parent_css
self.page.update_attribute(:css_design, self.css_design)
end
end
Then you can put a form field for it just like any other PageContent field. If it's more than one field, use update_attributes. Also, you probably want to make sure the attribute is set (using attribute_present?(attribute)) so it doesn't overwrite it with nil.
(Sorry I don't have time to test it right away. I'll try later. )
In a Ruby on Rails application I am trying to use information from fields that are not associated with the model in validation.
Here is part of the model as an example (the whole model has gotten kinda big):
class Scorecard < ActiveRecord::Base
belongs_to :course
belongs_to :user
validate :attributes_consistency
def attributes_consistency
# Executed for all scorecards. Checks if the user completed the hole attributes correctly
if ( params[:no_fairways] and any_fairways? and !only_nine? ) or ( params[:no_fairways] and !any_h1_to_h9_score_blank and any_h1_to_h9_fairway? and only_nine? ) or ( params[:no_fairways] and !any_h10_to_h18_score_blank and any_h10_to_h18_fairway? and only_nine? )
errors.add_to_base("You inidicated that you missed all the fairways, but you also marked one or more fairways in the scorecard. Either uncheck the fairways mistakenly marked or uncheck the 'No fairways' checkbox.")
end
if ( params[:no_girs] and any_girs? and !only_nine? ) or ( params[:no_girs] and !any_h1_to_h9_score_blank and any_h1_to_h9_gir? and only_nine? ) or ( params[:no_girs] and !any_h10_to_h18_score_blank and any_h10_to_h18_gir? and only_nine? )
errors.add_to_base("You inidicated that you missed all the greens, but you also marked one or more greens in the scorecard. Either uncheck the marked greens on the scorecard or uncheck the 'No GIRs' checkbox.")
end
end # attributes_consistency
def any_h1_to_h9_score_blank?
h1_score.blank? or h2_score.blank? or h3_score.blank? or h4_score.blank? or h5_score.blank? or h6_score.blank? or h7_score.blank? or h8_score.blank? or h9_score.blank?
end
def any_h10_to_h18_score_blank?
h10_score.blank? or h11_score.blank? or h12_score.blank? or h13_score.blank? or h14_score.blank? or h15_score.blank? or h16_score.blank? or h17_score.blank? or h18_score.blank?
end
def any_h1_to_h9_fairway?
h1_fairway? or h2_fairway? or h3_fairway? or h4_fairway? or h5_fairway? or h6_fairway? or h7_fairway? or h8_fairway? or h9_fairway?
end
def any_h10_to_h18_fairway?
h10_fairway? or h11_fairway? or h12_fairway? or h13_fairway? or h14_fairway? or h15_fairway? or h16_fairway? or h17_fairway? or h18_fairway?
end
def any_h1_to_h9_gir?
h1_gir? or h2_gir? or h3_gir? or h4_gir? or h5_gir? or h6_gir? or h7_gir? or h8_gir? or h9_gir?
end
def any_h10_to_h18_gir?
h10_gir? or h11_gir? or h12_gir? or h13_gir? or h14_gir? or h15_gir? or h16_gir? or h17_gir? or h18_gir?
end
So how can I access params from the model?
Don't let params sneak up to the model. There's no point of having a controller in that case. Instead, checkout this episode from Railscasts that talks about virtual attributes that do not go into the database but can still be used for validations.
You don't need a corresponding model attribute for the virtual attributes. Define attributes local to the class such as #no_fairways that hold the state.
class ScoreCard < ActiveRecord::Base
# define attributes and accessors for both fields
attr_accessor :no_fairways, :no_girs
..
end
Now inside you form, you could just write:
<% form_for #scorecard %>
<%= f.check_box :no_fairways %>
<% end %>
Found the solution, thanks for the lingo though, "virtual attribute" helped with the google searchin.
The cleanliest way to accomplish this is to create attributes that are not part of the database but still part of the model. In my case I put this into the model:
attr_accessor :no_fairways
attr_accessor :no_girs
That easy! Now #scorecard.no_fairways and #scorecard.no_girs act just like any other attribute but aren't part of the database.
I have the following models set up:
class Contact < ActiveRecord::Base
belongs_to :band
belongs_to :mode
validates_presence_of :call, :mode
validates_associated :mode, :band
validates_presence_of :band, :if => :no_freq?
validates_presence_of :freq, :if => :no_band?
protected
def no_freq?
freq.nil?
end
def no_band?
band.nil?
end
end
class Band < ActiveRecord::Base
has_many :logs
end
class Mode < ActiveRecord::Base
has_many :logs
end
When I enter a frequency on my new view it allows for no band to be specified if a freq is entered. This creates a problem in my other views though because band is now nil. How do I allow for band not to be specified and just show up as empty on my index and show views, and then in the edit view allow one to be specified at a later point in time.
I have been able to get my index to display a blank by doing:
contact.band && contact.band.name
But I'm not sure if this is a best approach, and I'm unsure of how to apply a similar solution to my other views.
Many thanks from a rails newb!
In my views, I use the following for potentially nil objects in my views:
<%= #contact.band.name unless #contact.band.blank? %>
if your object is an array or hash, you can use the empty? function instead.
<%= unless #contacts.empty? %>
..some code
<% end %>
Hope this helps!
D
A couple years old but still a top Google result for "rails view handle nil" so I'll add my suggestion for use with Rails 3.2.3 and Ruby 1.9.3p0.
In application_helper.rb, add this:
def blank_to_nbsp(value)
value.blank? ? " ".html_safe : value
end
Then to display a value in a view, write something like this:
<%= blank_to_nbsp contact.band %>
Benefits:
"blank" catches both nil values and empty strings (details).
Simply omitting a nil object, or using an empty string, may cause formatting issues. pushes a non-breaking space into the web page and preserves formatting.
With the "if" and "unless" suggestions in other answers, you have to type each object name twice. By using a helper, you only have to type each object name once.
<%= #contact.try(:band).try(:name) %>
This will return nil if band or name do not exist as methods on their respective objects.
You can use Object#andand for this:
<%= #contact.band.andand.name %>
<%= #contact.band if #contact.band %> also works