Skip to content
This repository has been archived by the owner on May 14, 2022. It is now read-only.

Commit

Permalink
Adding a state machine to manage states and transitions
Browse files Browse the repository at this point in the history
  • Loading branch information
escowles committed Dec 4, 2015
1 parent e52a940 commit cefd3e1
Show file tree
Hide file tree
Showing 22 changed files with 325 additions and 84 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ gem "omniauth-cas"
gem 'ezid-client'
gem 'sprockets-es6'
gem 'browse-everything', github: 'projecthydra-labs/browse-everything'
gem 'aasm'
source 'https://rails-assets.org' do
gem 'rails-assets-babel-polyfill'
end
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ GEM
remote: https://rubygems.org/
remote: https://rails-assets.org/
specs:
aasm (4.5.0)
actionmailer (4.2.4)
actionpack (= 4.2.4)
actionview (= 4.2.4)
Expand Down Expand Up @@ -784,6 +785,7 @@ PLATFORMS
ruby

DEPENDENCIES
aasm
active-fedora!
activefedora-aggregation!
browse-everything!
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/common_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def apply_remote_metadata
end

def check_completion
complete_record if self.state_changed? && state == 'complete'
complete_record if state_changed? && state == 'complete'
end

private
Expand Down
4 changes: 4 additions & 0 deletions app/models/solr_document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def state
Array(self[Solrizer.solr_name("state")]).first
end

def type
self['active_fedora_model_ssi']
end

def viewing_hint
Array(self[Solrizer.solr_name("viewing_hint")]).first
end
Expand Down
56 changes: 56 additions & 0 deletions app/models/state_workflow.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# State-based workflow: The primary workflow is to start at pending, and
# progress through metadata_review and final_review to complete. There are
# two exceptional workflows: between complete/takedown (for issues requiring
# an item be suppressed), and between complete/flagged (for issues where an
# item can remain accessible).
class StateWorkflow
include AASM

def initialize(state)
aasm.current_state = state.to_sym unless state.nil?
end

aasm do
state :pending, initial: true
state :metadata_review
state :final_review
state :complete
state :takedown
state :flagged

# ingest workflow
event :finalize_digitization do
transitions from: :pending, to: :metadata_review
end
event :finalize_metadata do
transitions from: :metadata_review, to: :final_review
end
event :complete do
transitions from: :final_review, to: :complete
end

# takedown/restore workflow
event :takedown do
transitions from: :complete, to: :takedown
end
event :restore do
transitions from: :takedown, to: :complete
end

# flag/unflag workflow
event :flag do
transitions from: :complete, to: :flagged
end
event :unflag do
transitions from: :flagged, to: :complete
end
end

def suppressed?
pending? || metadata_review? || final_review? || takedown?
end

def valid_transitions
aasm.states(permitted: true).map(&:name)
end
end
4 changes: 2 additions & 2 deletions app/presenters/curation_concerns_show_presenter.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class CurationConcernsShowPresenter < CurationConcerns::WorkShowPresenter
delegate :date_created, :viewing_hint, :viewing_direction, :state, :identifier, :workflow_note, to: :solr_document
delegate :date_created, :viewing_hint, :viewing_direction, :state, :type, :identifier, :workflow_note, to: :solr_document

def state_badge
StateBadge.new(solr_document).render
StateBadge.new(type, state).render
end
end
4 changes: 0 additions & 4 deletions app/presenters/file_set_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,4 @@ class FileSetPresenter < CurationConcerns::FileSetPresenter
def label
nil
end

def state_badge
StateBadge.new(solr_document).render
end
end
66 changes: 44 additions & 22 deletions app/presenters/state_badge.rb
Original file line number Diff line number Diff line change
@@ -1,41 +1,63 @@
class StateBadge
include ActionView::Context
include ActionView::Helpers::TagHelper

def initialize(solr_document)
@solr_document = solr_document
def initialize(type, state = nil)
@type = type.underscore
@state = StateWorkflow.new state
end

def render
content_tag(:span, link_title, title: link_title, class: "label #{dom_label_class}")
label = I18n.t("state.#{current_state}.label")
content_tag(:span, label, title: label, class: "label #{dom_label_class}")
end

