Skip to content

Commit

Permalink
Adds review workflow.
Browse files Browse the repository at this point in the history
closes #209
  • Loading branch information
justinlittman committed Jan 17, 2025
1 parent d1df0cc commit e018f0a
Show file tree
Hide file tree
Showing 28 changed files with 523 additions and 22 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ gem 'frozen_record' # For licenses
gem 'honeybadger'
gem 'kaminari' # For pagination
gem 'okcomputer'
gem 'state_machines-activerecord'
gem 'view_component'
gem 'whenever', require: false

Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,13 @@ GEM
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
ostruct
state_machines (0.6.0)
state_machines-activemodel (0.9.0)
activemodel (>= 6.0)
state_machines (>= 0.6.0)
state_machines-activerecord (0.9.0)
activerecord (>= 6.0)
state_machines-activemodel (>= 0.9.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.2)
Expand Down Expand Up @@ -601,6 +608,7 @@ DEPENDENCIES
solid_cable
solid_cache
solid_queue
state_machines-activerecord
stimulus-rails
turbo-rails
tzinfo-data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= render Elements::Tables::TableComponent.new(
id:, label: 'Drafts', show_label: false
) do |component| %>
<% component.with_header(
headers: ['Deposit', 'Collection', 'Owner', 'Last modified']
) %>
<% works.each do |work| %>
<% component.with_row(values: values_for(work), id: id_for(work)) %>
<% end %>
<% end %>
40 changes: 40 additions & 0 deletions app/components/dashboard/show/pending_review_list_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Dashboard
module Show
# Component for rendering a table on the work show page with works pending review.
class PendingReviewListComponent < ApplicationComponent
def initialize(works:)
@works = works
super()
end

attr_reader :works

def id
'pending-review-table'
end

def id_for(work)
dom_id(work, id)
end

def values_for(work)
[
link_to(work.title, link_for(work)),
link_to(work.collection.title, collection_path(druid: work.collection.druid)),
work.user.name,
I18n.l(work.updated_at, format: '%b %d, %Y')
]
end

private

def link_for(work)
return wait_works_path(work) unless work.druid

work_path(druid: work.druid)
end
end
end
end
16 changes: 16 additions & 0 deletions app/components/works/show/review_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<%= render Elements::AlertComponent.new do %>
<div class="h3">Review all details below then approve or reject this deposit.</div>
<%= form_with model: review_form, url: review_work_path, method: :put do |form| %>
<div class="form-check mb-3">
<%= form.radio_button :review_option, 'approve', class: 'form-check-input' %>
<%= form.label :review_option_approve, 'Approve', class: 'form-check-label' %>
</div>
<div class="form-check mb-3">
<%= form.radio_button :review_option, 'reject', class: 'form-check-input' %>
<%= form.label :review_option_reject, 'Reject', class: 'form-check-label' %>
<%= render Elements::Forms::TextareaFieldComponent.new(form:, field_name: :reject_reason, label: 'Reason for rejecting') %>
</div>

<%= render Elements::Forms::SubmitComponent.new(label: 'Submit') %>
<% end %>
<% end %>
20 changes: 20 additions & 0 deletions app/components/works/show/review_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Works
module Show
# Component providing a form for submitting a review.
class ReviewComponent < ApplicationComponent
def initialize(work:, review_form:)
@work = work
@review_form = review_form
super()
end

attr_reader :review_form

def render?
@work.pending_review? && helpers.allowed_to?(:review?, @work)
end
end
end
end
7 changes: 7 additions & 0 deletions app/components/works/show/review_rejected_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= render Elements::AlertComponent.new(variant: :warning) do %>
<div class="h3">Reviewer has rejected your deposit.</div>
<p>Fix the following and then submit again for approval:</p>
<blockquote class="blockquote">
<p><%= review_rejected_reason %></p>
</blockquote>
<% end %>
19 changes: 19 additions & 0 deletions app/components/works/show/review_rejected_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Works
module Show
# Component for rendering an alert for a rejected review.
class ReviewRejectedComponent < ApplicationComponent
def initialize(work:)
@work = work
super()
end

delegate :review_rejected_reason, to: :@work

def render?
@work.rejected_review?
end
end
end
end
6 changes: 5 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ def deposit?
params[:commit] == 'Deposit'
end

def request_review?
params[:commit] == 'Submit for review'
end

# NOTE: a `nil` validation context runs all validations without an explicit context
def validation_context
return :deposit if deposit?
return :deposit if deposit? || request_review?

nil
end
Expand Down
67 changes: 53 additions & 14 deletions app/controllers/works_controller.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
# frozen_string_literal: true

# Controller for a Work
class WorksController < ApplicationController
before_action :set_work, only: %i[show edit update destroy]
class WorksController < ApplicationController # rubocop:disable Metrics/ClassLength
before_action :set_work, only: %i[show edit update destroy review]
before_action :check_deposit_job_started, only: %i[show edit]
before_action :set_work_form_from_cocina, only: %i[show edit]
before_action :set_content, only: %i[show edit]
before_action :set_status, only: %i[show edit destroy]
before_action :set_presenter, only: %i[show edit]
before_action :set_work_form_from_cocina, only: %i[show edit review]
before_action :set_content, only: %i[show edit review]
before_action :set_status, only: %i[show edit destroy review]
before_action :set_presenter, only: %i[show edit review]

def show
authorize! @work

# This updates the Work with the latest metadata from the Cocina object.
# Does not update the Work's collection if the collection cannot be found.
ModelSync::Work.call(work: @work, cocina_object: @cocina_object, raise: false)

@review_form = ReviewForm.new
end

