Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the "rails way" to enforce a has_many but has-only-one-current association?

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!

like image 852
emrass Avatar asked Jul 11 '11 10:07

emrass


2 Answers

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
like image 65
edgerunner Avatar answered Nov 08 '22 14:11

edgerunner


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

like image 4
chrispanda Avatar answered Nov 08 '22 16:11

chrispanda