def render_buttons
html = render_radio_button(current_state, checked: true)
@state.valid_transitions.each do |valid_state|
html += render_radio_button(valid_state)
end
html
end

def render_hidden
tag :input, id: "#{field_id}", name: field_name, type: :hidden, value: current_state
end

private

def dom_label_class
if complete?
'label-success'
elsif review?
'label-info'
else
'label-warning'
def render_radio_button(state, checked = false)
content_tag :label, class: 'radio' do
tag(:input, id: field_id(state), name: field_name, type: :radio, value: state, checked: checked) +
content_tag(:span, I18n.t("state.#{state}.label"), class: "label #{dom_label_class(state)}", for: field_id(state)) +
" " + I18n.t("state.#{state}.desc")
end
end

def link_title
if complete?
'Complete'
elsif review?
'Review'
else
'Pending'
end
def dom_label_class(state = current_state)
state_classes[state]
end

def state_classes
@state_classes ||= {
pending: 'label-default',
metadata_review: 'label-info',
final_review: 'label-primary',
complete: 'label-success',
flagged: 'label-warning',
takedown: 'label-danger'
}
end

def current_state
@state.aasm.current_state
end

def complete?
@solr_document.state == 'complete'
def field_name
"#{@type}[state]"
end

def review?
@solr_document.state == 'review'
def field_id(state = current_state)
"#{@type}_state_#{state}"
end
end
18 changes: 15 additions & 3 deletions app/validators/state_validator.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
class StateValidator < ActiveModel::Validator
delegate :validate, to: :inclusivity_validator
def validate(record)
inclusivity_validator.validate record
validate_transition record
end

private

def validate_transition(record)
return unless record.state_changed? && !record.state_was.nil?
return if StateWorkflow.new(record.state_was).aasm.states(permitted: true).include? record.state.to_sym
record.errors.add :state, "Cannot transition from #{record.state_was} to #{record.state}"
end

