diff --git a/Gemfile b/Gemfile index ef23b7c8a..7062b33f2 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'coffee-rails', '~> 4.1.0' # Use jquery as the JavaScript library gem 'jquery-rails' +gem 'jquery-ui-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder @@ -53,6 +54,7 @@ gem 'hydra-works', github: 'projecthydra-labs/hydra-works', branch: 'master' gem 'hydra-pcdm', github: 'projecthydra-labs/hydra-pcdm', branch: 'master' gem 'activefedora-aggregation', github: 'projecthydra-labs/activefedora-aggregation', branch: 'master' gem 'hydra-derivatives', github: 'projecthydra/hydra-derivatives', branch: 'master' +gem 'active-fedora', github: 'projecthydra/active_fedora', branch: 'master' gem 'rubocop', require: false gem 'rubocop-rspec', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c153c005e..83da2f4f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,12 @@ GIT remote: git://github.com/projecthydra-labs/activefedora-aggregation.git - revision: fc28a063e9aa475cfaf78f891e0233afb9d8153c + revision: 00d46792af0fa887da59dd9581f3ecf6b5b822e6 branch: master specs: - activefedora-aggregation (0.5.0) + activefedora-aggregation (0.6.0) active-fedora (~> 9.5) activesupport - rdf (~> 1.1.16.0) + rdf (~> 1.1, >= 1.1.17.1) rdf-vocab (~> 0.8.1) GIT @@ -52,6 +52,22 @@ GIT hydra-file_characterization (~> 0.3, >= 0.3.3) hydra-pcdm (~> 0.3) +GIT + remote: git://github.com/projecthydra/active_fedora.git + revision: 608140a8ee2bf9a74fb89446558fbeb765f0dfca + branch: master + specs: + active-fedora (9.6.1) + active-triples (~> 0.7.1) + activesupport (>= 4.1.0) + deprecation + ldp (~> 0.4.0) + linkeddata + nom-xml (>= 0.5.1) + om (~> 3.1) + rdf-rdfxml (~> 1.1) + rsolr (~> 1.0.10) + GIT remote: git://github.com/projecthydra/hydra-collections.git revision: c8abfb472265e45f852b358b769f3d8cb7af9979 @@ -108,16 +124,6 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - active-fedora (9.5.0) - active-triples (~> 0.7.1) - activesupport (>= 4.1.0) - deprecation - ldp (~> 0.4.0) - linkeddata - nom-xml (>= 0.5.1) - om (~> 3.1) - rdf-rdfxml (~> 1.1.0) - rsolr (~> 1.0.10) active-triples (0.7.2) activemodel (>= 3.0.0) activesupport (>= 3.0.0) @@ -257,7 +263,7 @@ GEM docile (1.1.5) domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) - ebnf (0.3.9) + ebnf (1.0.0) rdf (~> 1.1) sxp (~> 0.1, >= 0.1.3) equivalent-xml (0.6.0) @@ -331,9 +337,9 @@ GEM jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) - json-ld (1.1.11) + json-ld (1.99.0) multi_json (~> 1.11) - rdf (~> 1.1, >= 1.1.7) + rdf (~> 1.99) kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -345,27 +351,27 @@ GEM linkeddata (>= 1.1) slop link_header (0.0.8) - linkeddata (1.1.11) - equivalent-xml (~> 0.5) - json-ld (~> 1.1, >= 1.1.8) + linkeddata (1.99.0) + equivalent-xml (~> 0.6) + json-ld (~> 1.99) nokogiri (~> 1.6) - rdf (~> 1.1, >= 1.1.11) - rdf-aggregate-repo (~> 1.1) - rdf-isomorphic (~> 1.1) + rdf (~> 1.99) + rdf-aggregate-repo (~> 1.99) + rdf-isomorphic (~> 1.99) rdf-json (~> 1.1, >= 1.1.2) - rdf-microdata (~> 2.0, >= 2.0.1) - rdf-n3 (~> 1.1, >= 1.1.3) - rdf-rdfa (~> 1.1, >= 1.1.6) - rdf-rdfxml (~> 1.1, >= 1.1.4) - rdf-reasoner (~> 0.2, >= 0.2.1) - rdf-tabular (~> 0.1, >= 0.1.2) - rdf-trig (~> 1.1, >= 1.1.4) - rdf-trix (~> 1.1) - rdf-turtle (~> 1.1, >= 1.1.6) - rdf-vocab (~> 0.8) - rdf-xsd (~> 1.1, >= 1.1.3) - sparql (~> 1.1, >= 1.1.5) - sparql-client (~> 1.1, >= 1.1.3) + rdf-microdata (~> 2.0, >= 2.0.2) + rdf-n3 (~> 1.99) + rdf-rdfa (~> 1.99) + rdf-rdfxml (~> 1.1, >= 1.1.5) + rdf-reasoner (~> 0.3) + rdf-tabular (~> 0.3) + rdf-trig (~> 1.99, >= 1.99.0.1) + rdf-trix (~> 1.99) + rdf-turtle (~> 1.99) + rdf-vocab (~> 0.8, >= 0.8.6) + rdf-xsd (~> 1.1, >= 1.1.5) + sparql (~> 1.99) + sparql-client (~> 1.99) logger (1.2.8) loofah (2.0.3) nokogiri (>= 1.5.9) @@ -378,7 +384,7 @@ GEM mime-types (2.6.2) mini_magick (4.3.6) mini_portile (0.6.2) - minitest (5.8.1) + minitest (5.8.2) modernizr-rails (2.7.1) mono_logger (1.1.0) multi_json (1.11.2) @@ -465,55 +471,56 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.0.0) rake (10.4.2) - rdf (1.1.16.1) + rdf (1.99.0) link_header (~> 0.0, >= 0.0.8) - rdf-aggregate-repo (1.1.0) - rdf (>= 1.1) - rdf-isomorphic (1.1.0.1) - rdf (~> 1.1, >= 1.1.16.1) + rdf-aggregate-repo (1.99.0) + rdf (~> 1.99) + rdf-isomorphic (1.99.0) + rdf (~> 1.99) rdf-json (1.1.2) rdf (~> 1.1) - rdf-microdata (2.0.1) + rdf-microdata (2.0.2) htmlentities (~> 4.3) nokogiri (~> 1.6) rdf (~> 1.1) rdf-xsd (~> 1.1) - rdf-n3 (1.1.3) - rdf (~> 1.1, >= 1.1.5) - rdf-rdfa (1.1.6) + rdf-n3 (1.99.0) + rdf (~> 1.99) + rdf-rdfa (1.99.0) haml (~> 4.0) htmlentities (~> 4.3) - rdf (~> 1.1, >= 1.1.6) + rdf (~> 1.99) rdf-aggregate-repo (~> 1.1) rdf-xsd (~> 1.1) - rdf-rdfxml (1.1.4) + rdf-rdfxml (1.1.5) htmlentities (~> 4.3) rdf (~> 1.1, >= 1.1.6) rdf-rdfa (~> 1.1, >= 1.1.4.1) rdf-xsd (~> 1.1) - rdf-reasoner (0.2.2) + rdf-reasoner (0.3.0) rdf (~> 1.1, >= 1.1.4.2) rdf-turtle (~> 1.1) rdf-vocab (~> 0.8) rdf-xsd (~> 1.1) - rdf-tabular (0.2.1) + rdf-tabular (0.3.0) addressable (~> 2.3) bcp47 (~> 0.3, >= 0.3.3) json-ld (~> 1.1) rdf (~> 1.1, >= 1.1.7) + rdf-vocab (~> 0.8) rdf-xsd (~> 1.1) - rdf-trig (1.1.5) - ebnf (~> 0.3, >= 0.3.7) - rdf (~> 1.1, >= 1.1.11) - rdf-turtle (~> 1.1, >= 1.1.7) - rdf-trix (1.1.0) - rdf (>= 1.1) - rdf-turtle (1.1.8) - ebnf (~> 0.3, >= 0.3.6) - rdf (~> 1.1, >= 1.1.10) + rdf-trig (1.99.0.1) + ebnf (~> 1.0) + rdf (~> 1.99) + rdf-turtle (~> 1.99) + rdf-trix (1.99.0) + rdf (~> 1.1) + rdf-turtle (1.99.0) + ebnf (~> 1.0) + rdf (~> 1.99) rdf-vocab (0.8.6) rdf (~> 1.1, >= 1.1.10) - rdf-xsd (1.1.4) + rdf-xsd (1.1.5) rdf (~> 1.1, >= 1.1.9) rdoc (4.2.0) redis (3.2.1) @@ -601,15 +608,15 @@ GEM nokogiri stomp xml-simple - sparql (1.1.9) + sparql (1.99.0) builder (~> 3.2) - ebnf (~> 0.3, >= 0.3.9) - rdf (~> 1.1, >= 1.1.13) - rdf-aggregate-repo (~> 1.1, >= 1.1.0) + ebnf (~> 1.0) + rdf (~> 1.99) + rdf-aggregate-repo (~> 1.99) rdf-xsd (~> 1.1) sparql-client (~> 1.1) sxp (~> 0.1) - sparql-client (1.1.6) + sparql-client (1.99.0) net-http-persistent (~> 2.9) rdf (~> 1.1) spring (1.4.0) @@ -667,6 +674,7 @@ PLATFORMS ruby DEPENDENCIES + active-fedora! activefedora-aggregation! capistrano-passenger capistrano-rails @@ -688,6 +696,7 @@ DEPENDENCIES jbuilder (~> 2.0) jettywrapper jquery-rails + jquery-ui-rails launchy modernizr-rails omniauth-cas diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6137ba247..a06673f34 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,6 +12,7 @@ // //= require jquery //= require jquery_ujs +//= require jquery-ui/sortable //= require turbolinks// // Required by Blacklight //= require blacklight/blacklight diff --git a/app/assets/javascripts/sortable.js.coffee b/app/assets/javascripts/sortable.js.coffee new file mode 100644 index 000000000..3b29ca79f --- /dev/null +++ b/app/assets/javascripts/sortable.js.coffee @@ -0,0 +1,71 @@ +jQuery -> + window.sort_manager = new SortManager +class Flash + constructor: -> + @element = $("*[data-reorder-action='flash']") + @element.children(".close").click => + @element.hide() + reset_type: -> + @element.removeClass("alert-success") + @element.removeClass("alert-danger") + set: (type, message) -> + this.reset_type() + @element.addClass("alert-#{type}") + @element.children(".text").text(message) + @element.show() + @element.removeClass("hidden") +class SortManager + constructor: -> + @element = $("#sortable") + @sorting_info = {} + @sorting_actions = [] + this.initialize_sort() + this.initialize_persist_button() + this.initialize_flash() + initialize_sort: -> + @element.sortable() + @element.disableSelection() + @element.on("sortstop", this.stopped_sorting) + @element.on("sortstart", this.started_sorting) + initialize_persist_button: -> + @persist_button = $("*[data-reorder-action='persist']") + @persist_button.click(this.persist_ordering) + initialize_flash: -> + @flash = new Flash + started_sorting: (event, ui) => + @sorting_element = $(ui.item) + @sorting_info.start = this.get_sort_position(ui.item) + window.sorting_element = $(ui.item) + stopped_sorting: (event, ui) => + @sorting_info.end = this.get_sort_position($(ui.item)) + return if @sorting_info.end == @sorting_info.start + @persist_button.removeClass("disabled") + get_sort_position: (item) -> + @element.children().index(item) + get_order: -> + $("*[data-reorder-id]").map(-> + $(this).data("reorder-id") + ).toArray() + persist_ordering: (event) => + event.preventDefault() + return if @persisting == true + @persisting = true + console.log("Persisting Reordering") + @persist_button.addClass("disabled") + @persist_button.data("old-text", @persist_button.text()) + @persist_button.text("Saving...") + order = this.get_order() + $.post("/concern/scanned_resources/#{@element.data("id")}/reorder.json", {order: order}).done( (data) => + @sorting_actions = [] + if data.message? + @flash.set('success', data.message) + ).always( => + @persist_button.text(@persist_button.data("old-text")) + @persisting = false + ).fail( (data, message) => + if data?.message? + @flash.set('danger', data.message) + else + @flash.set('danger', "Unable to save new order.") + @persist_button.removeClass("disabled") + ) diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 41a30852d..0c84b767b 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -31,3 +31,4 @@ @import 'components/positioning'; @import "components/viewer"; +@import "jquery-ui/sortable" diff --git a/app/controllers/curation_concerns/scanned_resources_controller.rb b/app/controllers/curation_concerns/scanned_resources_controller.rb index b82d86519..ef12080e6 100644 --- a/app/controllers/curation_concerns/scanned_resources_controller.rb +++ b/app/controllers/curation_concerns/scanned_resources_controller.rb @@ -4,7 +4,7 @@ class CurationConcerns::ScannedResourcesController < CurationConcerns::CurationConcernsController include CurationConcerns::ParentContainer set_curation_concern_type ScannedResource - skip_load_and_authorize_resource only: [:show, :manifest] + skip_load_and_authorize_resource only: [:show, :manifest, :reorder] def create super @@ -24,4 +24,29 @@ def pdf actor.generate_pdf redirect_to main_app.download_path(curation_concern, file: 'pdf') end + + def reorder + @members = presenter.file_presenters + end + + def save_order + lock_manager.lock(curation_concern.id) do + form = ReorderForm.new(curation_concern) + form.order = params[:order] + if form.save + render json: { message: "Succesfully updated order." } + else + render json: { message: form.errors.full_messages.to_sentence }, status: :bad_request + end + end + end + + private + + def lock_manager + @lock_manager ||= CurationConcerns::LockManager.new( + CurationConcerns.config.lock_time_to_live, + CurationConcerns.config.lock_retry_count, + CurationConcerns.config.lock_retry_delay) + end end diff --git a/app/forms/reorder_form.rb b/app/forms/reorder_form.rb new file mode 100644 index 000000000..105b75fa7 --- /dev/null +++ b/app/forms/reorder_form.rb @@ -0,0 +1,32 @@ +class ReorderForm + include ActiveModel::Model + attr_accessor :order + attr_reader :curation_concern + validates_with OrderLengthValidator + + def initialize(curation_concern) + @curation_concern = curation_concern + @order = curation_concern.ordered_member_ids + end + + def save + if valid? + apply_order && curation_concern.save + else + false + end + end + + def proxy_length + curation_concern.ordered_member_ids.length + end + + private + + def apply_order + curation_concern.ordered_member_proxies.each_with_index do |proxy, index| + proxy.proxy_for = ActiveFedora::Base.id_to_uri(order[index]) + end + curation_concern.list_source.order_will_change! + end +end diff --git a/app/validators/order_length_validator.rb b/app/validators/order_length_validator.rb new file mode 100644 index 000000000..5f735a817 --- /dev/null +++ b/app/validators/order_length_validator.rb @@ -0,0 +1,15 @@ +class OrderLengthValidator < ActiveModel::Validator + def validate(record) + length_validator(record.proxy_length).validate(record) + end + + private + + def length_validator(length) + @length_validator ||= ActiveModel::Validations::LengthValidator.new( + attributes: [:order], + is: length, + message: "given has the wrong number of elements (should be #{length})" + ) + end +end diff --git a/app/views/curation_concerns/scanned_resources/_member.html.erb b/app/views/curation_concerns/scanned_resources/_member.html.erb new file mode 100644 index 000000000..1850fb303 --- /dev/null +++ b/app/views/curation_concerns/scanned_resources/_member.html.erb @@ -0,0 +1,8 @@ +
  • +
    +
    <%= member.to_s %>
    +
    + <%= render_thumbnail_tag member %> +
    +
    +
  • diff --git a/app/views/curation_concerns/scanned_resources/reorder.html.erb b/app/views/curation_concerns/scanned_resources/reorder.html.erb new file mode 100644 index 000000000..4d0d06828 --- /dev/null +++ b/app/views/curation_concerns/scanned_resources/reorder.html.erb @@ -0,0 +1,24 @@ +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    diff --git a/config/routes.rb b/config/routes.rb index ced7d8b3d..0aa726d60 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,10 @@ get '/concern/scanned_resources/:id/manifest', to: 'curation_concerns/scanned_resources#manifest', as: 'curation_concerns_scanned_resource_manifest', defaults: { format: :json } get '/concern/scanned_resources/:id/pdf', to: 'curation_concerns/scanned_resources#pdf', as: 'curation_concerns_scanned_resource_pdf' + namespace :curation_concerns, path: :concern do + get '/scanned_resources/:id/reorder', to: 'scanned_resources#reorder', as: 'curation_concerns_scanned_resource_reorder' + post '/scanned_resources/:id/reorder', to: 'scanned_resources#save_order' + end namespace :curation_concerns, path: :concern do resources :scanned_resources, only: [:new, :create], path: 'container/:parent_id/scanned_resources', as: 'member_scanned_resource' diff --git a/spec/controllers/curation_concerns/scanned_resources_controller_spec.rb b/spec/controllers/curation_concerns/scanned_resources_controller_spec.rb index b0666ce1d..1ebc59097 100644 --- a/spec/controllers/curation_concerns/scanned_resources_controller_spec.rb +++ b/spec/controllers/curation_concerns/scanned_resources_controller_spec.rb @@ -55,8 +55,12 @@ describe "#manifest" do let(:solr) { ActiveFedora.solr.conn } + let(:user) { FactoryGirl.create(:user) } context "when requesting JSON" do render_views + before do + sign_in user + end it "builds a manifest" do resource = FactoryGirl.build(:scanned_resource) resource_2 = FactoryGirl.build(:scanned_resource) @@ -128,4 +132,52 @@ expect(response).to redirect_to(Rails.application.class.routes.url_helpers.download_path(scanned_resource, file: 'pdf')) end end + + describe "reorder" do + let(:resource) { FactoryGirl.create(:scanned_resource) } + let(:member) { FactoryGirl.create(:file_set) } + before do + 3.times { resource.ordered_members << member } + resource.save + sign_in user + get :reorder, id: resource.id + end + + it "finds all proxies for the resource" do + expect(assigns(:members).map(&:id)).to eq resource.ordered_member_ids + end + end + + describe "#save_order" do + let(:resource) { FactoryGirl.create(:scanned_resource, user: user) } + let(:member) { FactoryGirl.create(:file_set, user: user) } + let(:member_2) { FactoryGirl.create(:file_set, user: user) } + let(:new_order) { resource.ordered_member_ids } + let(:user) { FactoryGirl.create(:admin) } + render_views + before do + 3.times { resource.ordered_members << member } + resource.ordered_members << member_2 + resource.save + sign_in user + post :save_order, id: resource.id, order: new_order, format: :json + end + + context "when given a new order" do + let(:new_order) { [member.id, member.id, member_2.id, member.id] } + it "applies it" do + expect(response).to be_success + expect(resource.reload.ordered_member_ids).to eq new_order + end + end + + context "when given an incomplete order" do + let(:new_order) { [member.id] } + it "fails and gives an error" do + expect(response).not_to be_success + expect(JSON.parse(response.body)["message"]).to eq "Order given has the wrong number of elements (should be 4)" + expect(response).to be_bad_request + end + end + end end diff --git a/spec/factories/file_sets.rb b/spec/factories/file_sets.rb index cb6c0cd0f..937a00f65 100644 --- a/spec/factories/file_sets.rb +++ b/spec/factories/file_sets.rb @@ -21,7 +21,7 @@ if evaluator.content Hydra::Works::UploadFileToFileSet.call(file, evaluator.content) end - FactoryGirl.create(:generic_work, user: evaluator.user).file_sets << file + FactoryGirl.create(:generic_work, user: evaluator.user).ordered_members << file end end after(:build) do |file, evaluator|