Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 3834 lines (2854 sloc) 62.1 KB
#LyX 2.0 created this file. For more info see http://www.lyx.org/
\lyxformat 413
\begin_document
\begin_header
\textclass hobo
\use_default_options true
\master hobo.lyx
\begin_modules
logicalmkup
\end_modules
\maintain_unincluded_children false
\language english
\language_package default
\inputencoding auto
\fontencoding global
\font_roman default
\font_sans default
\font_typewriter default
\font_default_family default
\use_non_tex_fonts false
\font_sc false
\font_osf false
\font_sf_scale 100
\font_tt_scale 100
\graphics default
\default_output_format default
\output_sync 0
\bibtex_command default
\index_command default
\float_placement H
\paperfontsize default
\spacing single
\use_hyperref false
\papersize default
\use_geometry false
\use_amsmath 1
\use_esint 1
\use_mhchem 1
\use_mathdots 1
\cite_engine basic
\use_bibtopic false
\use_indices false
\paperorientation portrait
\suppress_date false
\use_refstyle 1
\boxbgcolor #e6e6e6
\index Index
\shortcut idx
\color #008000
\end_index
\secnumdepth 3
\tocdepth 3
\paragraph_separation skip
\defskip smallskip
\quotes_language english
\papercolumns 1
\papersides 1
\paperpagestyle default
\tracking_changes false
\output_changes false
\html_math_output 0
\html_css_as_file 0
\html_be_strict false
\end_header
\begin_body
\begin_layout Chapter
Chapter 10
\begin_inset Newline newline
\end_inset
HOBO LIFECYCLES
\end_layout
\begin_layout Standard
This chapter of the Hobo manual describes Hobo’s “lifecycle” mechanism.
This is an extension that lets you define a lifecycle for any Active Record
model.
Defining a lifecycle is like a finite state machine – a pattern which turns
out to be extremely useful for modeling all sorts of processes that crop
up in the world that we’re trying to model.
\end_layout
\begin_layout Standard
That might make Hobo’s lifecycles sound similar to the well known
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
acts_as_state_machine
\end_layout
\end_inset
plugin, and in a way they are, but with Hobo style.
The big win comes from the fact that, like many things in Hobo:
\end_layout
\begin_layout Standard
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
There is support for this feature in all three of the MVC layers
\end_layout
\end_inset
\end_layout
\begin_layout Standard
This is the secret to making it very quick and easy to get up and running.
\end_layout
\begin_layout Section
Introduction
\end_layout
\begin_layout Standard
In the REST style, which is popular with Rails coders, we view our objects
a bit like documents: you can post them to a website, get them again later,
make changes to them and delete them.
Of course, these objects also have behavior, which we often implement by
hooking functionality to the create / update / delete events (like using
callbacks such as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
after_create
\end_layout
\end_inset
in Active Record).
\end_layout
\begin_layout Standard
In a pinch we may have to fall back to the RPC style, which Hobo has support
for with the “Web Method” feature.
\end_layout
\begin_layout Standard
This works great for many situations, but some objects are
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
not
\end_layout
\end_inset
best thought of as documents that we create and edit.
In particular, applications often contain objects that model some kind
of
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
process
\end_layout
\end_inset
.
A good example is
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
friendship
\end_layout
\end_inset
in a social app.
Here’s a description of how friendship might work:
\end_layout
\begin_layout Itemize
Any user can
\begin_inset Flex Strong
status collapsed
\begin_layout Plain Layout
invite
\end_layout
\end_inset
friendship with another user
\end_layout
\begin_layout Itemize
The other user can
\begin_inset Flex Strong
status collapsed
\begin_layout Plain Layout
accept
\end_layout
\end_inset
or
\begin_inset Flex Strong
status collapsed
\begin_layout Plain Layout
reject
\end_layout
\end_inset
(or perhaps
\begin_inset Flex Strong
status collapsed
\begin_layout Plain Layout
ignore
\end_layout
\end_inset
) the invite.
\end_layout
\begin_layout Itemize
The friendship is only
\begin_inset Flex Strong
status collapsed
\begin_layout Plain Layout
active
\end_layout
\end_inset
once it’s been accepted
\end_layout
\begin_layout Itemize
An active friendship can be
\begin_inset Flex Strong
status collapsed
\begin_layout Plain Layout
cancelled
\end_layout
\end_inset
by either user.
\end_layout
\begin_layout Standard
Not a “create”, “update” or “delete” in sight.
Those bold words capture the way we think about the friendship much better.
Of course we
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
could
\end_layout
\end_inset
implement friendship in a RESTful style, but we’d be doing just that –
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
implementing
\end_layout
\end_inset
it, not
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
declaring
\end_layout
\end_inset
it.
\end_layout
\begin_layout Standard
The life-cycle of the friendship would be hidden in our code, scattered
across a bunch of callbacks, permission methods and state variables.
Experience has shown this type of code to be tedious to write,
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
extremely
\end_layout
\end_inset
error prone and fragile when changing.
\end_layout
\begin_layout Standard
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
Hobo lifecycles is a mechanism for declaring the lifecycle of a model in
a natural manner.
\end_layout
\end_inset
\end_layout
\begin_layout Standard
REST vs.
lifecycles is not an either/or choice.
Some models will support both styles.
A good example is a content management system with some kind of editorial
workflow.
An application like that might have an Article model, which can be created,
updated and deleted like any other REST resource.
The Article might also feature a lifecycle that defines how the article
goes from newly authored, through one or more stages of review (possibly
being rejected at any stage) before finally becoming accepted, and later
published.
\end_layout
\begin_layout Subsection*
An Example
\end_layout
\begin_layout Standard
Everyone loves an example, so here is one.
We’ll stick with the friendship idea.
If you want to try this out, create a blank app and add a model:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
>ruby script/generate hobo_model friendship
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Here’s the code for the friendship mode (don’t be put off by the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
MagicMailer
\end_layout
\end_inset
, that’s just a made-up class to illustrate a common use of the callback
actions – sending emails):
\end_layout
\begin_layout Standard
\begin_inset Float figure
wide false
sideways false
status open
\begin_layout Plain Layout
\begin_inset Graphics
filename figures/ch10-friendship.rb.png
width 90col%
\end_inset
\end_layout
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
Defining the Friendship model
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Usually, the lifecycle can be represented as a graph, just as we would draw
a finite state machine:
\end_layout
\begin_layout Standard
\begin_inset Float figure
wide false
sideways false
status open
\begin_layout Plain Layout
\begin_inset Graphics
filename figures/ch10lifecycle_diagram.png
width 90col%
\end_inset
\end_layout
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
Lifecycle diagram
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Let’s work through what we did there.
\end_layout
\begin_layout Standard
Because
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Friendship
\end_layout
\end_inset
has a lifecycle declared, a class is created that captures the lifecycle.
The class is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Friendship::Lifecycle
\end_layout
\end_inset
.
Each instance of
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Friendship
\end_layout
\end_inset
will have an instance of this class associated with it, available as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
my_friendship.lifecycle
\end_layout
\end_inset
.
\end_layout
\begin_layout Standard
The
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Friendship
\end_layout
\end_inset
model will also have a field called
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
state
\end_layout
\end_inset
declared.
The migration generator will create a database column for
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
state
\end_layout
\end_inset
.
\end_layout
\begin_layout Standard
The lifecycle has three states:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
state :invited, :active, :ignored
\end_layout
\end_inset
\end_layout
\begin_layout Standard
There is one ‘creator’ – this is a starting point for the lifecycle:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
create :invite, :params => [:invitee], :become => :invited,
\end_layout
\begin_layout Code
:available_to =>
\begin_inset Quotes eld
\end_inset
User
\begin_inset Quotes erd
\end_inset
\end_layout
\begin_layout Code
:user_becomes => :invitor do
\end_layout
\begin_layout Code
MagicMailer.send invitee,
\end_layout
\begin_layout Code
\begin_inset Quotes eld
\end_inset
#{invitor.name} wants to be friends with you
\begin_inset Quotes erd
\end_inset
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
This declaration specifies that:
\end_layout
\begin_layout Itemize
The name of the creator is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
invite
\end_layout
\end_inset
.
It will be available as a method
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Friendship::Lifecycle.invite(user, attributes)
\end_layout
\end_inset
.
Calling the method will instantiate the record, setting attributes from
the hash that is passed in.
\end_layout
\begin_layout Itemize
The
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:params
\end_layout
\end_inset
option specifies which attributes can be set by this create step:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:params => [ :invitee ]
\end_layout
\end_inset
\end_layout
\begin_layout Standard
(Any other key in the attributes hash passed to invite will be ignored.)
\end_layout
\end_deeper
\begin_layout Itemize
The lifecycle state after this create step will be invited:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:become => :invited,
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Itemize
To have access to this create step, the acting user must be an instance
of User (i.e.
not a guest):
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:available_to => "User"
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Itemize
After the create step, the invitor association of the Friendship will be
set to the acting user:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:user_becomes => :invitor
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Itemize
After the create step has completed (and the database updated), the block
within do..end is executed:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:user_becomes => :invitor do
\end_layout
\begin_layout Code
MagicMailer.send invitee,
\end_layout
\begin_layout Code
\begin_inset Quotes eld
\end_inset
#{invitor.name} wants to be friends with you
\begin_inset Quotes erd
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Standard
There are five transitions declared:
\end_layout
\begin_layout Itemize
accept
\end_layout
\begin_layout Itemize
reject
\end_layout
\begin_layout Itemize
ignore
\end_layout
\begin_layout Itemize
retract
\end_layout
\begin_layout Itemize
cancel
\end_layout
\begin_layout Standard
These become methods on the lifecycle object (not the lifecycle class),
For example:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
my_fiendship.lifecycle.accept!(user, attributes)
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Calling that method will:
\end_layout
\begin_layout Itemize
Check if the transition is allowed.
\end_layout
\begin_layout Itemize
If it is, update the record with the passed in attributes.
The attributes that can change are declared in a
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:params
\end_layout
\end_inset
option, as we saw with the creator.
None of the friendship transitions declare any
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:params
\end_layout
\end_inset
, so no attributes will change
\end_layout
\begin_layout Itemize
Change the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
state
\end_layout
\end_inset
field to the new state, then save the record, as long as validations pass.
\end_layout
\begin_layout Standard
Each transition declares:
\end_layout
\begin_layout Itemize
Which states it goes from and to, e.g., accept goes from invited to active:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
transition :accept, { :invited => :active }
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Some of the transitions are to a pseudo state: :destroy.
To move to this state is to destroy the record.
\end_layout
\end_deeper
\begin_layout Itemize
Who has access to it:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:available_to => :invitor
\end_layout
\begin_layout Code
:available_to => :invitee
\end_layout
\end_inset
\end_layout
\begin_layout Standard
In the create step the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option was set to a class name, here it is set to a method (a
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
belongs_to
\end_layout
\end_inset
association).
\end_layout
\begin_layout Standard
To be allowed,
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
the acting user must be the same user returned by this method
\end_layout
\end_inset
.
There are a variety ways that
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
can be used, which will be discussed in detail later.
\end_layout
\end_deeper
\begin_layout Itemize
A callback (the block).
This is called after the transition completes.
Notice that in the block for the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
cancel
\end_layout
\end_inset
transition we’re accessing
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
acting_user
\end_layout
\end_inset
, which is a reference to the user performing the transition.
\end_layout
\begin_layout Standard
Hopefully that worked example has clarified what lifecycles are all about.
We’ll move on and look at the details now.
\end_layout
\begin_layout Section
Key concepts
\end_layout
\begin_layout Standard
Before getting into the API we’ll recap some of the key concepts very briefly.
\end_layout
\begin_layout Standard
As mentioned in the introduction, the lifecycle is essentially a finite
state machine.
It consists of:
\end_layout
\begin_layout Itemize
One or more
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
states
\end_layout
\end_inset
.
Each has a name, and the current state is stored in a simple string field
in the record.
If you like to think of a finite state machine as a graph, these are the
nodes.
\end_layout
\begin_layout Itemize
Zero or more
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
creators
\end_layout
\end_inset
.
Each has a name, and they define actions that can start the lifecycle,
setting the state to be some start-state.
\end_layout
\begin_layout Itemize
Zero or more
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
transitions
\end_layout
\end_inset
.
Each has a name, and they define actions that can change the state.
Again, thinking in terms of a graph, these are the arcs between the nodes.
\end_layout
\begin_layout Standard
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
The creators and the transitions are together known as the steps of the
lifecycle.
\end_layout
\end_inset
\end_layout
\begin_layout Standard
There are a variety of ways to limit which users are allowed to perform
which steps, and there are ways to attach custom actions (e.g., send an email)
both to steps and to states.
\end_layout
\begin_layout Section
Defining a lifecycle
\end_layout
\begin_layout Standard
Any Hobo model can be given a lifecycle like this:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
class Friendship < ActiveRecord::Base
\end_layout
\begin_layout Code
hobo_model
\end_layout
\begin_layout Code
\end_layout
\begin_layout Code
lifecycle do
\end_layout
\begin_layout Code
...
define lifecycle steps and states ..
\end_layout
\begin_layout Code
end
\end_layout
\begin_layout Code
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Any model that has such a declaration will gain the following features:
\end_layout
\begin_layout Itemize
The lifecycle definition becomes a class called
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Lifecycle
\end_layout
\end_inset
which is nested inside the model class (e.g.
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Friendship::Lifecycle
\end_layout
\end_inset
) and is a subclass of
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Hobo::Lifecycles::Lifecycle
\end_layout
\end_inset
.
The class has methods for each of the creators.
\end_layout
\begin_layout Itemize
Every instance of the model will have an instance of this class available
from the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
#lifecycle
\end_layout
\end_inset
method.
The instance has methods for each of the transitions:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
my_friendship.lifecycle.class # Friendship::Lifecyle
\end_layout
\begin_layout Code
my_friendship.lifecycle.reject!(user)
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Standard
The
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
lifecyle
\end_layout
\end_inset
declaration can take three options:
\end_layout
\begin_layout Itemize
:state_field - the name of the database field (a string field) to store
the current state in.
Default ’
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
state
\end_layout
\end_inset
\end_layout
\begin_layout Itemize
:key_timestamp_field - the name of the database field (a datetime field)
to store a timestamp for transitions that require a key (discussed later).
Set to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
false
\end_layout
\end_inset
if you don’t want this field.
Default ’
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
key_timestamp
\end_layout
\end_inset
’.
\end_layout
\begin_layout Itemize
:key_timeout - keys will expire after this amount of time.
Default
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
999.years
\end_layout
\end_inset
.
\end_layout
\begin_layout Standard
Note that both of these fields are declared
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
never_show
\end_layout
\end_inset
and
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
attr_protected
\end_layout
\end_inset
.
\end_layout
\begin_layout Standard
Within the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
lifecycle do ...
end
\end_layout
\end_inset
a simple DSL is in effect.
Using this we can add states and steps to the lifecycle.
\end_layout
\begin_layout Section
Defining states
\end_layout
\begin_layout Standard
To declare states:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
lifecycle do
\end_layout
\begin_layout Code
state :my_state, :my_other_state
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
You can call
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
state
\end_layout
\end_inset
many times, or pass several state names to the same call.
\end_layout
\begin_layout Standard
Each state can have an action associated with it:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
state :active do
\end_layout
\begin_layout Code
MagicMailer.send [invitee, invitor],
\end_layout
\begin_layout Code
"Congratulations, you are now friends"
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
You can provide the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:default => true
\end_layout
\end_inset
option to have the database default for the state field be this state:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
state :invited, :default => true
\end_layout
\end_inset
\end_layout
\begin_layout Standard
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
This will take effect the next time you generate and apply a hobo migration.
\end_layout
\end_inset
\end_layout
\begin_layout Section
Defining creators
\end_layout
\begin_layout Standard
A creator is the starting point for a lifecycle.
They provide a way for the record to be created (in addition to the regular
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
new
\end_layout
\end_inset
and
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
create
\end_layout
\end_inset
methods).
Each creator becomes a method on the lifecycle class.
The definition looks like:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
create name, options do
\end_layout
\begin_layout Code
...
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
The name is a symbol.
It should be a valid ruby name that does not conflict with the class methods
already present on the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Hobo::Lifecycles::Lifecycle
\end_layout
\end_inset
class.
\end_layout
\begin_layout Standard
The options are:
\end_layout
\begin_layout Itemize
:params - an array of attribute names that are parameters of this create
step.
These attributes can be set when the creator runs.
\end_layout
\begin_layout Itemize
:become - the state to enter after running this creator.
This does not have to be static but can depend on runtime state.
Provide one of:
\end_layout
\begin_deeper
\begin_layout Itemize
A symbol – the name of the state
\end_layout
\begin_layout Itemize
A proc – if the proc takes one argument it is called with the record, if
it takes none it is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record.
Should return the name of the state
\end_layout
\begin_layout Itemize
A string – evaluated as a Ruby expression with in the context of the record
\end_layout
\end_deeper
\begin_layout Itemize
:if and :unless – a precondition on the creator.
Pass either:
\end_layout
\begin_deeper
\begin_layout Itemize
A symbol – the name of a method to be called on the record
\end_layout
\begin_layout Itemize
A string – a Ruby expression, evaluated in the context of the record
\end_layout
\begin_layout Itemize
A proc – if the proc takes one argument it is called with the record, if
it takes none it is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record.
\end_layout
\begin_layout Standard
Note that the precondition is evaluated before any changes are made to the
record using the parameters to the lifecycle step.
\end_layout
\end_deeper
\begin_layout Itemize
:new_key – generate a new lifecycle key for this record by setting the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
key_timestamp
\end_layout
\end_inset
field to be the current time.
\end_layout
\begin_layout Itemize
:user_becomes – the name of an attribute (typically a
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
belongs_to
\end_layout
\end_inset
relationship) that will set to the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
acting_user
\end_layout
\end_inset
.
\end_layout
\begin_layout Itemize
:available_to – Specifies who is allowed access to the creator.
This check is in addition to the precondition (
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:if
\end_layout
\end_inset
or
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:unless
\end_layout
\end_inset
).
There are a variety of ways to provide the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option, discussed later on.
\end_layout
\begin_layout Standard
The block given to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
create
\end_layout
\end_inset
provides a callback which will be called after the record has been created.
You can give a block with a single argument, in which case it will be passed
the record, or with no arguments in which case it will be
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record.
\end_layout
\begin_layout Section
Defining transitions
\end_layout
\begin_layout Standard
A transition is an arc in the graph of the finite state machine – an operation
that takes the lifecycle from one state to another (or, potentially, back
to the same state.).
Each transition becomes a method on the lifecycle object (with ! appended).
The definition looks like:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
transition name, { from => to }, options do ...
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
The name is a symbol.
It should be a valid Ruby name
\end_layout
\begin_layout Standard
The second argument is a hash with a single item:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
{ from => to }
\end_layout
\end_inset
\end_layout
\begin_layout Standard
(We chose this syntax for the API just because the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
=>
\end_layout
\end_inset
is quite nice to indicate a transition)
\end_layout
\begin_layout Standard
This transition can only be fired in the state or states given as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
from
\end_layout
\end_inset
, which can be either a symbol or an array of symbols.
On completion of this transition, the record will be in the state give
as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
to
\end_layout
\end_inset
which can be one of:
\end_layout
\begin_layout Itemize
A symbol – the name of the state
\end_layout
\begin_layout Itemize
A proc – if the proc takes one argument it is called with the record, if
it takes none it is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record.
Should return the name of the state.
\end_layout
\begin_layout Itemize
A string – evaluated as a Ruby expression with in the context of the record.
\end_layout
\begin_layout Standard
The options are:
\end_layout
\begin_layout Itemize
:params - an array of attribute names that are parameters of this transition.
These attributes can be set when the transition runs.
\end_layout
\begin_layout Itemize
:if and :unless – a precondition on the transition.
Pass either:
\end_layout
\begin_deeper
\begin_layout Itemize
A symbol – the name of a method to be called on the record
\end_layout
\begin_layout Itemize
A string – a Ruby expression, evaluated in the context of the record
\end_layout
\begin_layout Itemize
A proc – if the proc takes one argument it is called with the record, if
it takes none it is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record.
\end_layout
\end_deeper
\begin_layout Itemize
:new_key – generate a new lifecycle key for this record by setting the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
key_timestamp
\end_layout
\end_inset
field to be the current time.
\end_layout
\begin_layout Itemize
:user_becomes – the name of an attribute (typically a
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
belongs_to
\end_layout
\end_inset
relationship) that will set to the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
acting_user
\end_layout
\end_inset
.
\end_layout
\begin_layout Itemize
:available_to – Specifies who is allowed access to the transition.
This check is in addition to the precondition (
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:if
\end_layout
\end_inset
or
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:unless
\end_layout
\end_inset
).
There are a variety of ways to provide the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option, discussed later on.
\end_layout
\begin_layout Standard
The block given to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
transition
\end_layout
\end_inset
provides a callback which will be called after the record has been updated.
You can give a block with a single argument, in which case it will be passed
the record, or with no arguments in which case it will be
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record.
\end_layout
\begin_layout Section
Repeated transition names
\end_layout
\begin_layout Standard
It is not required that a transition name is distinct from all the others.
For example, a process may have many stages (states) and there may be an
option to abort the process at any stage.
It is possible to define several transitions called
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:abort
\end_layout
\end_inset
, each starting from a different start state.
You could achieve a similar effect by listing all the start states in a
single transition, but by defining separate transitions, each one could,
for example, be given a different action (block).
\end_layout
\begin_layout Subsection*
The
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option
\end_layout
\begin_layout Standard
Both create and transition steps can be made accessible to certain users
with the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option.
If this option is given, the step is considered ‘publishable’, and there
will be automatic support for the step in both the controller and view
layers.
\end_layout
\begin_layout Standard
The rules for the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option are as follows.
Firstly, it can be one of these special values:
\end_layout
\begin_layout Itemize
:all – anyone, including guest users, can trigger the step
\end_layout
\begin_layout Itemize
:key_holder – (transitions only) anyone can trigger the transition, provided
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
record.lifecycle.provided_key
\end_layout
\end_inset
is set to the correct key.
Discussed in detail later.
\end_layout
\begin_layout Standard
If
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
is not one of those, it is an indication of some code to run (just like
the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:if
\end_layout
\end_inset
option for example):
\end_layout
\begin_layout Itemize
A symbol – the name of a method to call
\end_layout
\begin_layout Itemize
A string – a Ruby expression which is evaluated in the context of the record
\end_layout
\begin_layout Itemize
A proc – if the proc takes one argument it is called with the record, if
it takes none it is
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
instance_eval
\end_layout
\end_inset
‘d on the record
\end_layout
\begin_layout Standard
The value returned is then used to determine if the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
acting_user
\end_layout
\end_inset
has access or not.
The value is expected to be:
\end_layout
\begin_layout Itemize
A class – access is granted if the acting_user is a
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
kind_of?
\end_layout
\end_inset
that class.
\end_layout
\begin_layout Itemize
A collection – if the value responds to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:include?
\end_layout
\end_inset
, access is granted if
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
include?(acting_user)
\end_layout
\end_inset
is true.
\end_layout
\begin_layout Itemize
A record – if the value is neither a class or a collection, access is granted
if the value
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
is
\end_layout
\end_inset
the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
acting_user
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Some examples:
\end_layout
\begin_layout Standard
Say a model has an owner:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
belongs_to :owner, :class_name => "User"
\end_layout
\end_inset
\end_layout
\begin_layout Standard
You can just give the name of the relationship (since it is also a method)
to restrict the transition to that user:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:available_to => :owner
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Or a model might have a list of collaborators associated with it:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
has_many :collaborators, :class_name => "User"
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Again it’s easy to make the lifecycle step available to them only (since
the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
has_many
\end_layout
\end_inset
does respond to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:include?
\end_layout
\end_inset
):
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:available_to => :collaborators
\end_layout
\end_inset
\end_layout
\begin_layout Standard
If you were building more sophisticated role based permissions, you could
make sure you role object responds to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:include?
\end_layout
\end_inset
and then say, for example:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
:available_to => "Roles.editor"
\end_layout
\end_inset
\end_layout
\begin_layout Section
Validations
\end_layout
\begin_layout Standard
Validations have been extended so you can give the name of a lifecycle step
to the :on option.
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
validates_presence_of :notes, :on => :submit
\end_layout
\end_inset
\end_layout
\begin_layout Standard
There is now support for:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
record.lifecycle.valid_for_foo?
\end_layout
\end_inset
\end_layout
\begin_layout Standard
where
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
foo
\end_layout
\end_inset
is a lifecycle transition.
\end_layout
\begin_layout Section
Controller actions and routes
\end_layout
\begin_layout Standard
As well as providing the lifecycle mechanism in the model, Hobo also supports
the lifecycle in the controller layer, and provides an automatic user interface
in the view layer.
All of this can be fully customized of course.
In this section we’ll look at the controller layer features, including
the routes that get generated.
\end_layout
\begin_layout Standard
Lifecycle steps that include the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option are considered
\begin_inset Flex Emph
status collapsed
\begin_layout Plain Layout
publishable
\end_layout
\end_inset
.
It is these that Hobo generates controller actions for.
Any step that does not have the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option can be thought of as ‘internal’.
\end_layout
\begin_layout Standard
Of course you can call those create steps and transitions from your own
code, but Hobo will never do that for you.
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
auto_actions
\end_layout
\end_inset
\end_layout
\begin_layout Standard
The lifecycle actions are added to your controller by the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
auto_actions
\end_layout
\end_inset
directive.
To get them you need to say one of:
\end_layout
\begin_layout Itemize
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
auto_actions :all
\end_layout
\end_inset
\end_layout
\begin_layout Itemize
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
auto_actions :lifecycle
\end_layout
\end_inset
– adds only the lifecycle actions
\end_layout
\begin_layout Itemize
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
auto_actions :accept, :do_accept
\end_layout
\end_inset
(for example) – as always, you can list the method names explicitly (the
method names that relate to lifecycle actions are given below)
\end_layout
\begin_layout Standard
You can also remove lifecycle actions with:
\end_layout
\begin_layout Itemize
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
auto_actions ...
:except => :lifecycle
\end_layout
\end_inset
– don’t create any lifecycle actions or routes
\end_layout
\begin_layout Itemize
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
auto_actions ...
:except => [:do_accept, ...]
\end_layout
\end_inset
– don’t create the listed lifecycle actions or routes
\end_layout
\begin_layout Subsection*
Create steps
\end_layout
\begin_layout Standard
For each create step that is publishable, the model controller adds two
actions.
Going back to the friendship example, two actions will be created for the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
invite
\end_layout
\end_inset
step.
Both of these actions will pass the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
current_user
\end_layout
\end_inset
to the lifecycle, so access restrictions (the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to
\end_layout
\end_inset
option) will be enforced, as will any preconditions (
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:if
\end_layout
\end_inset
and
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:unless
\end_layout
\end_inset
).
\end_layout
\begin_layout Subsubsection*
The “create page” action
\end_layout
\begin_layout Standard
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
FriendshipsController#invite
\end_layout
\end_inset
will be routed as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
/friendships/invite
\end_layout
\end_inset
for GET requests.
This action is intended to render a form for the create step.
An object that provides metadata about the create step will be available
in
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
@creator
\end_layout
\end_inset
(an instance of
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Hobo::Lifecycles::Creator
\end_layout
\end_inset
).
\end_layout
\begin_layout Standard
If you want to implement this action yourself, you can do so using the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
creator_page_action
\end_layout
\end_inset
method:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
def invite
\end_layout
\begin_layout Code
creator_page_action :invite
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Following the pattern of all the action methods, you can pass a block in
which you can customize the response by setting a flash message, rendering
or redirecting.
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
do_creator_action
\end_layout
\end_inset
also takes a single option:
\end_layout
\begin_layout Itemize
:redirect – change where to redirect to on a successful submission.
Pass a symbol to redirect to that action (show actions only) or an array
of arguments which are passed to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
object_url
\end_layout
\end_inset
.
Passing a String or a Hash will pass your arguments straight to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
redirect_to
\end_layout
\end_inset
.
\end_layout
\begin_layout Subsubsection*
The ‘do create’ action
\end_layout
\begin_layout Standard
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
FriendshipsController#do_invite
\end_layout
\end_inset
will be routed as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
/friendships/invite
\end_layout
\end_inset
for POST requests.
\end_layout
\begin_layout Standard
This action is where the form should POST to.
It will run the create step, passing in parameters from the form.
As with normal form submissions (i.e.
create and update actions), the result will be an HTTP redirect, or the
form will be re-rendered in the case of validation failures.
\end_layout
\begin_layout Standard
Again you can implement this action yourself:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
def do_invite
\end_layout
\begin_layout Code
do_creator_action :invite
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
You can give a block to customize the response, or pass the redirect option:
\end_layout
\begin_layout Itemize
:redirect – change where to redirect to on a successful submission.
Pass a symbol to redirect to that action (show actions only) or an array
of arguments that are passed to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
object_url
\end_layout
\end_inset
.
Passing a String or a Hash will pass your arguments straight to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
redirect_to
\end_layout
\end_inset
.
\end_layout
\begin_layout Section
Transitions
\end_layout
\begin_layout Standard
As with create steps, for each publishable transition there are two actions.
For both of these actions, if
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
parmas[:key]
\end_layout
\end_inset
is present, it will be set as the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
provided_key
\end_layout
\end_inset
on the lifecycle, so transitions that are
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to => :key_holder
\end_layout
\end_inset
will work automatically.
\end_layout
\begin_layout Standard
We’ll take the friendship
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
accept
\end_layout
\end_inset
transition as an example.
\end_layout
\begin_layout Subsubsection*
The transition page
\end_layout
\begin_layout Standard
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
FriendshipsController#accept
\end_layout
\end_inset
will be routed as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
/friendships/:id/accept
\end_layout
\end_inset
for GET requests.
\end_layout
\begin_layout Standard
This action is intended to render a form for the transition.
An object that provides metadata about the transition will be available
in
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
@transition
\end_layout
\end_inset
(an instance of
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Hobo::Lifecycles::Transition
\end_layout
\end_inset
).
\end_layout
\begin_layout Standard
You can implement this action yourself using the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
transition_page_action
\end_layout
\end_inset
method
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
def accept
\end_layout
\begin_layout Code
transition_page_action :accept
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
As usual, you can customize the response by passing a block.
And you can pass the following option:
\end_layout
\begin_layout Itemize
:key – the key to set as the provided key, for transitions that are:
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to => :key_holder
\end_layout
\end_inset
.
Defaults to params[:key]
\end_layout
\begin_layout Subsubsection*
The ‘do transition’ action
\end_layout
\begin_layout Standard
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
FriendshipsController#do_accept
\end_layout
\end_inset
will be routed as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
/friendships/:id/accept
\end_layout
\end_inset
for POST requests.
\end_layout
\begin_layout Standard
This action is where the form should POST to.
It will run the transition, passing in parameters from the form.
As with normal form submissions (i.e., create and update actions), the result
will be an HTTP redirect, or the form will be re-rendered in the case of
validation failures.
\end_layout
\begin_layout Standard
You can implement this action yourself using the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
do_transition_action
\end_layout
\end_inset
method:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
def do_accept
\end_layout
\begin_layout Code
do_transition_action :accept
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\begin_layout Standard
As usual, you can customize the response by passing a block.
And you can pass the following options:
\end_layout
\begin_layout Itemize
:redirect – change where to redirect to on a successful submission.
Pass a symbol to redirect to that action (show actions only) or an array
of arguments which are passed to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
object_url
\end_layout
\end_inset
.
\end_layout
\begin_layout Itemize
:key – the key to set as the provided key, for transitions that are
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to => :key_holder
\end_layout
\end_inset
.
Defaults to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
params[:key]
\end_layout
\end_inset
\end_layout
\begin_layout Section
Keys and secure links
\end_layout
\begin_layout Standard
Hobo’s lifecycles also provide support for the “secure link” pattern.
By “secure” we mean that on one other than the holder of the link can access
the page or feature in question.
This is achieved by including some kind of cryptographic key in the URL,
which is typically sent in an email address.
The two very common examples are:
\end_layout
\begin_layout Itemize
Password reset – following the link gives the ability to set a new password
for a specific account.
By using a secure link and emailing it to the account holders email address,
only a person with access to that email account can chose the new password.
\end_layout
\begin_layout Itemize
Email activation – by following the link, the user has effectively proved
that they have access to that email account.
Many sites use this technique to verify that the email address you have
given is one that you do in fact have access to.
\end_layout
\begin_layout Standard
In fact the idea of a secure link is more general than that.
It can be applied in any situation where you want a particular person to
participate in a process, but that person does not have an account on the
site.
\end_layout
\begin_layout Standard
For example, in a CMS workflow application, you might want to email a particular
person to ask them to verify that the content of an article is technically
correct.
Perhaps this is a one-off request so you don’t want to trouble them with
signing up.
Your app could provide a page with “approve”/”reject” buttons, and access
to that page could be protected using the secure link pattern.
In this way, the person you email the secure link to, and no one else,
would be able to accept or reject the article.
\end_layout
\begin_layout Standard
Hobo’s lifecycles provide support for the secure-link pattern with the following
:
\end_layout
\begin_layout Itemize
A field added to the database called (by default) ”
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
key_timestamp
\end_layout
\end_inset
”.
This is a date-time field, and is used to generate a key as follows:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
Digest::SHA1.hexdigest{“#{id_of_record}-
\backslash
\end_layout
\begin_layout Code
#{current_state}-#{key_timestamp}”}
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Itemize
Both create and transition steps can be given the option
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:new_key => true
\end_layout
\end_inset
.
This causes the
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
key_timestamp
\end_layout
\end_inset
to be updated to
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
Time.now
\end_layout
\end_inset
.
\end_layout
\begin_layout Itemize
The
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to => :key_holder
\end_layout
\end_inset
option (transitions only).
Setting this means the transition is only allowed if the correct key has
been provided, like this:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
record.lifecycle.provided_key = the_key
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Hobo’s “model controller” also has (very simple) support for the secure-link
pattern.
Prior to rendering the form for a transition, or accepting the form submission
of a transition, it does (by default):
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
record.lifecycle.provided_key = params[:key]
\end_layout
\end_inset
\end_layout
\begin_layout Subsubsection*
Implementing a lifecycle with a secure-link
\end_layout
\begin_layout Standard
Stringing this all together, we would typically implement the secure-link
pattern as follows.
\end_layout
\begin_layout Standard
We’re assuming some knowledge of Rails mailers here, so you may need to
read up on those.
\end_layout
\begin_layout Itemize
Create a mailer (
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
script/generate mailer
\end_layout
\end_inset
) which will be used to send the secure link.
\end_layout
\begin_layout Itemize
In your lifecycle definition, two steps will work together:
\end_layout
\begin_deeper
\begin_layout Itemize
A create or transition will initiate the process, by generating a new key,
emailing the link, and putting the lifecycle in the correct state.
\end_layout
\begin_layout Itemize
A transition from this state will be declared as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to => :key_holder
\end_layout
\end_inset
, and will perform the protected action.
\end_layout
\end_deeper
\begin_layout Itemize
Add
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:new_key => true
\end_layout
\end_inset
to the create or transition step that initiates the process.
\end_layout
\begin_layout Itemize
On this same step, add a callback that uses the mailer to send the key to
the appropriate user.
The key is available as
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
lifecycle.key
\end_layout
\end_inset
.
For example, the default Hobo user model has:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
Transition :request_pasword_rest,
\end_layout
\begin_layout Code
{ :active => :active },
\end_layout
\begin_layout Code
:new_key => true do
\end_layout
\begin_layout Code
UserMailer.deliver_forgot_password(self, lifecycle.key)
\end_layout
\begin_layout Code
end
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Itemize
Add
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
:available_to => :key_holder
\end_layout
\end_inset
to the subsequent transition – the one you want to make available only
to recipients of the email.
\end_layout
\begin_layout Itemize
The mailer should include a link in the email, and they key should be part
of this link as a query parameter.
Hobo creates a named route for each transition page, so there will be a
URL helper available.
For example, if the transition is on
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
User
\end_layout
\end_inset
and is called
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
reset_password
\end_layout
\end_inset
, the link in your mailer template should look something like:
\end_layout
\begin_deeper
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
<%= user_reset_password_url
\end_layout
\begin_layout Code
:host => @host, :id => @user, :key => @key %>
\end_layout
\end_inset
\end_layout
\end_deeper
\begin_layout Subsubsection*
Testing for the active step.
\end_layout
\begin_layout Standard
In some rare cases your code might need to know if a lifecycle step is currently
in progress or not (e.g.
in a callback or a validation).
For this you can access either:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
record.lifecycle.submit_in_progress.active_step.name
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Or, if you are interested in a particular step, it’s easier to call:
\end_layout
\begin_layout Standard
\begin_inset Box Shadowbox
position "t"
hor_pos "c"
has_inner_box 1
inner_pos "t"
use_parbox 0
use_makebox 0
width "100col%"
special "none"
height "1in"
height_special "totalheight"
status open
\begin_layout Code
record.lifecycle.submit_in_progress?
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Where
\begin_inset Flex Code
status collapsed
\begin_layout Plain Layout
submit
\end_layout
\end_inset
can be any lifecycle step.
\end_layout
\begin_layout Standard
\end_layout
\end_body
\end_document
Something went wrong with that request. Please try again.