Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve previous and next members in a work #2418

Merged
merged 14 commits into from
Oct 30, 2023
3 changes: 3 additions & 0 deletions app/controllers/admin/assets_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def show

@exiftool_result = Kithe::ExiftoolCharacterization.presenter_for(@asset&.exiftool_result)

getter = MemberPreviousAndNextGetter.new(@asset)
@previous_url, @next_url = getter.previous_url, getter.next_url

if @asset.stored?
@checks = @asset.fixity_checks.order('created_at asc')
@latest_check = @checks.last
Expand Down
53 changes: 53 additions & 0 deletions app/services/member_previous_and_next_getter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Finds the next and previous members in a work.
# In cases where positions are noncontiguous, non-unique, or nil,
# you can still navigate through the members sequentially.
class MemberPreviousAndNextGetter
# @param member [Kithe::Model]
def initialize(member)
@member = member
end

def next_url
@next_url ||= url(:next)
end

def previous_url
@previous_url ||= url(:previous)
end

private

def url(previous_or_next)
return nil unless @member.parent.present?
if (neighbor_id = query["#{previous_or_next}_friendlier_id"])
# ok, we have a neighbor. construct the right kind of link to it.
neighbor_type = query["#{previous_or_next}_type"]
if neighbor_type == 'Work'
Rails.application.routes.url_helpers.admin_work_path(neighbor_id)
elsif neighbor_type == 'Asset'
Rails.application.routes.url_helpers.admin_asset_path(neighbor_id)
end
end
end

# Takes less than a millisecond.
def query
@query ||= ActiveRecord::Base.connection.execute("""
SELECT * FROM (
SELECT
id,
LAG (friendlier_id, 1) over (order by position, id) AS previous_friendlier_id,
LAG (type, 1) over (order by position, id) AS previous_type,
LEAD (friendlier_id,1) over (order by position, id) AS next_friendlier_id,
LEAD (type,1) over (order by position, id) AS next_type
FROM (
SELECT id, friendlier_id, type, position
FROM kithe_models
WHERE parent_id = '#{@member.parent.id}'
) AS members
) AS all_members
WHERE id = '#{@member.id}';
""".squish)[0]
end
end

17 changes: 17 additions & 0 deletions app/views/admin/assets/_prev_next.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<p>

<% if @previous_url %>
<%= link_to "« Previous", @previous_url %>
<% else %>
<%= link_to "« Previous", "", class: "btn-link disabled" %>
<% end %>

|

<% if @next_url %>
<%= link_to "Next »", @next_url %>
<% else %>
<%= link_to "Next »", "", class: "btn-link disabled" %>
<% end %>

</p>
1 change: 1 addition & 0 deletions app/views/admin/assets/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<p class="text-danger">NO PARENT</p>
<% end %>

<%= render 'admin/assets/prev_next' %>

<p>
<%= link_to "Edit", edit_path(@asset), class: "btn btn-primary #{"disabled" unless can?(:update, @asset)}"%>
Expand Down
80 changes: 80 additions & 0 deletions spec/services/member_previous_and_next_getter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
require 'rails_helper'

describe MemberPreviousAndNextGetter, type: :model do
let(:members) { parent_work.members.order(:position, :id) }

let(:actual_urls) do
members.map do |m|
getter = MemberPreviousAndNextGetter.new(m)
[ getter&.previous_url, getter&.next_url ]
end
end

let(:wrk_prefix) { "/admin/works/" }
let(:ast_prefix) { "/admin/asset_files/" }

describe "navigation between members of a parent work" do
context "four assets and one child work" do
let(:parent_work) { FactoryBot.create(:work, :with_assets, asset_count: 4) }
let!(:child_work) { FactoryBot.create(:work, :with_assets, parent: parent_work, position: 5) }
it "finds previous and next members correctly" do
expect(actual_urls).to eq ([
[ nil,
"#{ast_prefix}#{members[1].friendlier_id}"],

[ "#{ast_prefix}#{members[0].friendlier_id}",
"#{ast_prefix}#{members[2].friendlier_id}"],

[ "#{ast_prefix}#{members[1].friendlier_id}",
"#{ast_prefix}#{members[3].friendlier_id}"],

[ "#{ast_prefix}#{members[2].friendlier_id}",
"#{wrk_prefix}#{members[4].friendlier_id}"],

[ "#{ast_prefix}#{members[3].friendlier_id}",
nil]
])
end
end

context "non-consecutive, duplicate and nil positions" do
let (:basic_uuid) {'00000000-0000-4000-8000-%012x'}
let!(:parent_work) { FactoryBot.create(:work, members: [
FactoryBot.create(:asset, id: basic_uuid % 1, position: nil),
FactoryBot.create(:asset, id: basic_uuid % 2, position: 7 ),
FactoryBot.create(:asset, id: basic_uuid % 3, position: 29 ),
FactoryBot.create(:work, id: basic_uuid % 4, position: nil),
FactoryBot.create(:work, id: basic_uuid % 5, position: 7 )
] )
}
it "finds previous and next members correctly" do
expect(actual_urls).to eq ([
[ nil,
"#{wrk_prefix}#{members[1].friendlier_id}"],

[ "#{ast_prefix}#{members[0].friendlier_id}",
"#{ast_prefix}#{members[2].friendlier_id}"],

[ "#{wrk_prefix}#{members[1].friendlier_id}",
"#{ast_prefix}#{members[3].friendlier_id}"],

[ "#{ast_prefix}#{members[2].friendlier_id}",
"#{wrk_prefix}#{members[4].friendlier_id}"],

[ "#{ast_prefix}#{members[3].friendlier_id}",
nil]
])
end
end

context "parent_work is absent" do
let(:members) {[
FactoryBot.create(:asset),
FactoryBot.create(:asset),
]}
it "doesn't throw an error" do
expect(actual_urls).to eq([[nil, nil], [nil, nil]])
end
end
end
end