def inclusivity_validator
@inclusivity_validator ||= ActiveModel::Validations::InclusionValidator.new(
attributes: :state,
Expand All @@ -13,9 +22,12 @@ def inclusivity_validator

def valid_states
[
"complete",
"pending",
"review"
"metadata_review",
"final_review",
"complete",
"flagged",
"takedown"
]
end
end
14 changes: 8 additions & 6 deletions app/views/curation_concerns/base/_attributes.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
<%= @presenter.permission_badge %>
</td>
</tr>
<tr>
<th>State</th>
<td>
<%= @presenter.state_badge %>
</td>
</tr>
<% if @presenter.respond_to? :state_badge %>
<tr>
<th>State</th>
<td>
<%= @presenter.state_badge %>
</td>
</tr>
<% end %>
<%= @presenter.attribute_to_html(:embargo_release_date) %>
<%= @presenter.attribute_to_html(:lease_expiration_date) %>
<%= @presenter.attribute_to_html(:rights) %>
Expand Down
20 changes: 4 additions & 16 deletions app/views/curation_concerns/base/_form_state.html.erb
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
<% unless curation_concern.new_record? %>
<% if curation_concern.new_record? %>
<%= StateBadge.new(curation_concern.class.to_s).render_hidden %>
<% else %>
<fieldset id="set-state">
<legend>State</legend>
<div class="form-group">
<label class="radio">
<%= f.radio_button :state, 'pending', checked: curation_concern.state != 'complete' && curation_concern.state != 'review' %>
<%= f.label 'Pending', class: 'label label-warning', for: 'scanned_resource_state_pending' %>
In progress and suppressed from public display
</label>
<label class="radio">
<%= f.radio_button :state, 'review', checked: curation_concern.state == 'review' %>
<%= f.label 'Review', class: 'label label-info', for: 'scanned_resource_state_review' %>
In need of review
</label>
<label class="radio">
<%= f.radio_button :state, 'complete', checked: curation_concern.state == 'complete' %>
<%= f.label 'Complete', class: 'label label-success', for: 'scanned_resource_state_complete' %>
Finished work, visibility determined by Access Rights
</label>
<%= StateBadge.new(curation_concern.class.to_s, curation_concern.state).render_buttons %>
</div>
</fieldset>
<% end %>
19 changes: 19 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,22 @@ en:
"": 'Single Page'
non-paged: 'Non-Paged'
facing-pages: 'Facing pages'
state:
pending:
label: 'Pending'
desc: 'Initial digitization, suppressed from display'
metadata_review:
label: 'Metadata Review'
desc: 'Awaiting metadata approval'
final_review:
label: 'Final Review'
desc: 'Awaiting final approval before being published'
complete:
label: 'Complete'
desc: 'Published and accessible according to access control rules'
flagged:
label: 'Flagged'
desc: 'In need of attention, but still accessible according to access rules'
takedown:
label: 'Takedown'
desc: 'Formerly-published but suppressed from display'
2 changes: 1 addition & 1 deletion spec/factories/scanned_resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use_and_reproduction "Jamie, remind me to give you a lesson in tying knots, sometime."
description "900 years of time and space, and I’ve never been slapped by someone’s mother."
visibility Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC
state "complete"
state "metadata_review"

transient do
user { FactoryGirl.create(:user) }
Expand Down
4 changes: 2 additions & 2 deletions spec/features/edit_multi_volume_work_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
fill_in 'multi_volume_work_portion_note', with: 'new portion note'
fill_in 'multi_volume_work_description', with: 'new description'
fill_in 'multi_volume_work_workflow_note', with: 'New note'
choose 'Complete'
choose 'Metadata Review'

click_button 'Update Multi volume work'
expect(page).to have_text("Test title (Multi Volume Work)")
expect(page).to have_text("New note")
expect(page).to have_selector("span.label-success", "Complete")
expect(page).to have_selector("span.label-info", "Metadata Review")
end

scenario "User can create a new scanned resource attached to the multi-volume work" do
Expand Down
4 changes: 2 additions & 2 deletions spec/features/edit_scanned_resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
fill_in 'scanned_resource_source_metadata_identifier', with: '1234568'
fill_in 'scanned_resource_portion_note', with: 'new portion note'
fill_in 'scanned_resource_description', with: 'new description'
choose 'Pending'
choose 'Final Review'

click_button 'Update Scanned resource'
expect(page).to have_text("Test title (Scanned Resource)")
expect(page).to have_selector("span.label-warning", "Pending")
expect(page).to have_selector("span.label-primary", "Final Review")
end

scenario "User can add a new file" do
Expand Down
2 changes: 1 addition & 1 deletion spec/features/new_scanned_resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
click_button 'Create Scanned resource'

expect(page).to have_selector("h1", "Test Title")
expect(page).to have_selector("span.label-warning", "Pending")
expect(page).to have_selector("span.label-default", "Pending")
expect(page).to have_text("Jamie, remind me to give you a lesson in tying knots, sometime")
end
end
Expand Down
10 changes: 9 additions & 1 deletion spec/models/scanned_resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
end

describe "#check_completion" do
subject { FactoryGirl.build(:scanned_resource, source_metadata_identifier: '12345', access_policy: 'Policy', use_and_reproduction: 'Statement', state: 'final_review') }
before do
subject.save
end
Expand All @@ -192,7 +193,7 @@
expect { subject.check_completion }.not_to change { ActionMailer::Base.deliveries.count }
end
it "does not complete record when state isn't 'complete'" do
subject.state = 'pending'
subject.state = 'final_review'
expect(subject).not_to receive(:complete_record)
expect { subject.check_completion }.not_to change { ActionMailer::Base.deliveries.count }
end
Expand All @@ -204,6 +205,13 @@
expect { subject.check_completion }.to change { ActionMailer::Base.deliveries.count }.by(1)
expect(subject.identifier).to eq('1234')
end
it "does not complete the record when the state transition is invalid" do
allow(subject).to receive("state_changed?").and_return true
subject.state = 'pending'
expect(subject).not_to receive(:complete_record)
expect { subject.check_completion }.not_to change { ActionMailer::Base.deliveries.count }
expect(subject.identifier).to eq(nil)
end
end

describe "#pending_uploads" do
Expand Down
Loading

0 comments on commit cefd3e1

Please sign in to comment.