问题
I have a simple rails app with models project and phase. A project has many phases, but only on phase can be active (i.e. "current") at a time. I still want the other phases to be accessible, but the current phase should be the main anchor for the application. The decision on how to implement this requirement has major implications on how I handle model access, validations and views / forms for creation update.
So the question is: How do I achieve this "has_many but has-only-one-current association" without adding too much complexity? Main goals being: simplicity in access of current phase + ensuring there cannot be more than 1 active phase at a time.
Naturally, I had some thoughts myself and came up with three options, which I want to present here. Any feedback on why I should choose one option over the other (or suggestion of a simpler solution) would be appreciated:
First Option:
[Project] has_many :phases
[Project] has_one :current_phase, :class_name => "Phase", :conditions => { :current => true }
Drawback: I have a nested form for creating projects and corresponding phases. There seems to be no easy way to set exactly one of the newly created phases as active
Second Option:
[Project] has an attribute "current_phase_id"
[Project] has_many :phases
[Project] belongs_to phase, :foreign_key => "current_phase_id"
Drawback: same as option 1, but I have another attribute and a belongs_to association, which seems weird (why should a project belong to one of its phases?)
Third Option:
[Phase] has an attribute "active" (boolean)
[Phase] scope :active, :conditions => { :active => true}
# Access to current phase via: project.phases.active
Drawback: I have to ensure via validations that there is only one active phase at a time, which is hard if multiple phases are created / edited at the same time OR during switch from one phase to another; plus: project.phases.active returns an array, if I'm not mistaken
Your help is greatly appreciated. Thanks!
Update
Added a bounty to encourage further opinions on the topic. Bounty will be awarded to the solution which best addresses the main goals expressed above; or if no alternative solution is mentioned, to the answer that best explains why I should favor one of the given options over the other. Thanks!
回答1:
Why don't you just add a date-time column called activated_at
to your Phase
model. Then set this to the current time whenever you want to make a phase active.
At any given time, the phase with the latest activated_at
value is the current phase so you can just get it with @project.phases.order('activated_at DESC').first
. Just wrap this in a method in Project
and you have a very concise representation:
# in project.rb
def current_phase
phases.where("activated_at is NOT NULL").order('activated_at DESC').first
end
回答2:
A well-presented question. I have struggled with something very similar. What I ended up with was similar to your option 1, but using a join table.
class Project < ActiveRecord::Base
has_many :phases, :through=> :project_phase
has_one :active_project_phase, :class_name => 'ProjectPhase'`
To set exactly one of the newly created phases active I have a bit of code in the controller that makes them all inactive and then either adds a new active phase if there are no phases or picks one to make active depending on the parameters passed in and a bunch of rules. It's not pretty, but it works. I did try option 3 first, but found this got very messy for the reasons you describe
回答3:
Option 1 looks very native. You need just to add validation to validate if there is only one phase with current
flag and project_id
and some javascript to control checkboxes on client side.
class Project < AR::Base
has_many :phases
has_one :current_phase, :class_name => "Phase", :conditions => { :current => true }
accepts_nested_attributes_for :phases, :allow_destroy => true
end
class Phase < AR::Base
belongs_to :project
validates :project_id, :uniqueness => {:scope => :current}, :if => proc{ self.current }
end
So, your views:
<%= form_for @project do |f| %>
...
<%= f.fields_for :phases do |phase| %>
<%= phase.text_field :title %> # or whatever
<%= phase.check_box :current, :class => "current_phase" %>
<% end %>
...
<% end %>
And small javascript (jQuery actually) to uncheck all current
checkboxes but one you clicked.
$(document).ready(function(){
$(".current_phase").click(function(){
$(".current_phase").not(this).attr('checked', false);
}
})
来源:https://stackoverflow.com/questions/6648792/what-is-the-rails-way-to-enforce-a-has-many-but-has-only-one-current-associati