Skip to content

Commit

Permalink
Refactor ImagesController using interactor pattern
Browse files Browse the repository at this point in the history
This demonstrates a demo of how the ImagesController could work using
the interactor gem.

There were some small changes I made here compared to the existing code:

1. We no longer preview on image creation. There isn't any point doing
   this as it won't succeed - as initial image is invalid
2. We round the number as part of cropping an image. This saves this
   frequently being off by 1 pixel, fixing this made tests easier.
  • Loading branch information
kevindew committed Apr 12, 2019
1 parent 906bd53 commit 4802ab2
Show file tree
Hide file tree
Showing 9 changed files with 592 additions and 118 deletions.
172 changes: 54 additions & 118 deletions app/controllers/images_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,21 @@ def index
end

def create
Edition.find_and_lock_current(document: params[:document]) do |edition|
@issues = ::Requirements::ImageUploadChecker.new(params[:image]).issues

if @issues.any?
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.index.flashes.upload_requirements"),
"items" => @issues.items,
}

render :index,
assigns: { edition: edition },
layout: rendering_context,
status: :unprocessable_entity
next
end

image_revision = ImageUploadService.new(params[:image], edition.revision).call(current_user)
updater = Versioning::RevisionUpdater.new(edition.revision, current_user)
updater.update_image(image_revision)
edition.assign_revision(updater.next_revision, current_user).save!
PreviewService.new(edition).try_create_preview
redirect_to crop_image_path(params[:document], image_revision.image_id, wizard: "upload")
result = Images::Create.call(params: params, user: current_user)
if result.failure?
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.index.flashes.upload_requirements"),
"items" => result.issues.items,
}

render :index,
assigns: { edition: result.edition },
layout: rendering_context,
status: :unprocessable_entity
else
redirect_to crop_image_path(result.edition.document,
result.image_revision.image_id,
wizard: "upload")
end
end

Expand All @@ -39,21 +32,10 @@ def crop
end

def update_crop
Edition.find_and_lock_current(document: params[:document]) do |edition|
image_revision = edition.image_revisions.find_by!(image_id: params[:image_id])
image_updater = Versioning::ImageRevisionUpdater.new(image_revision, current_user)
image_updater.assign(update_crop_params)

if image_updater.changed?
updater = Versioning::RevisionUpdater.new(edition.revision, current_user)
updater.update_image(image_updater.next_revision)
edition.assign_revision(updater.next_revision, current_user).save!
TimelineEntry.create_for_revision(entry_type: :image_updated, edition: edition)
PreviewService.new(edition).try_create_preview
end

redirect_to edit_image_path(edition.document, image_revision.image_id, wizard: params[:wizard])
end
result = Images::UpdateCrop.call(params: params, user: current_user)
redirect_to edit_image_path(result.edition.document,
result.image_revision.image_id,
wizard: params[:wizard])
end

def edit
Expand All @@ -63,80 +45,49 @@ def edit
end

def update
Edition.find_and_lock_current(document: params[:document]) do |edition| # rubocop:disable Metrics/BlockLength
image_revision = edition.image_revisions.find_by!(image_id: params[:image_id])
image_updater = Versioning::ImageRevisionUpdater.new(image_revision, current_user)

image_updater.assign(update_params)
next_image_revision = image_updater.next_revision

issues = Requirements::ImageRevisionChecker.new(next_image_revision)
.pre_preview_metadata_issues

if issues.any?
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.edit.flashes.requirements"),
"items" => issues.items,
}

render :edit,
assigns: { edition: edition, image_revision: next_image_revision, issues: issues },
layout: rendering_context,
status: :unprocessable_entity
next
end

updater = Versioning::RevisionUpdater.new(edition.revision, current_user)
updater.update_image(next_image_revision, params[:lead_image] == "on")

if updater.changed?
timeline_entry_type = if updater.selected_lead_image?
:lead_image_selected
elsif updater.removed_lead_image?
:lead_image_removed
else
:image_updated
end

TimelineEntry.create_for_revision(entry_type: timeline_entry_type, edition: edition)
edition.assign_revision(updater.next_revision, current_user).save!
PreviewService.new(edition).try_create_preview
end

if updater.selected_lead_image?
redirect_to document_path(edition.document),
result = Images::Update.call(params: params, user: current_user)

if result.failure?
flash.now["alert_with_items"] = {
"title" => I18n.t!("images.edit.flashes.requirements"),
"items" => result.issues.items,
}

render :edit,
assigns: { edition: result.edition,
image_revision: result.image_revision,
issues: result.issues },
layout: rendering_context,
status: :unprocessable_entity
else
document = result.edition.document

if result.updater.selected_lead_image?
redirect_to document_path(document),
notice: t("documents.show.flashes.lead_image.selected",
file: image_revision.filename)
elsif updater.removed_lead_image?
redirect_to images_path(edition.document),
file: result.image_revision.filename)
elsif result.updater.removed_lead_image?
redirect_to images_path(document),
notice: t("images.index.flashes.lead_image.removed",
file: image_revision.filename)
file: result.image_revision.filename)
else
redirect_to images_path(edition.document)
redirect_to images_path(document)
end
end
end

def destroy
Edition.find_and_lock_current(document: params[:document]) do |edition|
image_revision = edition.image_revisions.find_by!(image_id: params[:image_id])
updater = Versioning::RevisionUpdater.new(edition.revision, current_user)