def new
Expand Down Expand Up @@ -52,11 +54,9 @@ def create # rubocop:disable Metrics/AbcSize

# The validation_context param determines whether extra validations are applied, e.g., for deposits.
if @work_form.valid?(validation_context)
# Setting the deposit_job_started_at to the current time to indicate that the deposit job has started and user
# should be "waiting".
work = Work.create!(title: @work_form.title, user: current_user, deposit_job_started_at: Time.zone.now,
collection: @collection)
DepositWorkJob.perform_later(work:, work_form: @work_form, deposit: deposit?)
perform_deposit(work:)
redirect_to wait_works_path(work.id)
else
@content = Content.find(@work_form.content_id)
Expand All @@ -65,16 +65,13 @@ def create # rubocop:disable Metrics/AbcSize
end
end

def update # rubocop:disable Metrics/AbcSize
def update
authorize! @work

@work_form = WorkForm.new(**update_work_params)
# The validation_context param determines whether extra validations are applied, e.g., for deposits.
if @work_form.valid?(validation_context)
# Setting the deposit_job_started_at to the current time to indicate that the deposit job has started and user
# should be "waiting".
@work.update!(deposit_job_started_at: Time.zone.now)
DepositWorkJob.perform_later(work: @work, work_form: @work_form, deposit: deposit?)
perform_deposit(work: @work)
redirect_to wait_works_path(@work.id)
else
@content = Content.find(@work_form.content_id)
Expand Down Expand Up @@ -106,12 +103,28 @@ def wait
redirect_to work_path(druid: work.druid) if work.deposit_job_finished?
end

def review
authorize! @work

@review_form = ReviewForm.new(**review_form_params)
if @review_form.valid?
redirect_path = perform_review
redirect_to redirect_path
else
render :show, status: :unprocessable_entity
end
end

private

def work_params
params.expect(work: WorkForm.user_editable_attributes + [WorkForm.nested_attributes])
end

def review_form_params
params.expect(review: %i[review_option reject_reason])
end

def update_work_params
work_params.merge(druid: params[:druid])
end
Expand Down Expand Up @@ -163,4 +176,30 @@ def new_work_form
release_date: @collection.max_release_date
)
end

def perform_deposit(work:)
# Setting the deposit_job_started_at to the current time to indicate that the deposit job has started and user
# should be "waiting".
work.update!(deposit_job_started_at: Time.zone.now)
deposit = deposit?
if request_review?
work.request_review!
deposit = false # Will be saved, but not deposited until approved.
end
DepositWorkJob.perform_later(work:, work_form: @work_form, deposit:)
end

# @return [String] path to redirect to after review
def perform_review
if @review_form.review_option == 'approve'
# Deposit
@work.update!(deposit_job_started_at: Time.zone.now)
@work.approve!
DepositWorkJob.perform_later(work: @work, work_form: @work_form, deposit: true)
wait_works_path(@work.id)
else
@work.reject_with_reason!(reason: @review_form.reject_reason)
work_path(druid: @work.druid)
end
end
end
10 changes: 10 additions & 0 deletions app/forms/review_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

# Form object for reviewing a work.
class ReviewForm < ApplicationForm
attribute :review_option, :string, default: 'approve'
validates :review_option, inclusion: { in: %w[approve reject] }

attribute :reject_reason, :string
validates :reject_reason, presence: true, if: -> { review_option == 'reject' }
end
4 changes: 4 additions & 0 deletions app/models/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ def max_release_date
def add_user_as_manager
managers << user if user.present? && managers.exclude?(user)
end

def review_enabled?
review_enabled
end
end
4 changes: 4 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ def your_collections
def your_works
Work.where(collection: your_collections)
end

def your_pending_review_works
Work.where(collection: reviewer_for).with_review_state(:pending_review)
end
end
21 changes: 21 additions & 0 deletions app/models/work.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ class Work < ApplicationRecord
belongs_to :user
belongs_to :collection

state_machine :review_state, initial: :none do
event :request_review do
transition none: :pending_review
transition rejected_review: :pending_review
end

event :approve do
transition pending_review: :none
end

event :reject do
transition pending_review: :rejected_review
end
end

def reject_with_reason!(reason:)
self.review_rejected_reason = reason
self.review_state_event = 'reject'
save!
end

# deposit_job_started_at indicates that the job is queued or running.
# User should be "waiting" until the job is completed.
def deposit_job_started?
Expand Down
15 changes: 14 additions & 1 deletion app/policies/work_policy.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
# frozen_string_literal: true

class WorkPolicy < ApplicationPolicy
alias_rule :new?, :create?, :show?, :update?, :edit?, :wait?, :destroy?, to: :manage?
alias_rule :new?, :create?, :update?, :edit?, :destroy?, to: :manage?
alias_rule :wait?, to: :show?

def manage?
record.user_id == user.id || collection_manager? || collection_owner? || collection_depositor?
end

def show?
manage? || collection_reviewer?
end

def review?
collection_reviewer?
end

def collection_manager?
return record.managers.include?(user) if record.is_a? Collection

Expand All @@ -22,4 +31,8 @@ def collection_depositor?
def collection_owner?
record.collection.user_id == user.id if record.is_a? Work
end

def collection_reviewer?
record.collection.reviewers.include?(user)
end
end
11 changes: 11 additions & 0 deletions app/presenters/work_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ def terms_of_use
TermsOfUseSupport.full_statement(custom_rights_statement:)
end

def status_message
case work.review_state
when 'pending_review'
'Pending review'
when 'rejected_review'
'Rejected'
else
super
end
end

private

delegate :collection, :created_at, :user, to: :work
Expand Down
Loading

0 comments on commit e018f0a

Please sign in to comment.