updater.remove_image(image_revision)
edition.assign_revision(updater.next_revision, current_user).save!

TimelineEntry.create_for_revision(entry_type: :image_deleted, edition: edition)
PreviewService.new(edition).try_create_preview

if updater.removed_lead_image?
redirect_to images_path(edition.document),
notice: t("images.index.flashes.lead_image.deleted",
file: image_revision.filename)
else
redirect_to images_path(edition.document),
notice: t("images.index.flashes.deleted",
file: image_revision.filename)
end
result = Images::Destroy.call(params: params, user: current_user)
document = result.edition.document

if result.updater.removed_lead_image?
redirect_to images_path(document),
notice: t("images.index.flashes.lead_image.deleted",
file: result.image_revision.filename)
else
redirect_to images_path(document),
notice: t("images.index.flashes.deleted",
file: result.image_revision.filename)
end
end

Expand All @@ -151,19 +102,4 @@ def download
type: image_revision.content_type,
)
end

private

def update_params
params.require(:image_revision).permit(:caption, :alt_text, :credit)
end

def update_crop_params
image_aspect_ratio = Image::HEIGHT.to_f / Image::WIDTH

params
.require(:image_revision)
.permit(:crop_x, :crop_y, :crop_width, :crop_width)
.tap { |p| p[:crop_height] = (p[:crop_width].to_i * image_aspect_ratio).round }
end
end
40 changes: 40 additions & 0 deletions app/interactors/images/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

class Images::Create
include Interactor
delegate :params, :user, to: :context

def initialize(params:, user:)
super
end

def call
Edition.find_and_lock_current(document: params[:document]) do |edition|
check_issues(edition, params[:image])
image_revision = upload_image(edition, params[:image])
update_edition(edition, image_revision)
update_context(edition: edition, image_revision: image_revision)
end
end

private

def check_issues(edition, image_params)
issues = Requirements::ImageUploadChecker.new(image_params).issues
context.fail!(edition: edition, issues: issues) if issues.any?
end

def upload_image(edition, image_params)
ImageUploadService.new(image_params, edition.revision).call(user)
end

def update_edition(edition, image_revision)
updater = Versioning::RevisionUpdater.new(edition.revision, user)
updater.update_image(image_revision, false)
edition.assign_revision(updater.next_revision, user).save!
end

def update_context(attributes)
attributes.each { |k, v| context[k.to_sym] = v }
end
end
34 changes: 34 additions & 0 deletions app/interactors/images/destroy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

class Images::Destroy
include Interactor
delegate :params, :user, to: :context

def initialize(params:, user:)
super
end

def call
Edition.find_and_lock_current(document: params[:document]) do |edition|
image_revision = edition.image_revisions.find_by!(image_id: params[:image_id])
updater = Versioning::RevisionUpdater.new(edition.revision, user)

updater.remove_image(image_revision)
edition.assign_revision(updater.next_revision, user).save!

TimelineEntry.create_for_revision(entry_type: :image_deleted,
edition: edition)
PreviewService.new(edition).try_create_preview

update_context(edition: edition,
image_revision: image_revision,
updater: updater)
end
end

private

def update_context(attributes)
attributes.each { |k, v| context[k.to_sym] = v }
end
end
82 changes: 82 additions & 0 deletions app/interactors/images/update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

class Images::Update
include Interactor
delegate :params, :user, to: :context

def initialize(params:, user:)
super
end

def call
Edition.find_and_lock_current(document: params[:document]) do |edition|
image_revision = update_image_revision(edition, image_params)
check_issues(edition, image_revision)

updater = Versioning::RevisionUpdater.new(edition.revision, user)
updater.update_image(image_revision, params[:lead_image] == "on")

if updater.changed?
create_timeline_entry(edition, updater)
edition.assign_revision(updater.next_revision, user).save!
PreviewService.new(edition).try_create_preview
end

update_context(edition: edition,
image_revision: image_revision,
updater: updater)
end
end

private

def update_image_revision(edition, update_params)
image_revision = edition.image_revisions.find_by!(image_id: params[:image_id])
updater = Versioning::ImageRevisionUpdater.new(image_revision, user)
updater.assign(update_params)
updater.next_revision
end

def image_params
params.require(:image_revision).permit(:caption, :alt_text, :credit)
end

def check_issues(edition, image_revision)
checker = Requirements::ImageRevisionChecker.new(image_revision)
issues = checker.pre_preview_metadata_issues
if issues.any?
context.fail!(edition: edition,
image_revision: image_revision,
issues: issues)
end
end

def upload_image(image_params)
upload_service = ImageUploadService.new(image_params, context.edition.revision)
context.image_revision = upload_service.call(context.user)
end

def update_edition
updater = Versioning::RevisionUpdater.new(context.edition.revision,
context.user)

updater.update_image(context.image_revision, false)
context.edition.assign_revision(updater.next_revision, context.user).save!
end

def create_timeline_entry(edition, updater)
timeline_entry_type = if updater.selected_lead_image?
:lead_image_selected
elsif updater.removed_lead_image?
:lead_image_removed
else
:image_updated
end

TimelineEntry.create_for_revision(entry_type: timeline_entry_type, edition: edition)
end

def update_context(attributes)
attributes.each { |k, v| context[k.to_sym] = v }
end
end
Loading

0 comments on commit 4802ab2

Please sign in to comment.