diff --git a/.rspec b/.rspec index ffebb7f177..8c18f1abdd 100644 --- a/.rspec +++ b/.rspec @@ -1,4 +1,2 @@ --format documentation ---format RspecJunitFormatter --color ---out rspec.xml diff --git a/.travis.yml b/.travis.yml index 13e36d173a..84d5c8fd0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ git: submodules: false bundler_args: --without development debug before_install: + - sudo apt-get update - sudo apt-get install mediainfo - sudo ln -s /usr/bin/lsof /usr/sbin/lsof - cp spec/config/*.yml config @@ -12,9 +13,14 @@ before_install: before_script: - bundle exec rake jetty:unzip jetty:config jetty:start delayed_job:start - bundle exec rake db:migrate + - bundle exec rake avalon:db_migrate notifications: - irc: "chat.freenode.net#projectvov" + irc: "chat.freenode.net#projectvov-updates" language: ruby rvm: - - 2.0.0 - - 2.1.5 + - 2.1.9 + - 2.2.5 + - 2.3.1 +addons: + code_climate: + repo_token: 1fb78f221b36e5615428f2ada12950b39a3b702b23fdd41e1b980dc4b47d0233 diff --git a/Gemfile b/Gemfile index 1c84a426ad..f3eb36f29f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,36 +1,51 @@ source 'http://rubygems.org' + # active anno dev + gem 'active_annotations', git: 'https://github.com/avalonmediasystem/active_annotations.git', tag: 'avalon-r5' + gem 'iconv' gem 'rails', '~>4.0.3' gem 'sprockets', '~>2.11.0' #gem 'protected_attributes' gem 'builder', '~>3.1.0' - - gem 'hydra', '~>8.0' - gem 'hydra-access-controls', git: 'https://github.com/projecthydra/hydra-head.git', branch: '8-1-stable' + gem 'rake', '~>10.4' + +# gem 'hydra', '~>8.0' + gem 'hydra-head', git: 'https://github.com/avalonmediasystem/hydra-head.git', branch: '8-1-stable' + gem 'active-fedora', '~> 8.1.0' + gem 'om', '~> 3.1.0' + gem 'solrizer', '~> 3.3.0' + gem 'rsolr', '~> 1.0.12' + gem 'blacklight', '~> 5.10' + gem 'nokogiri', '~> 1.6.5' + gem 'rubydora', '~> 1.8.1' + gem 'nom-xml', '~> 0.5.2' gem 'activerecord-session_store' gem 'bcrypt-ruby', '~> 3.1.0' gem 'kaminari', '~> 0.15.0' gem 'avalon-workflow', git: 'https://github.com/avalonmediasystem/avalon-workflow.git', tag: 'avalon-r4' - gem 'mediaelement_rails', git: 'https://github.com/avalonmediasystem/mediaelement_rails.git', tag: 'avalon-r4' + gem 'mediaelement_rails', git: 'https://github.com/avalonmediasystem/mediaelement_rails.git', tag: 'avalon-r5' gem 'mediaelement-qualityselector', git:'https://github.com/avalonmediasystem/mediaelement-qualityselector.git', tag: 'avalon-r4' gem 'media_element_thumbnail_selector', git: 'https://github.com/avalonmediasystem/media-element-thumbnail-selector', tag: 'avalon-r4' - gem 'mediaelement-skin-avalon', git:'https://github.com/avalonmediasystem/mediaelement-skin-avalon.git', tag: 'avalon-r4' + gem 'mediaelement-skin-avalon', git:'https://github.com/avalonmediasystem/mediaelement-skin-avalon.git', tag: 'avalon-r5' gem 'mediaelement-title', git:'https://github.com/avalonmediasystem/mediaelement-title.git', tag: 'avalon-r4' gem 'mediaelement-hd-toggle', git:'https://github.com/avalonmediasystem/mediaelement-hd-toggle.git', tag: 'avalon-r4' gem 'media-element-logo-plugin' + gem 'media_element_add_to_playlist', git: 'https://github.com/avalonmediasystem/media-element-add-to-playlist.git', tag: 'avalon-r5' gem 'browse-everything', '0.6.3' - + gem 'roo', git: 'https://github.com/Empact/roo', ref: '9e1b969762cbb80b1c52cfddd848e489f22f468f' - gem 'multipart-post' + gem 'multipart-post' gem 'modal_logic' - + gem 'rubyzip', '0.9.9' gem 'hooks' + gem 'addressable' + gem 'acts_as_list' # microdata gem 'ruby-duration' @@ -41,7 +56,7 @@ # gem 'zoom', '~>0.4.1', :git => 'https://github.com/bricestacey/ruby-zoom.git' gem 'marc' - + platforms :jruby do gem 'jruby-openssl' gem 'activerecord-jdbcsqlite3-adapter' @@ -63,8 +78,8 @@ #gem 'devise-guests' gem 'haml' - gem 'active-encode', git: "https://github.com/projecthydra-labs/active-encode.git" - gem 'rubyhorn', git: "https://github.com/avalonmediasystem/rubyhorn.git" + gem 'active_encode', git: "https://github.com/projecthydra-labs/active_encode.git", tag: 'v0.0.3' + gem 'rubyhorn', git: "https://github.com/avalonmediasystem/rubyhorn.git", tag: 'avalon-r5' gem 'validates_email_format_of' gem 'loofah' gem 'omniauth-identity' @@ -79,6 +94,8 @@ gem 'equivalent-xml' gem 'net-ldap' + gem 'api-pagination' + group :assets, :production do gem 'coffee-rails' gem 'uglifier', '>= 1.0.3' @@ -94,7 +111,8 @@ gem 'font-awesome-rails', '~> 4.3' gem 'bootstrap_form' gem 'handlebars_assets' - gem 'twitter-typeahead-rails', '=0.10.5' + #gem 'twitter-typeahead-rails', '~>0.11.1' + gem 'twitter-typeahead-rails', '= 0.11.1.pre.corejavascript' end group :development do @@ -113,7 +131,7 @@ gem 'felixwrapper', git: "https://github.com/avalonmediasystem/felixwrapper.git", tag: 'avalon-r4' gem 'red5wrapper', git: "https://github.com/avalonmediasystem/red5wrapper.git", tag: 'avalon-r4' gem 'daemons' - gem 'rspec-rails', '~>2.9' + gem 'rspec-rails' gem 'puma' gem 'rb-fsevent', '~> 0.9.1' gem 'letter_opener' @@ -131,8 +149,6 @@ gem 'factory_girl_rails' gem 'mime-types', ">=1.1" gem "headless" - gem "rspec_junit_formatter" - gem 'rspec-its' gem 'simplecov' gem 'email_spec' gem 'capybara' @@ -141,6 +157,7 @@ gem 'fakefs', require: "fakefs/safe" gem 'fakeweb' gem 'hashdiff' + gem 'coveralls' end extra_gems = File.expand_path("../Gemfile.local",__FILE__) diff --git a/Gemfile.lock b/Gemfile.lock index 268aa9b979..70fdfcb861 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,6 +17,15 @@ GIT rails (>= 3.2) rugged +GIT + remote: https://github.com/avalonmediasystem/active_annotations.git + revision: 63c683fdf9f8d629116a28b9e8439b7e2269c265 + tag: avalon-r5 + specs: + active_annotations (0.1.0) + json-ld + rdf-vocab + GIT remote: https://github.com/avalonmediasystem/avalon-about.git revision: 4bb6dda8426bfdad0969bbadb8f921c633d1cda0 @@ -53,6 +62,37 @@ GIT logger mediashelf-loggable +GIT + remote: https://github.com/avalonmediasystem/hydra-head.git + revision: 95d4a526ba73e449829f221565c3186f93c375b9 + branch: 8-1-stable + specs: + hydra-access-controls (8.1.0) + active-fedora (~> 8.0) + activesupport (~> 4.0) + blacklight (~> 5.10) + cancancan (~> 1.8) + deprecation (~> 0.1) + om (~> 3.0, >= 3.0.7) + sass-rails + hydra-core (8.1.0) + block_helpers + hydra-access-controls (= 8.1.0) + jettywrapper (~> 1.5) + rails (~> 4.0) + hydra-head (8.1.0) + hydra-access-controls (= 8.1.0) + hydra-core (= 8.1.0) + rails (~> 4.0) + +GIT + remote: https://github.com/avalonmediasystem/media-element-add-to-playlist.git + revision: d5a20bc90c93bda2377f2d53e3a3fefb3e7ba4b0 + tag: avalon-r5 + specs: + media_element_add_to_playlist (0.0.1) + rails (~> 4.0) + GIT remote: https://github.com/avalonmediasystem/media-element-thumbnail-selector revision: b919ad61525cd608f42983846c87f48212e11646 @@ -77,8 +117,8 @@ GIT GIT remote: https://github.com/avalonmediasystem/mediaelement-skin-avalon.git - revision: 0a1ac038135d107c700805e905be061b4c7ae0a3 - tag: avalon-r4 + revision: a564874595672d5f751a83ae43685728c247f49e + tag: avalon-r5 specs: mediaelement-skin-avalon (0.0.1) @@ -91,8 +131,8 @@ GIT GIT remote: https://github.com/avalonmediasystem/mediaelement_rails.git - revision: 8df94f1ec5fb8011e2ba307b50d4d02f0d2226b9 - tag: avalon-r4 + revision: 8bb5ddad7abd2249ecb0f2891903728fc627e04b + tag: avalon-r5 specs: mediaelement_rails (0.5.1) jquery-rails (>= 1.0) @@ -122,6 +162,7 @@ GIT GIT remote: https://github.com/avalonmediasystem/rubyhorn.git revision: 36f79674d16c555bda0415f34b5d3dabe8885e19 + tag: avalon-r5 specs: rubyhorn (0.0.6) activesupport @@ -133,36 +174,13 @@ GIT rest-client GIT - remote: https://github.com/projecthydra-labs/active-encode.git - revision: fa02be775b5a204f89587437e5473445b78bf3eb + remote: https://github.com/projecthydra-labs/active_encode.git + revision: 5e68c05fd4023d8bc6655d9a65ae709553c7ab8f + tag: v0.0.3 specs: - active-encode (0.0.1) - activemodel + active_encode (0.0.2) activesupport -GIT - remote: https://github.com/projecthydra/hydra-head.git - revision: 723d08f6773fec4e7e6a3b353ce4695d2638204f - branch: 8-1-stable - specs: - hydra-access-controls (8.1.0) - active-fedora (~> 8.0.0) - activesupport (~> 4.0) - blacklight (~> 5.10) - cancancan (~> 1.8) - deprecation (~> 0.1) - om (~> 3.0, >= 3.0.7) - sass-rails - hydra-core (8.1.0) - block_helpers - hydra-access-controls (= 8.1.0) - jettywrapper (~> 1.5) - rails (~> 4.0) - hydra-head (8.1.0) - hydra-access-controls (= 8.1.0) - hydra-core (= 8.1.0) - rails (~> 4.0) - GEM remote: http://rubygems.org/ specs: @@ -175,14 +193,14 @@ GEM erubis (~> 2.7.0) rack (~> 1.5.2) rack-test (~> 0.6.2) - active-fedora (8.0.1) + active-fedora (8.1.0) active-triples (~> 0.4.0) activesupport (>= 3.0.0) deprecation nom-xml (>= 0.5.1) om (~> 3.1) rdf-rdfxml (~> 1.1.0) - rsolr (~> 1.0.10) + rsolr (~> 1.0.11) rubydora (~> 1.8) active-triples (0.4.0) activemodel (>= 3.0.0) @@ -209,15 +227,17 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) + acts_as_list (0.7.4) + activerecord (>= 3.0) addressable (2.3.7) + api-pagination (4.1.1) arel (4.0.2) autoparse (0.3.3) addressable (>= 2.3.1) extlib (>= 0.9.15) multi_json (>= 1.0.0) - autoprefixer-rails (5.1.7) + autoprefixer-rails (6.3.6) execjs - json bcrypt (3.1.10) bcrypt-ruby (3.1.5) bcrypt (>= 3.1.3) @@ -227,10 +247,10 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - blacklight (5.10.3) + blacklight (5.18.0) bootstrap-sass (~> 3.2) deprecation - kaminari (~> 0.13) + kaminari (>= 0.15) nokogiri (~> 1.6) rails (>= 3.2.6, < 5) rsolr (~> 1.0.11) @@ -256,7 +276,7 @@ GEM debugger-linecache (~> 1.2) slop (~> 3.6) callsite (0.0.11) - cancancan (1.10.1) + cancancan (1.13.1) capistrano (2.12.0) highline net-scp (>= 1.0.0) @@ -290,6 +310,12 @@ GEM compass (>= 0.12.2) compass-susy-plugin (0.9) compass (>= 0.11.1) + coveralls (0.8.0) + multi_json (~> 1.10) + rest-client (>= 1.6.8, < 2) + simplecov (~> 0.9.1) + term-ansicolor (~> 1.3) + thor (~> 0.19.1) daemons (1.2.3) debug_inspector (0.0.2) debugger (1.6.8) @@ -318,8 +344,7 @@ GEM dot-properties (0.1.3) dropbox-sdk (1.6.4) json - ebnf (0.3.7) - haml (~> 4.0) + ebnf (1.0.0) rdf (~> 1.1) sxp (~> 0.1, >= 0.1.3) edtf (2.1.0) @@ -330,7 +355,7 @@ GEM equivalent-xml (0.5.1) nokogiri (>= 1.4.3) erubis (2.7.0) - execjs (2.3.0) + execjs (2.6.0) extlib (0.9.16) factory_girl (4.5.0) activesupport (>= 3.0.0) @@ -376,7 +401,7 @@ GEM hike (1.2.3) hooks (0.3.6) uber (~> 0.0.4) - htmlentities (4.3.3) + htmlentities (4.3.4) httmultiparty (0.3.16) httparty (>= 0.7.3) mimemagic @@ -386,18 +411,6 @@ GEM httparty (0.13.3) json (~> 1.8) multi_xml (>= 0.5.2) - hydra (8.0.0) - active-fedora (~> 8.0.1) - blacklight (~> 5.10.0) - hydra-head (~> 8.1.0) - jettywrapper (~> 1.8.3) - nokogiri (~> 1.6.5) - nom-xml (~> 0.5.2) - om (~> 3.1.0) - rails (>= 3.2.15, < 5.0) - rsolr (~> 1.0.12) - rubydora (~> 1.8.1) - solrizer (~> 3.3.0) i18n (0.7.0) iconv (1.0.4) ims-lti (1.1.8) @@ -415,8 +428,9 @@ GEM jquery-ui-rails (5.0.0) railties (>= 3.2.16) json (1.8.3) - json-ld (1.1.8) - rdf (~> 1.1, >= 1.1.7) + json-ld (1.1.11.1) + multi_json (~> 1.11) + rdf (~> 1.1, >= 1.1.17, < 1.99) jwt (1.3.0) kaminari (0.15.1) actionpack (>= 3.0.0) @@ -429,29 +443,28 @@ GEM license_header (0.0.4) highline link_header (0.0.8) - linkeddata (1.1.2) - equivalent-xml (~> 0.4) - json-ld (~> 1.1, >= 1.1.7) - nokogiri (~> 1.6) - rdf (~> 1.1, >= 1.1.7) - rdf-aggregate-repo (~> 1.1) - rdf-isomorphic (~> 1.1) - rdf-json (~> 1.1) - rdf-microdata (~> 2.0) - rdf-n3 (~> 1.1) - rdf-rdfa (~> 1.1, >= 1.1.5) - rdf-rdfxml (~> 1.1, >= 1.1.3) - rdf-reasoner (~> 0.2, >= 0.2.1) - rdf-trig (~> 1.1, >= 1.1.3) - rdf-trix (~> 1.1) - rdf-turtle (~> 1.1, >= 1.1.5) - sparql (~> 1.1, >= 1.1.4) - sparql-client (~> 1.1, >= 1.1.3) + linkeddata (1.1.1) + equivalent-xml (>= 0.4.0) + json-ld (>= 1.1.1) + nokogiri (>= 1.6.1) + rdf (>= 1.1.1) + rdf-aggregate-repo (>= 1.1.0) + rdf-isomorphic (>= 1.1.0) + rdf-json (>= 1.1.0) + rdf-microdata (>= 1.1.1) + rdf-n3 (>= 1.1.0) + rdf-rdfa (>= 1.1.1) + rdf-rdfxml (>= 1.1.0) + rdf-trig (>= 1.1.2) + rdf-trix (>= 1.1.0) + rdf-turtle (>= 1.1.2) + sparql (>= 1.1.2) + sparql-client (>= 1.1.1) logger (1.2.8) loofah (2.0.1) nokogiri (>= 1.5.9) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) marc (1.0.0) scrub_rb (>= 1.0.1, < 2) unf @@ -464,14 +477,14 @@ GEM rack-contrib (~> 1.1) railties (>= 3.0.0, < 5.0.0) method_source (0.8.2) - mime-types (2.6.1) + mime-types (2.99.1) mimemagic (0.2.1) - mini_portile (0.6.2) + mini_portile2 (2.0.0) minitest (4.7.5) modal_logic (0.0.8) handlebars_assets (= 0.14.1) rails (>= 4) - multi_json (1.11.2) + multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) net-http-digest_auth (1.4) @@ -485,8 +498,8 @@ GEM net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) netrc (0.10.3) - nokogiri (1.6.6.2) - mini_portile (~> 0.6.0) + nokogiri (1.6.7.2) + mini_portile2 (~> 2.0.0.rc2) nom-xml (0.5.2) activesupport (>= 3.2.18) i18n @@ -542,83 +555,72 @@ GEM activesupport (= 4.0.13) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.4.2) + rake (10.5.0) rb-fsevent (0.9.4) - rdf (1.1.10) + rdf (1.1.17.1) link_header (~> 0.0, >= 0.0.8) - rdf-aggregate-repo (1.1.0) - rdf (>= 1.1) - rdf-isomorphic (1.1.0) - rdf (>= 1.1) - rdf-json (1.1.1) - rdf (~> 1.1) - rdf-microdata (2.0) + rdf-aggregate-repo (1.1.0.1) + rdf (~> 1.1, < 1.99) + rdf-isomorphic (1.1.0.2) + rdf (~> 1.1, >= 1.1.17, < 1.99) + rdf-json (1.1.2.1) + rdf (~> 1.1, < 1.99) + rdf-microdata (2.0.2) htmlentities (~> 4.3) nokogiri (~> 1.6) rdf (~> 1.1) rdf-xsd (~> 1.1) - rdf-n3 (1.1.2) - rdf (~> 1.1, >= 1.1.5) - rdf-rdfa (1.1.5) + rdf-n3 (1.1.3.1) + rdf (~> 1.1, >= 1.1.17, < 1.99) + rdf-rdfa (1.1.6.1) haml (~> 4.0) htmlentities (~> 4.3) - rdf (~> 1.1, >= 1.1.6) - rdf-aggregate-repo - rdf-xsd (~> 1.1) - rdf-rdfxml (1.1.3) + rdf (~> 1.1, >= 1.1.6, < 1.99) + rdf-aggregate-repo (~> 1.1, < 1.99) + rdf-xsd (~> 1.1, < 1.99) + rdf-rdfxml (1.1.5.1) 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.1) - rdf (~> 1.1, >= 1.1.4.2) - rdf-turtle (~> 1.1) - rdf-xsd (~> 1.1) - rdf-trig (1.1.3.1) - ebnf (~> 0.3, >= 0.3.5) + rdf (~> 1.1, >= 1.1.6, < 1.99) + rdf-rdfa (~> 1.1, >= 1.1.4.1, < 1.99) + rdf-xsd (~> 1.1, < 1.99) + rdf-trig (1.1.3) + ebnf (>= 0.3.5) rdf (~> 1.1, >= 1.1.2.1) rdf-turtle (~> 1.1, >= 1.1.3) - rdf-trix (1.1.0) - rdf (>= 1.1) - rdf-turtle (1.1.5) - ebnf (~> 0.3, >= 0.3.6) - rdf (~> 1.1, >= 1.1.4) - rdf-xsd (1.1.3) - rdf (~> 1.1, >= 1.1.9) + rdf-trix (1.99.0) + rdf (~> 1.1) + rdf-turtle (1.1.8.1) + ebnf (~> 1.0) + rdf (~> 1.1, >= 1.1.10, < 1.99) + rdf-vocab (0.8.8) + rdf (~> 1.1, >= 1.1.10) + rdf-xsd (1.1.5.1) + rdf (~> 1.1, >= 1.1.9, < 1.99) ref (1.0.5) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) retriable (1.4.1) - rsolr (1.0.12) + rsolr (1.0.13) builder (>= 2.1.2) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec-collection_matchers (1.1.2) - rspec-expectations (>= 2.99.0.beta1) - rspec-core (2.99.2) - rspec-expectations (2.99.2) - diff-lcs (>= 1.1.3, < 2.0) - rspec-its (1.0.1) - rspec-core (>= 2.99.0.beta1) - rspec-expectations (>= 2.99.0.beta1) - rspec-mocks (2.99.3) - rspec-rails (2.99.0) - actionpack (>= 3.0) - activemodel (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-collection_matchers - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec_junit_formatter (0.2.0) - builder (< 4) - rspec (>= 2, < 4) - rspec-core (!= 2.12.0) + rspec-core (3.4.1) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-rails (3.4.0) + actionpack (>= 3.0, < 4.3) + activesupport (>= 3.0, < 4.3) + railties (>= 3.0, < 4.3) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) ruby-box (1.15.0) addressable json @@ -675,15 +677,16 @@ GEM nokogiri stomp xml-simple - sparql (1.1.4) - builder (~> 3.0) - ebnf (~> 0.3, >= 0.3.5) - rdf (~> 1.1, >= 1.1.4) - rdf-aggregate-repo (~> 1.1, >= 1.1.0) - rdf-xsd (~> 1.1) - sparql-client (~> 1.1) - sxp (~> 0.1) - sparql-client (1.1.4) + sparql (1.1.2.1) + builder (>= 3.0) + ebnf (>= 0.3.2) + json (>= 1.7) + rdf (>= 1.1.0) + rdf-aggregate-repo (>= 1.1.0) + rdf-xsd (>= 1.0.2) + sparql-client (>= 1.1) + sxp (>= 0.1.3) + sparql-client (1.99.0) net-http-persistent (~> 2.9) rdf (~> 1.1) spreadsheet (1.0.1) @@ -693,24 +696,27 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.2) + sprockets-rails (2.3.3) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.10) stomp (1.3.4) sxp (0.1.5) + term-ansicolor (1.3.2) + tins (~> 1.0) therubyracer (0.12.1) libv8 (~> 3.16.14.0) ref thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - twitter-typeahead-rails (0.10.5) + tins (1.6.0) + twitter-typeahead-rails (0.11.1.pre.corejavascript) actionpack (>= 3.1) jquery-rails railties (>= 3.1) - tzinfo (0.3.44) + tzinfo (0.3.49) uber (0.0.13) uglifier (2.7.1) execjs (>= 0.3.0) @@ -737,14 +743,20 @@ PLATFORMS DEPENDENCIES about_page! - active-encode! + active-fedora (~> 8.1.0) + active_annotations! + active_encode! activerecord-jdbcsqlite3-adapter activerecord-session_store + acts_as_list + addressable + api-pagination avalon-about! avalon-workflow! bcrypt-ruby (~> 3.1.0) better_errors binding_of_caller + blacklight (~> 5.10) bootstrap-sass (= 3.3.3) bootstrap_form browse-everything (= 0.6.3) @@ -754,6 +766,7 @@ DEPENDENCIES coffee-rails compass-rails compass-susy-plugin (~> 0.9.0) + coveralls daemons database_cleaner! delayed_job (= 4.0.4) @@ -774,8 +787,7 @@ DEPENDENCIES hashdiff headless hooks - hydra (~> 8.0) - hydra-access-controls! + hydra-head! iconv jdbc-sqlite3 jettywrapper @@ -788,6 +800,7 @@ DEPENDENCIES loofah marc media-element-logo-plugin + media_element_add_to_playlist! media_element_thumbnail_selector! mediaelement-hd-toggle! mediaelement-qualityselector! @@ -800,6 +813,9 @@ DEPENDENCIES modal_logic multipart-post net-ldap + nokogiri (~> 1.6.5) + nom-xml (~> 0.5.2) + om (~> 3.1.0) omniauth-identity omniauth-lti! pry @@ -808,26 +824,31 @@ DEPENDENCIES pry-rails puma rails (~> 4.0.3) + rake (~> 10.4) rb-fsevent (~> 0.9.1) red5wrapper! roo! - rspec-its - rspec-rails (~> 2.9) - rspec_junit_formatter + rsolr (~> 1.0.12) + rspec-rails ruby-duration + rubydora (~> 1.8.1) rubyhorn! rubyzip (= 0.9.9) rvm-capistrano sass-rails (= 4.0.3) shoulda-matchers simplecov + solrizer (~> 3.3.0) sprockets (~> 2.11.0) sqlite3 therubyracer (>= 0.12.0) therubyrhino - twitter-typeahead-rails (= 0.10.5) + twitter-typeahead-rails (= 0.11.1.pre.corejavascript) uglifier (>= 1.0.3) validates_email_format_of whenever with_locking xray-rails + +BUNDLED WITH + 1.12.4 diff --git a/NOTICE b/NOTICE index 5dbb4782f9..f020b908f2 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ Avalon Media System -Copyright 2011-2015 The Trustees of Indiana University and +Copyright 2011-2016 The Trustees of Indiana University and Northwestern University diff --git a/README.md b/README.md index 1b0ddc069c..f11d413d68 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,15 @@ [![Build Status](https://travis-ci.org/avalonmediasystem/avalon.svg?branch=develop)](https://travis-ci.org/avalonmediasystem/avalon) +[![Coverage Status](https://coveralls.io/repos/avalonmediasystem/avalon/badge.svg?branch=master&service=github)](https://coveralls.io/github/avalonmediasystem/avalon?branch=master) + For more information and regular project updates visit the [Avalon blog](http://www.avalonmediasystem.org/blog). # Installing Avalon Media System Instructions on how to get a local installation of Avalon Media System installed on your system are available for [Linux](https://wiki.dlib.indiana.edu/display/VarVideo/Getting+Started+(Linux)) and [OS X](https://wiki.dlib.indiana.edu/display/VarVideo/Getting+Started+(OS+X)). # Getting started -The following steps will let you run the avalon stack locally in order to +The following steps will let you run the avalon stack locally in order to explore the out-of-the-box functionality or do basic development. * Ensure that you're running one of the Ruby versions listed in under rvm in ".travis.yml". @@ -17,11 +19,17 @@ explore the out-of-the-box functionality or do basic development. * ```git submodule update``` * Install [Mediainfo cli](http://mediainfo.sourceforge.net) * Copy config/avalon.yml.example to config/avalon.yml and [change](https://wiki.dlib.indiana.edu/display/VarVideo/Configuration+Files#ConfigurationFiles-config%2Favalon.yml) as necessary -* Copy config/authentication.yml.example to config/authentication.yml -* Copy config/controlled_vocabulary.yml.example to config/controlled_vocabulary.yml -* Setup config/secrets.yml +* ```cp config/authentication.yml.example config/authentication.yml``` +* ```cp config/controlled_vocabulary.yml.example config/controlled_vocabulary.yml``` +* Install [cmake](https://cmake.org/) if necessary. This can typically be installed via package manager +* ```bundle install``` +* ```cp config/secrets.yml.example config/secrets.yml``` +* ```rake secret``` * ```rake avalon:services:start``` -* ```rake db:migrate``` +* ```rake avalon:db_migrate``` * ```rake db:test:prepare``` * ```rake spec``` * ```rails s``` + +# Browser Testing +Testing support for Avalon Media System is provided by [BrowserStack](https://www.browserstack.com). diff --git a/app/assets/javascripts/access_control_step.js.coffee b/app/assets/javascripts/access_control_step.js.coffee new file mode 100644 index 0000000000..911378b3dc --- /dev/null +++ b/app/assets/javascripts/access_control_step.js.coffee @@ -0,0 +1,30 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +$('.date-input').datepicker + dateFormat: 'yy-mm-dd' + +$('#ajax-modal').on 'shown.bs.modal', (event) -> + $('.date-input').datepicker + dateFormat: 'yy-mm-dd' + onSelect: -> + $(this).change() + return + $('.access_date').on 'change', (event) -> + if @value != '' + $(this).closest('label').find('.remove_access').prop 'disabled', true + else + $(this).closest('label').find('.remove_access').prop 'disabled', false + return + return \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7a90eca2ec..c2076e78a8 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,15 +1,15 @@ -/* +/* * Copyright 2011-2015, The Trustees of Indiana University and Northwestern * University. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. - * + * * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed + * + * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * --- END LICENSE_HEADER BLOCK --- */ @@ -31,9 +31,10 @@ //= require fix_console //= require jquery //= require jquery_ujs +//= require jquery.nestable //= require blacklight/blacklight -// Let's be selective on which modules we include instead of going down the +// Let's be selective on which modules we include instead of going down the // kitchen sink route. Even some of these may not be needed down the road. // // Required by Blacklight @@ -42,20 +43,19 @@ //= require bootstrap-sprockets +//= require browse_everything + +/* bootstrap 3 eliminated typeahead. use twitter-typeahead-rails instead */ +//= require twitter/typeahead /* requirements for handling modals with modal logic gem */ //= require modal_logic //= require handlebars.runtime //= require templates/modal/crud -//= require browse_everything - -/* bootstrap 3 eliminated typeahead. use twitter-typeahead-rails instead */ -//= require twitter/typeahead - /* * Place any local overrides in avalon.js (for Blacklight, Hydra, jQuery, - * etc) + * etc) */ //= require avalon //= require keyboard_access diff --git a/app/assets/javascripts/autocomplete.js.coffee b/app/assets/javascripts/autocomplete.js.coffee index 283b3c66b7..8a8dbd91b1 100644 --- a/app/assets/javascripts/autocomplete.js.coffee +++ b/app/assets/javascripts/autocomplete.js.coffee @@ -17,27 +17,27 @@ $t.attr('autocomplete','off') mySource = new Bloodhound( limit: 10 - datumTokenizer: (d) -> - Bloodhound.tokenizers.whitespace(d.display) + datumTokenizer: Bloodhound.tokenizers.whitespace('display') queryTokenizer: Bloodhound.tokenizers.whitespace - remote: "#{$('body').data('mountpoint')}autocomplete?q=%QUERY&t=#{$t.data('model')}" + remote: + url: "#{$('body').data('mountpoint')}autocomplete?q=%QUERY&t=#{$t.data('model')}" + wildcard: '%QUERY' ) mySource.initialize() $t.typeahead( minLength: 2 + highlight: true , - displayKey: (suggestion) -> + display: (suggestion) -> if suggestion.display is "" suggestion.id else suggestion.display - source: mySource.ttAdapter() - templates: - suggestion: (suggestion) -> - "

" + suggestion.display + "

" + source: mySource ).on("typeahead:selected typeahead:autocompleted", (event, suggestion, dataset) -> target = $("##{$t.data('target')}") target.val suggestion["id"] + $t.data('matched_val',suggestion["display"]) return ).on("keypress", (e) -> if e.which is 13 @@ -48,17 +48,15 @@ typed = $(this).val() if typed is "" target.val "" - else - matches = $.grep(mySource.index.datums, (e) -> - e.display.toLowerCase() is typed.toLowerCase() - ) - if matches.length > 0 - target.val matches[0].id - else if !$validate - target.val typed - else - target.val "" - $(this).val "" + else if $t.data('matched_val') != typed + mySource.remote.get typed, (matches) -> + if matches.length > 0 + target.val matches[0].id + else if !$validate + target.val typed + else + target.val "" + $(this).val "" return $('.typeahead.from-model').each -> diff --git a/app/assets/javascripts/avalon.js b/app/assets/javascripts/avalon.js index 3cab17cde5..3499a04437 100644 --- a/app/assets/javascripts/avalon.js +++ b/app/assets/javascripts/avalon.js @@ -31,6 +31,33 @@ $(document).ready(function() { $( document ).on('click', '.btn-stateful-loading', function() { $(this).button('loading'); }); + $(document).on("click", ".btn-confirmation+.popover .btn", function() { + $('.btn-confirmation').popover('hide'); + return true; + }); + + $('.btn-confirmation').popover({ + trigger: 'manual', + html: true, + content: function() { + var button; + if ( typeof $(this).attr('form') === typeof undefined) { + button = "Yes, Delete"; + } else { + button = ''; + $('#'+$(this).attr('form')).find("[name='_method']").val("delete"); + } + return "

Are you sure?

"+button+" No, Cancel"; + } + }).click(function() { + var t = this; + $('.btn-confirmation').filter(function() { + return this !== t; + }).popover('hide'); + $(this).popover('show'); + return false; + }); + $('.popover-target').popover({ placement: 'top', html: true, diff --git a/app/assets/javascripts/avalon_player.js.coffee b/app/assets/javascripts/avalon_player.js.coffee index 9d1f4dbec6..493ca9506e 100644 --- a/app/assets/javascripts/avalon_player.js.coffee +++ b/app/assets/javascripts/avalon_player.js.coffee @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -19,9 +19,11 @@ class AvalonPlayer @stream_info = stream_info removeOpt = (key) -> value = opts[key]; delete opts[key]; value thumbnail_selector = if removeOpt('thumbnailSelector') then 'thumbnailSelector' else null + add_to_playlist = if removeOpt('addToPlaylist') then 'addToPlaylist' else null start_time = removeOpt('startTime') - - features = ['playpause','current','progress','duration','volume','qualities',thumbnail_selector,'fullscreen','responsive'] + success_callback = removeOpt('success') + + features = ['playpause','current','progress','duration','volume','tracks','qualities',thumbnail_selector, add_to_playlist, 'fullscreen','responsive'] features = (feature for feature in features when feature?) player_options = mode: 'auto_plugin' @@ -29,22 +31,26 @@ class AvalonPlayer usePluginFullScreen: false thumbnailSelectorUpdateURL: '/update-url' thumbnailSelectorEnabled: true + addToPlaylistEnabled: true features: features startQuality: 'low' + customError: 'This browser requires Adobe Flash Player to be installed for media playback.' + toggleCaptionsButtonWhenOnlyOne: 'true' success: (mediaElement, domObject, player) => @boundPrePlay = => if mejs.MediaFeatures.isAndroid then AndroidShim.androidPrePlay(this, player) @boundPrePlay() - + if success_callback then success_callback() + player_options[key] = val for key, val of opts @player = new MediaElementPlayer element, player_options @refreshStream() $(document).ready => @initStructureHandlers() - + setupCreationTrigger: -> watchForCreation = => # Special case visibility trigger for IE if (mejs.PluginDetector.ua.match(/trident/gi) != null) then @container.find('#content').css('visibility','visible') - + if @player? && @player.created? && @player.created $(@player).trigger('created') else @@ -56,18 +62,32 @@ class AvalonPlayer @player.pause() videoNode = $(@player.$node) videoNode.html('') - + for flash in @stream_info.stream_flash videoNode.append "" for hls in @stream_info.stream_hls videoNode.append "" + if @stream_info.captions_path + videoNode.append "" if @stream_info.poster_image? then @player.setPoster(@stream_info.poster_image) initialTime = if @stream_info.t? then (parseFloat(@stream_info.t)||0) else 0 if @player.qualities? && @player.qualities.length > 0 @player.buildqualities(@player, @player.controls, @player.layers, @player.media) - initialize_view = => @player.setCurrentTime(initialTime) + initialize_view = => + if _this.stream_info.hasOwnProperty('t') and _this.player.options.displayMediaFragment + duration = _this.stream_info.duration + t = _this.stream_info.t.split(',') + start_percent = Math.round(if isNaN(parseFloat(t[0])) then 0 else (100*parseFloat(t[0]) / duration)) + end_percent = Math.round(if t.length < 2 or isNaN(parseFloat(t[1])) then 100 else (100*parseFloat(t[1]) / duration)) + annotation_span = $('').addClass('mejs-time-annotation') + annotation_span.css 'left', start_percent+'%' + annotation_span.css 'width', end_percent-start_percent+'%' + $('.mejs-time-total').append annotation_span + @player.setCurrentTime initialTime + + @player.options.playlistItemDefaultTitle = @stream_info.embed_title $(@player).one 'created', => $(@player.media).on 'timeupdate', => @setActiveSection() @@ -77,21 +97,25 @@ class AvalonPlayer $(@player.media).one 'loadeddata', initialize_view keyboardAccess() @boundPrePlay() + if @player.options.autostart + @player.media.play() - @player.load() @setupCreationTrigger() - + @player.cleartracks(@player, null, null, null) + @player.rebuildtracks() + + setStreamInfo: (value) -> @stream_info = value @refreshStream() - + setActiveSection: -> if @active_segment != @stream_info.id @active_segment = @stream_info.id @container.find("a.current-section").removeClass('current-section') @container.find("a[data-segment='#{@active_segment}']:first").addClass('current-section') - + section_nodes = @container.find("a[data-segment='#{@active_segment}'].playable") current_time = if @player? then @player.getCurrentTime() else 0 active_node = null @@ -103,14 +127,14 @@ class AvalonPlayer active_node = node break # active_node ||= $('a.current-section') - - current_stream = @container.find('a.current-stream') + + current_stream = @container.find('a.current-stream') if current_stream[0] != active_node current_stream.removeClass('current-stream') $(active_node) .addClass('current-stream') .trigger('streamswitch', [@stream_info]) - + marked_node = @container.find('i.now-playing') now_playing_node = @container.find('a.current-stream') if now_playing_node.length == 0 @@ -118,18 +142,18 @@ class AvalonPlayer unless now_playing_node == marked_node marked_node.remove() now_playing_node.before('') - + initStructureHandlers: -> @container.find('a[data-segment]').on 'click', (e) => target = $(e.currentTarget) segment = target.data('segment') - - current_stream = - @container.find('a.current-stream').data() || - @container.find('a.current-section').data() || + + current_stream = + @container.find('a.current-stream').data() || + @container.find('a.current-section').data() || {} target_stream = target.data() - + if current_stream.isVideo == target_stream.isVideo e.preventDefault() if current_stream.segment == target_stream.segment @@ -137,10 +161,9 @@ class AvalonPlayer else $('.mejs-overlay-loading').show().closest('.mejs-layer').show() splitUrl = (target_stream.nativeUrl || target.attr('href')).split('?') - uri = "#{splitUrl[0]}.json" + uri = "#{splitUrl[0]}.js" params = ["content=#{segment}"] params.push(splitUrl[1]) if splitUrl[1]? $.getJSON uri, params.join('&'), (data) => @setStreamInfo(data) - -(exports ? this).AvalonPlayer = AvalonPlayer - + +(exports ? this).AvalonPlayer = AvalonPlayer diff --git a/app/assets/javascripts/avalon_playlists/playlist_items.js b/app/assets/javascripts/avalon_playlists/playlist_items.js new file mode 100644 index 0000000000..7ffea664b2 --- /dev/null +++ b/app/assets/javascripts/avalon_playlists/playlist_items.js @@ -0,0 +1,36 @@ +// This is for the playlists edit page +Blacklight.onLoad(function(){ + + // Display the drag handle + $('.dd-handle').removeClass('hidden'); + + // Initialize drag-and-drop behavior + $('.dd').nestable({ maxDepth: 1, dropCallback: function(data){ + allItemsData = $('.dd').nestable('serialize'); + itemsContainer = $('.dd'); + reorderItems(allItemsData, itemsContainer); + } }); + + var reorderItems = function(data, container) { + var playlistId = container.data('playlist_id'); + var items = data; + for(var i in data){ + items[i]['position'] = (parseInt(i) + 1).toString(); + } + + $.ajax({ + type: "PATCH", + url: '/playlists/' + playlistId + '.json', + data: { playlist: {items_attributes: items}}, + success: function(data, status){ + } + }); + + // Update the position text in the form + var textElements = $('.dd .position-input'); + for(var i in textElements) { + textElements[i].value = parseInt(i) + 1; + } + }; + +}); diff --git a/app/assets/javascripts/file_upload_step.js.coffee b/app/assets/javascripts/file_upload_step.js.coffee index 22d373c769..0ea940e846 100644 --- a/app/assets/javascripts/file_upload_step.js.coffee +++ b/app/assets/javascripts/file_upload_step.js.coffee @@ -25,23 +25,5 @@ $('input[type=text]',section_form).each () -> double.val($(this).val()) $('input[type=submit]',section_form).hide() -$(document).on "click", ".btn-confirmation+.popover .btn", -> - $('.btn-confirmation').popover('hide') - return true - -$('.btn-confirmation') - .popover - trigger: 'manual', - html: true, - content: () -> - "

Are you sure?

- Yes, Delete - No, Cancel" - placement: 'left' - .click () -> - t = this - $('.btn-confirmation') - .filter(() -> this isnt t) - .popover('hide') - $(this).popover('show') - return false +$('.date-input').datepicker + dateFormat: 'yy-mm-dd' diff --git a/app/assets/javascripts/keyboard_access.js b/app/assets/javascripts/keyboard_access.js index 8a3169d9f4..25d125292b 100644 --- a/app/assets/javascripts/keyboard_access.js +++ b/app/assets/javascripts/keyboard_access.js @@ -1,15 +1,15 @@ -/* +/* * Copyright 2011-2015, The Trustees of Indiana University and Northwestern * University. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. - * + * * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed + * + * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * --- END LICENSE_HEADER BLOCK --- */ @@ -22,47 +22,47 @@ $(document).ready(function() { }); var keyboardAccess = function() { - var interactive_elements = [ "a", "input", "button", "textarea" ]; + var interactive_elements = [ "a", "input", "button", "textarea" ]; - function addElementOutline( element ) { - $( element ).addClass( "outline_on" ); - $( element ).on( "focus", function() { - if ( $( this ).hasClass( "outline_on" )) { - var player = $( ".avalon-player" )[ 0 ]; - if ( player && $.contains( player, $( this )[ 0 ] )) { - $( this ).addClass( "player_element_outline" ); - } else { - $( this ).addClass( "page_element_outline" ) - } + function addElementOutline( element ) { + $( element ).addClass( "outline_on" ); + $( element ).on( "focus", function() { + if ( $( this ).hasClass( "outline_on" )) { + var player = $( ".avalon-player" )[ 0 ]; + if ( player && $.contains( player, $( this )[ 0 ] )) { + $( this ).addClass( "player_element_outline" ); + } else { + $( this ).addClass( "page_element_outline" ) } - }) - }; + } + }) + }; - function removeElementOutline( element ) { - $( element ).on( "blur", function() { - $( this ).removeClass( "player_element_outline" ); - $( this ).removeClass( "page_element_outline" ); - }); - }; + function removeElementOutline( element ) { + $( element ).on( "blur", function() { + $( this ).removeClass( "player_element_outline" ); + $( this ).removeClass( "page_element_outline" ); + }); + }; - function hideOutlineForMouse( element ) { - $( element ).on( "mouseover", function() { - $( this ).removeClass( "outline_on" ); - }); - $( element ).on( "mouseout", function() { - $( this ).addClass( "outline_on" ); - }); - }; + function hideOutlineForMouse( element ) { + $( element ).on( "mouseover", function() { + $( this ).removeClass( "outline_on" ); + }); + $( element ).on( "mouseout", function() { + $( this ).addClass( "outline_on" ); + }); + }; - function interactiveElements() { - $.each( interactive_elements, function( index, value ) { - addElementOutline( value ); - removeElementOutline( value ); - hideOutlineForMouse( value ); - }); - } + function interactiveElements() { + $.each( interactive_elements, function( index, value ) { + addElementOutline( value ); + removeElementOutline( value ); + hideOutlineForMouse( value ); + }); + } - interactiveElements(); + interactiveElements(); // Special case for the play/pause overlay and play controls background $( ".mejs-overlay-play, .mejs-controls" ).on( "mouseover", function() { $( ".mejs-playpause-button button" ).removeClass( "outline_on" )}); @@ -119,16 +119,21 @@ var keyboardAccess = function() { } }); - // Hide the controls when tabbing out of the video player - $( ".mejs-controls button:last" ).on( "keydown", function( e ) { - if ( !e.shiftKey && e.keyCode == 9 && $( "#content div[itemprop='video']" ).length !== 0 ) { - $( ".mejs-controls" ).css( "visibility", "hidden" ); - } - }); + // Hide the controls when tabbing out of the video player + $( ".mejs-controls button:last" ).on( "keydown", function( e ) { + if ( !e.shiftKey && e.keyCode == 9 && $( "#content div[itemprop='video']" ).length !== 0 ) { + $( ".mejs-controls" ).css( "visibility", "hidden" ); + } + }); - $( ".mejs-controls button:first" ).on( "keydown", function( e ) { - if ( e.shiftKey && e.keyCode == 9 && $( "#content div[itemprop='video']" ).length !== 0 ) { - $( ".mejs-controls" ).css( "visibility", "hidden" ); - } - }); + $( ".mejs-controls button:first" ).on( "keydown", function( e ) { + if ( e.shiftKey && e.keyCode == 9 && $( "#content div[itemprop='video']" ).length !== 0 ) { + $( ".mejs-controls" ).css( "visibility", "hidden" ); + } + }); + + // Hide the volume slider when tabbing off of the mute button + $( ".mejs-button.mejs-volume-button.mejs-mute button:first").on("blur", function( e ) { + $( ".mejs-volume-slider" ).hide(); + }); }; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 58858c8d0b..2da9c48b2b 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,15 +1,15 @@ -/* +/* * Copyright 2011-2015, The Trustees of Indiana University and Northwestern * University. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. - * + * * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed + * + * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * --- END LICENSE_HEADER BLOCK --- */ @@ -25,9 +25,9 @@ @import "typeahead"; /* - * This is the default theme. You can modify many of the settings + * This is the default theme. You can modify many of the settings * by editing your local app/assets/stylesheets/blacklight_themes/standard.css - * require 'blacklight_themes/avalon' + * require 'blacklight_themes/avalon' * * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. @@ -40,9 +40,9 @@ * * require jquery.fileupload-ui *= require admin - + * Required by Hydra - * require 'hydra/styles' + * require 'hydra/styles' * * require_tree . *= require 'avalon-engage' @@ -50,3 +50,6 @@ */ //= require rails_bootstrap_forms @import "avalon"; +@import "blacklight_folders/forms"; +@import "blacklight_folders/folders"; +@import "blacklight_folders/nestable"; diff --git a/app/assets/stylesheets/avalon.css.scss b/app/assets/stylesheets/avalon.css.scss index 2b5ed76ff1..2a3b8d913b 100644 --- a/app/assets/stylesheets/avalon.css.scss +++ b/app/assets/stylesheets/avalon.css.scss @@ -243,6 +243,15 @@ label { .remove:hover, .remove:active { @extend .btn-danger; } + td.access_list_label { + width: 55%; + } + td.access_list_dates { + width: 35%; + } + td.access_list_remove { + width: 10%; + } } /* UI Fixes */ @@ -869,7 +878,7 @@ div.status-detail { list-style: none; padding: 0; margin: 0; - min-width: 800px; + min-width: 920px; } ul.header { background-color: $lightgray; @@ -890,26 +899,34 @@ div.status-detail { &:nth-of-type(2) { width: 30%; } &:nth-of-type(3) { width: 40%; } &:nth-of-type(4) { width: 75px; text-align: left; } - &:nth-of-type(5) { width: 80px; text-align: center; float: right; } + &:nth-of-type(5) { width: 75px; text-align: center; } + &:nth-of-type(6) { width: 75px; text-align: center; float: right; } } } - div.structure_xml { + div.structure_tool { padding: 10px 20px; border-top: 1px dotted $gray; min-height: 40px; - div.structure_view ul { - padding-left: 20px; - li { - display: block; - width: 100%; + div.structure_view { + margin-left: 20px; + ul { + padding-left: 20px; + li { + display: block; + width: 100%; + } } } - div.structure_actions { - float: right; + div.tool_actions { + width: 100%; form { display: inline; + float: right; } } + span.tool_label { + font-weight: bold; + } } } div.structure_edit { @@ -973,3 +990,145 @@ div.structure_edit { .mejs-overlay-loading { border-radius: 50% !important; } + +.me-cannotplay { + @extend .alert; + @extend .alert-danger; + text-align: center; + visibility: visible !important; +} + +/* Playlist show page */ +.queue { + opacity:.80; + &:hover { + opacity:1; + } +} +.now-playing { + padding:0; + margin:.25em 0; +} +h5.panel-title { + font-size:1.15em; + padding-left:.4em; + min-height: 1em; +} +.panel-heading { + border-top:1px solid $gray; +} +.panel-heading .accordion-toggle:before { + font-family: 'FontAwesome'; + content: "\f078"; +} +.panel-heading .accordion-toggle.collapsed:before { + content: "\f054"; +} +#metadata_header h3 { + font-size:18px; +} +.indicator { + font-size:18px; + padding-right:4px; +} +.now-playing-title { + width:80%; + float:left; +} +.side-playlist{ + border-top:1px solid $gray; + padding-top: .6em; + li { + margin-top: .2em; + float: left; + width:100%; + } + .pull-right { + text-align:right; + } +} +.playlist-title { + margin:0;padding:0; + font-size:1.5em; +} +#section-label { + float:left; +} +.playlist-title { + width:100%; + border-bottom:1px solid $gray; + line-height:2em; + .fa { + font-size:.7em; + } +} +.playlist_actions { + textarea { + resize: vertical; + } +} +.playlist_item_edit { + border: 2pt solid $gray; + border-top: 1pt solid $gray; + border-left: 1pt solid $gray; + border-right: 1pt solid $gray; + border-radius: 5pt; + background-color: $lightgray; + padding: 10pt; + margin-top: 2pt; + margin-bottom: 10pt; + label { + text-align: right; + margin-top: 2pt; + } + input, textarea { + margin-bottom: 4pt; + } + textarea { + resize: vertical; + } +} +.annotation_title { + font-weight: bold; + left: 0 ; + position: absolute; +} +.annotation_start_time { + position: absolute; + right: 240px; +} +.annotation_end_time { + position: absolute; + right: 130px; +} +.annotation_position { + position: absolute; + right: 0; +} +.related_item { + position: relative; +} + +.mejs-time-annotation { + background: linear-gradient(#00F,#00F); + opacity: .75; + height: 4px !important; + top: 2px !important; +} + +.playlist-description { + margin-top: .6em; + margin-bottom: .4em; +} + +.playlist_item_denied { + color: gray ; +} + +.show_playlist_player_denied_title { + display: block; +} + +.nowrap { + white-space: nowrap; +} diff --git a/app/assets/stylesheets/blacklight_folders/_folders.scss b/app/assets/stylesheets/blacklight_folders/_folders.scss new file mode 100644 index 0000000000..010602cff8 --- /dev/null +++ b/app/assets/stylesheets/blacklight_folders/_folders.scss @@ -0,0 +1,55 @@ +$table-border-color: #dddddd; +$table_border: 1px dotted $table-border-color; + +.folder-index { + #sort-tool { + margin-top: 30px + } +} + +.folder-view { + .bookmarkTools { + margin-top: 20px; + } +} + +.blacklight-folders-edit { + ol { + list-style-type: none; + border: 1px solid $table-border-color; + padding: 0; + margin-top: 20px; + + li { + padding: 5px; + margin: 0; + border-top: $table_border; + + .title { + padding-top: 6px; + padding-left: 0; + } + } + + li:first-child { + border-top: none; + } + } +} + +.glyphicon-unlock:before { + // The Bootstrap halflings doesn't have the unlock icon, so we'll use the globe + // You should be able to override this if you meet the license terms for + // glyphicons. See: http://glyphicons.com/license/ + content: "\e135" +} + +#folder-tools { + margin-top: 5px; + height: 25px; +} + +.position-input { + width: 40%; + text-align: center; +} \ No newline at end of file diff --git a/app/assets/stylesheets/blacklight_folders/_forms.scss b/app/assets/stylesheets/blacklight_folders/_forms.scss new file mode 100644 index 0000000000..7edb2207ad --- /dev/null +++ b/app/assets/stylesheets/blacklight_folders/_forms.scss @@ -0,0 +1,10 @@ +.actions { + padding: 1px; + margin-top: 1em; + @extend .clearfix; +} + +.primary-actions { + margin-top: 3px; + @extend .pull-right; +} \ No newline at end of file diff --git a/app/assets/stylesheets/blacklight_folders/_nestable.scss b/app/assets/stylesheets/blacklight_folders/_nestable.scss new file mode 100644 index 0000000000..13a3fa40e7 --- /dev/null +++ b/app/assets/stylesheets/blacklight_folders/_nestable.scss @@ -0,0 +1,129 @@ +// Drag and drop elements for 'nestable' gem + +.dd { + position: relative; + display: block; + margin: 0; + padding: 0; + list-style: none; +} + +tbody.dd-list { + display: table-row-group; + padding-left: 0; +} + +tbody.dd-dragel { + display: table; +} + +li.dd-item { + list-style: none; +} + +tr.dd-item { + display: table-row; + position: relative; +} + +.dd-list { display: block; position: relative; margin: 0; padding: 0; list-style: none; } +.dd-list .dd-list { padding-left: 30px; } +.dd-collapsed .dd-list { display: none; } + +.dd-item, +.dd-empty, +.dd-placeholder { display: block; position: relative; margin: 0; padding: 0; min-height: 20px; line-height: 20px; } + +.dd-handle { + display: inline-block; + height: 100%; + margin: 0 0.2em 0 0; + padding: 5px 10px; + color: #333; + text-decoration: none; + text-align: center; + font-size: 1.5em; + font-weight: bold; + border: 1px solid #ccc; + background: #fafafa; + background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); + background: linear-gradient(top, #fafafa 0%, #eee 100%); + -webkit-border-radius: 3px; + border-radius: 3px; + box-sizing: border-box; -moz-box-sizing: border-box; +} +.dd-handle:hover { + color: #2ea8e5; + background: #fff; + cursor: grab; +} + +.dd-item > button { display: block; position: relative; cursor: pointer; float: left; width: 25px; height: 20px; margin: 5px 0; padding: 0; text-indent: 100%; white-space: nowrap; overflow: hidden; border: 0; background: transparent; line-height: 1; text-align: center; font-weight: bold; } +.dd-item > button:before { content: '+'; display: block; position: absolute; width: 100%; text-align: center; text-indent: 0; } +.dd-item > button[data-action="collapse"]:before { content: '-'; } + +.dd-placeholder, +.dd-empty { margin: 5px 0; padding: 0; min-height: 30px; border: 1px dashed #b6bcbf; box-sizing: border-box; -moz-box-sizing: border-box; } +.dd-empty { border: 1px dashed #bbb; min-height: 100px; + background-image: -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-image: -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-size: 60px 60px; + background-position: 0 0, 30px 30px; +} + +.dd-dragel { position: absolute; pointer-events: none; z-index: 9999; } +.dd-dragel > .dd-item .dd-handle { margin-top: 0; } +.dd-dragel .dd-handle { + -webkit-box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); + box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); +} + +/** + * Nestable Extras + */ + +.nestable-lists { display: block; clear: both; padding: 30px 0; width: 100%; border: 0; border-top: 2px solid #ddd; border-bottom: 2px solid #ddd; } + + +.dd-hover > .dd-handle { background: #2ea8e5 !important; } + +/** + * Nestable Draggable Handles + */ + +.dd3-content { display: block; margin: 5px 0; padding: 0 0 0 30px; + background: #fafafa; + background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); + background: linear-gradient(top, #fafafa 0%, #eee 100%); + -webkit-border-radius: 3px; + border-radius: 3px; + box-sizing: border-box; -moz-box-sizing: border-box; +} +.dd3-content.page-admin { + position: relative; +} +.dd3-content .panel-body {background: white;} +.dd-dragel > .dd3-item > .dd3-content { margin: 0; } + +.dd3-item > button { margin-left: 30px; } + +.dd3-handle { + position: absolute; margin: 0; left: 0; top: 0; cursor: pointer; width: 30px; + text-indent: 100px; + white-space: nowrap; overflow: hidden; + border: 1px solid #aaa; + background: #ddd; + background: -webkit-linear-gradient(top, #ddd 0%, #bbb 100%); + background: -moz-linear-gradient(top, #ddd 0%, #bbb 100%); + background: linear-gradient(top, #ddd 0%, #bbb 100%); + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.dd3-handle:before { content: '≡'; display: block; position: absolute; left: 0; top: 9px; width: 100%; text-align: center; text-indent: 0; color: #fff; font-weight: normal; } +.dd3-handle:hover { background: #ddd; } \ No newline at end of file diff --git a/app/assets/stylesheets/typeahead.css b/app/assets/stylesheets/typeahead.css index cd3b8be3bc..14de64bba1 100644 --- a/app/assets/stylesheets/typeahead.css +++ b/app/assets/stylesheets/typeahead.css @@ -127,7 +127,7 @@ fieldset[disabled] .twitter-typeahead .tt-input { cursor: not-allowed; background-color: #eeeeee !important; } -.tt-dropdown-menu { +.tt-menu { position: absolute; top: 100%; left: 0; @@ -148,7 +148,7 @@ fieldset[disabled] .twitter-typeahead .tt-input { *border-right-width: 2px; *border-bottom-width: 2px; } -.tt-dropdown-menu .tt-suggestion { +.tt-menu .tt-suggestion { display: block; padding: 3px 20px; clear: both; @@ -157,16 +157,16 @@ fieldset[disabled] .twitter-typeahead .tt-input { color: #333333; white-space: nowrap; } -.tt-dropdown-menu .tt-suggestion.tt-cursor { +.tt-menu .tt-suggestion.tt-cursor { text-decoration: none; outline: 0; background-color: #f5f5f5; color: #262626; } -.tt-dropdown-menu .tt-suggestion.tt-cursor a { +.tt-menu .tt-suggestion.tt-cursor a { color: #262626; } -.tt-dropdown-menu .tt-suggestion p { +.tt-menu .tt-suggestion p { margin: 0; } .input-group .twitter-typeahead .tt-hint, diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 44b4f7d076..37c6f20a9b 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -14,11 +14,12 @@ class Admin::CollectionsController < ApplicationController before_filter :authenticate_user! - load_and_authorize_resource except: [:remove, :show] + load_and_authorize_resource except: [:index] + before_filter :load_and_authorize_collections, only: [:index] respond_to :html # Catching a global exception seems like a bad idea here - rescue_from Exception do |e| + rescue_from ArgumentError do |e| if e.message == "UserIsEditor" flash[:notice] = "User #{params[:new_depositor]} needs to be removed from manager or editor role first" redirect_to @collection @@ -27,27 +28,38 @@ class Admin::CollectionsController < ApplicationController end end + def load_and_authorize_collections + @collections = get_user_collections + authorize!(params[:action].to_sym, Admin::Collection) + end + # GET /collections def index - @collections = get_user_collections + respond_to do |format| + format.html + format.json { paginate json: @collections } + end end # GET /collections/1 def show - @collection = Admin::Collection.find(params[:id]) - redirect_to admin_collections_path unless can? :read, @collection - @groups = @collection.default_local_read_groups - @users = @collection.default_read_users - @virtual_groups = @collection.default_virtual_read_groups - @visibility = @collection.default_visibility - - @addable_groups = Admin::Group.non_system_groups.reject { |g| @groups.include? g.name } - @addable_courses = Course.all.reject { |c| @virtual_groups.include? c.context_id } + respond_to do |format| + format.json { render json: @collection.to_json } + format.html { + @groups = @collection.default_local_read_groups + @users = @collection.default_read_users + @virtual_groups = @collection.default_virtual_read_groups + @ip_groups = @collection.default_ip_read_groups + @visibility = @collection.default_visibility + + @addable_groups = Admin::Group.non_system_groups.reject { |g| @groups.include? g.name } + @addable_courses = Course.all.reject { |c| @virtual_groups.include? c.context_id } + } + end end # GET /collections/new def new - @collection = Admin::Collection.new respond_to do |format| format.js { render json: modal_form_response(@collection) } format.html { render 'new' } @@ -56,15 +68,20 @@ def new # GET /collections/1/edit def edit - @collection = Admin::Collection.find(params[:id]) respond_to do |format| format.js { render json: modal_form_response(@collection) } end end + + # GET /collections/1/items + def items + mos = paginate @collection.media_objects + render json: mos.collect{|mo| [mo.pid, mo.to_json] }.to_h + end # POST /collections def create - @collection = Admin::Collection.create(params[:admin_collection].merge(managers: [user_key])) + @collection = Admin::Collection.create(params[:admin_collection]) if @collection.persisted? User.where(username: [RoleControls.users('administrator')].flatten).each do |admin_user| NotificationsMailer.delay.new_collection( @@ -74,34 +91,25 @@ def create subject: "New collection: #{@collection.name}" ) end - - render json: modal_form_response(@collection, redirect_location: admin_collection_path(@collection)) + render json: {id: @collection.pid}, status: 200 else - render json: modal_form_response(@collection) + logger.warn "Failed to create collection #{@collection.name rescue ''}: #{@collection.errors.full_messages}" + render json: {errors: ['Failed to create collection:']+@collection.errors.full_messages}, status: 422 end end # PUT /collections/1 def update - @collection = Admin::Collection.find(params[:id]) - if params[:admin_collection].present? && params[:admin_collection][:name].present? - if params[:admin_collection][:name] != @collection.name && can?('update_name', @collection) - @old_name = @collection.name - @collection.name = params[:admin_collection][:name] - if @collection.save - User.where(username: [RoleControls.users('administrator')].flatten).each do |admin_user| - NotificationsMailer.delay.update_collection( - updater_id: current_user.id, - collection_id: @collection.id, - user_id: admin_user.id, - old_name: @old_name, - subject: "Notification: collection #{@old_name} changed to #{@collection.name}" - ) - end + name_changed = false + if params[:admin_collection].present? + if params[:admin_collection][:name].present? + if params[:admin_collection][:name] != @collection.name && can?('update_name', @collection) + @old_name = @collection.name + @collection.name = params[:admin_collection][:name] + name_changed = true end end end - ["manager", "editor", "depositor"].each do |title| if params["submit_add_#{title}"].present? if params["add_#{title}"].present? && can?("update_#{title.pluralize}".to_sym, @collection) @@ -123,13 +131,20 @@ def update # If Save Access Setting button or Add/Remove User/Group button has been clicked if can?(:update_access_control, @collection) - ["group", "class", "user"].each do |title| + ["group", "class", "user", "ipaddress"].each do |title| if params["submit_add_#{title}"].present? if params["add_#{title}"].present? - if ["group", "class"].include? title - @collection.default_read_groups += [params["add_#{title}"].strip] + val = params["add_#{title}"].strip + if title=='user' + @collection.default_read_users += [val] + elsif title=='ipaddress' + if ( IPAddr.new(val) rescue false ) + @collection.default_read_groups += [val] + else + flash[:notice] = "IP Address #{val} is invalid. Valid examples: 124.124.10.10, 124.124.0.0/16, 124.124.0.0/255.255.0.0" + end else - @collection.default_read_users += [params["add_#{title}"].strip] + @collection.default_read_groups += [val] end else flash[:notice] = "#{title.titleize} can't be blank." @@ -137,7 +152,7 @@ def update end if params["remove_#{title}"].present? - if ["group", "class"].include? title + if ["group", "class", "ipaddress"].include? title @collection.default_read_groups -= [params["remove_#{title}"]] else @collection.default_read_users -= [params["remove_#{title}"]] @@ -149,27 +164,45 @@ def update @collection.default_hidden = params[:hidden] == "1" end - - @collection.save + @collection.update_attributes params[:admin_collection] if params[:admin_collection].present? + saved = @collection.save + if saved and name_changed + User.where(username: [RoleControls.users('administrator')].flatten).each do |admin_user| + NotificationsMailer.delay.update_collection( + updater_id: current_user.id, + collection_id: @collection.id, + user_id: admin_user.id, + old_name: @old_name, + subject: "Notification: collection #{@old_name} changed to #{@collection.name}" + ) + end + end + respond_to do |format| - format.html { redirect_to @collection } - format.js do - @collection.update_attributes params[:admin_collection] - render json: modal_form_response(@collection) + format.html do + flash[:notice] = Array(flash[:notice]) + @collection.errors.full_messages unless @collection.valid? + redirect_to @collection + end + format.json do + if saved + render json: {id: @collection.pid}, status: 200 + else + logger.warn "Failed to update collection #{@collection.name rescue ''}: #{@collection.errors.full_messages}" + render json: {errors: ['Failed to update collection:']+@collection.errors.full_messages}, status: 422 + end end end end - # GET /collections/1/reassign + # GET /collections/1/remove def remove - @collection = Admin::Collection.find(params[:id]) @objects = @collection.media_objects @candidates = get_user_collections.reject { |c| c == @collection } end # DELETE /collections/1 def destroy - @source_collection = Admin::Collection.find(params[:id]) + @source_collection = @collection target_path = admin_collections_path if @source_collection.media_objects.count > 0 @target_collection = Admin::Collection.find(params[:target_collection_id]) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f01d586496..aa62181a27 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,18 +14,18 @@ class ApplicationController < ActionController::Base before_filter :store_location + around_action :handle_api_request, if: proc{|c| request.format.json?} # Adds a few additional behaviors into the application controller include Blacklight::Controller # Adds Hydra behaviors into the application controller include Hydra::Controller::ControllerBehavior - include AccessControlsHelper layout 'avalon' # Please be sure to implement current_user and user_session. Blacklight depends on # these methods in order to perform user specific actions. - protect_from_forgery + protect_from_forgery unless: proc{|c| request.headers['Avalon-Api-Key'].present? } after_filter :set_access_control_headers @@ -42,7 +42,9 @@ def can_embed? end def current_ability - @current_ability ||= Ability.new(current_user, user_session) + session_opts ||= user_session + session_opts ||= {} + @current_ability ||= Ability.new(current_user, session_opts.merge(remote_ip: request.remote_ip)) end def store_location @@ -51,6 +53,32 @@ def store_location end end + def handle_api_request + if request.headers['Avalon-Api-Key'].present? + token = request.headers['Avalon-Api-Key'] + #verify token (and IP) + apiauth = Avalon::Authentication::Config.find {|p| p[:id] == :api } + if apiauth[:tokens].include? token + token_creds = apiauth[:tokens][token] +# user = User.find_by_api(token_creds[:username], token_creds[:email]) + user = User.find_by_username(token_creds[:username]) || + User.find_by_email(token_creds[:email]) || + User.create(:username => token_creds[:username], :email => token_creds[:email]) + + sign_in user, event: :authentication + user_session[:json_api_login] = true + user_session[:full_login] = false + else + render json: {errors: ["Permission denied."]}, status: 403 + return + end + end + yield + if user_session.present? && !!user_session[:json_api_login] + sign_out current_user + end + end + def after_sign_in_path_for(resource) session[:previous_url] || root_path end @@ -65,17 +93,21 @@ def after_sign_in_path_for(resource) rescue_from CanCan::AccessDenied do |exception| if current_user - if exception.subject.class == MediaObject && exception.action == :update - redirect_to exception.subject, flash: { notice: 'You are not authorized to edit this document. You have been redirected to a read-only view.' } - else - redirect_to root_path, flash: { notice: 'You are not authorized to perform this action.' } - end + redirect_to root_path, flash: { notice: 'You are not authorized to perform this action.' } else session[:previous_url] = request.fullpath unless request.xhr? redirect_to new_user_session_path, flash: { notice: 'You are not authorized to perform this action. Try logging in.' } end end + rescue_from ActiveFedora::ObjectNotFoundError do |exception| + if request.format == :json + render json: {errors: ["#{params[:id]} not found"]}, status: 404 + else + render '/errors/unknown_pid', status: 404 + end + end + def get_user_collections if can? :manage, Admin::Collection Admin::Collection.all diff --git a/app/controllers/avalon_annotation_controller.rb b/app/controllers/avalon_annotation_controller.rb new file mode 100644 index 0000000000..889a91da55 --- /dev/null +++ b/app/controllers/avalon_annotation_controller.rb @@ -0,0 +1,106 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +# Controller class for the AvalonAnnotation Model +# Implements show, create, update, and delete +class AvalonAnnotationController < ApplicationController + # after_action: + def initialize + @not_found_messages = { annotation: 'Annotation Not Found', + master_file: 'Master File Not Found' } + @attr_keys = [:title, :start_time, :end_time, :comment] + end + + # Finds the annotation based on passed uuid and renders the annotation's json + # @param [Hash] params the parameters used by the controller + # @option params [String] :id the uuid of the annotation you wish to render + # @example Rails Console Call To Show Annotation that is resolved by AvalonAnnotation.where(uuid: '56')[0] + # app.get('/avalon_annotation/56') + def show + lookup_annotation + render json: @annotation.pretty_annotation + end + + # Creates an annotation and renders it as JSON + # @param [Hash] params the parameters used for creating an annotation + # @option params [String] :master_file the pid of the MasterFile to be used for this annotation's source + # @option params [String] :title the title to use for the annotation, defaults the the MasterFile title + # @option params [String] :start_time the time point the annotation begins, defaults to 0 + # @option params [String] :end_time the time point the annotation ends, defaults to the duration of the MasterFile + # @option params [String] :comment Any comment the user wishes to supply, defaults to nil + # @example Rails Console command to create an annotation based of the MasterFile whose pid is avalon:20 and default values for all other fields in the annotation + # app.post('/avalon_annotation/', {master_file: 'avalon:20'}) + def create + fail ArgumentError, 'Master File Not Supplied' if params[:master_file].nil? + begin + mf = MasterFile.find(params[:master_file]) + rescue + mf = nil + end + not_found(item: :master_file) if mf.nil? + @annotation = AvalonAnnotation.create(master_file: mf) + selected_key_updates + render json: @annotation.pretty_annotation + end + + # Updates an annotation and renders it as JSON + # @param [Hash] params the parameters used for updating an annotation + # @option params [String] :id the id of the annotation to update + # @option params [String] :title the title to use for the annotation + # @option params [String] :start_time the time point the annotation + # @option params [String] :end_time the time point the annotation ends + # @option params [String] :comment Any comment the user wishes to supply + # @example Rails Console command to update the title of the annotation with a uuid of 56 to be 'Hail' + # app.put('/avalon_annotation/56', {title: 'Hail'}) + def update + lookup_annotation + selected_key_updates + render json: @annotation.pretty_annotation + end + + # Destroy an annotation based on uuid + # @param [Hash] params the parameters used by the controller + # @option params [String] :id the uuid of the annotation you wish to destroy + # @example Rails Console command to destroy the annotation with an uuid of 56 + # app.delete('/avalon_annotation/56') + def destroy + lookup_annotation + id = @annotation.uuid + @annotation.destroy + render json: { action: 'destroy', id: id, success: true } + end + + # Looks up an annotation using the id key in params and sets @annotation + def lookup_annotation + @annotation = AvalonAnnotation.where(uuid: params[:id])[0] + not_found if @annotation.nil? + end + + # Using the params passed into the controller, update the annotation and reload it + def selected_key_updates + updates = {} + @attr_keys.each do |key| + updates[key] = params[key] unless params[key].nil? + end + @annotation.update(updates) unless updates.keys.empty? + @annotation.reload + end + + # Raises a 404 error when an annotation or master_file cannot be Found + # @param [Symbol] The object that cannot be found (:annotation or :master_file) + # @raise ActionController::RoutingError resolves as a 404 in browser + def not_found(item: :annotation) + raise ActionController::RoutingError.new(@not_found_messages[item]) + end +end diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index ec6a1638f9..66d6e3b074 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -1,24 +1,27 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- class BookmarksController < CatalogController + + before_action :authenticate_user! + include Blacklight::Bookmarks #HACK next two lines are a hack for problems in the puppet VM tomcat/solr BookmarksController.search_params_logic -= [:add_query_to_solr] BookmarksController.search_params_logic += [:rewrite_bookmarks_search] - + blacklight_config.show.document_actions[:email].if = false if blacklight_config.show.document_actions[:email] blacklight_config.show.document_actions[:citation].if = false if blacklight_config.show.document_actions[:citation] @@ -47,7 +50,7 @@ def solr_escape val, options={} val.gsub("'", "\\\\\'").gsub('"', "\\\\\"") + options[:quote] end - return val + return val end @@ -72,7 +75,7 @@ def verify_permissions def index @bookmarks = token_or_current_or_guest_user.bookmarks bookmark_ids = @bookmarks.collect { |b| b.document_id.to_s } - + @response, @document_list = get_solr_response_for_document_ids(bookmark_ids, defType: 'edismax') respond_to do |format| @@ -168,7 +171,7 @@ def move_action documents else success_ids << id end - end + end flash[:success] = t("blacklight.move.success", count: success_ids.count, collection_name: collection.name) if success_ids.count > 0 flash[:alert] = "#{t('blacklight.move.alert', count: errors.count)}
#{ errors.join('
') }".html_safe if errors.count > 0 MediaObject.move_bulk success_ids, params diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 0871d57232..e85ff03003 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -19,7 +19,7 @@ class CatalogController < ApplicationController include Blacklight::Catalog # Extend Blacklight::Catalog with Hydra behaviors (primarily editing). include Hydra::Controller::ControllerBehavior - include Hydra::PolicyAwareAccessControlsEnforcement + include Hydra::MultiplePolicyAwareAccessControlsEnforcement before_filter :save_sticky_settings @@ -80,6 +80,8 @@ def search_builder processor_chain = search_params_logic config.add_facet_field 'workflow_published_sim', label: 'Published', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" config.add_facet_field 'created_by_sim', label: 'Created by', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" config.add_facet_field 'read_access_virtual_group_ssim', label: 'External Group', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow", helper_method: :vgroup_display + config.add_facet_field 'date_digitized_sim', label: 'Date Digitized', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow"#, partial: 'blacklight/hierarchy/facet_hierarchy' + config.add_facet_field 'date_ingested_sim', label: 'Date Ingested', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" # Have BL send all facet field names to Solr, which has been the default # previously. Simply remove these lines if you'd rather use Solr request @@ -88,8 +90,8 @@ def search_builder processor_chain = search_params_logic # solr fields to be displayed in the index (search results) view # The ordering of the field names is the order of the display - config.add_index_field 'creator_ssim', label: 'Main contributors', helper_method: :contributor_index_display config.add_index_field 'date_ssi', label: 'Date', helper_method: :combined_display_date + config.add_index_field 'creator_ssim', label: 'Main contributors', helper_method: :contributor_index_display config.add_index_field 'summary_ssi', label: 'Summary', helper_method: :description_index_display # solr fields to be displayed in the show (single result) view diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index 2381d9ea9f..875a38e308 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -20,10 +20,23 @@ class MasterFilesController < ApplicationController include Avalon::Controller::ControllerBehavior - skip_before_filter :verify_authenticity_token, :only => [:update] before_filter :authenticate_user!, :only => [:create] before_filter :ensure_readable_filedata, :only => [:create] + + # Renders the captions content for an object or alerts the user that no caption content is present with html present + # @return [String] The rendered template + def captions + @masterfile = MasterFile.find(params[:id]) + authorize! :read, @masterfile + ds = @masterfile.datastreams['captions'] + if ds.nil? or ds.new? + render :text => 'Not Found', :status => :not_found + else + render :text => ds.content, :content_type => ds.mimeType, :label => ds.label + end + end + def can_embed? params[:action] == 'embed' end @@ -41,8 +54,8 @@ def embed end respond_to do |format| format.html do - response.headers.delete "X-Frame-Options" - render :layout => 'embed' + response.headers.delete "X-Frame-Options" + render :layout => 'embed' end end end @@ -113,22 +126,54 @@ def attach_structure end end - # Creates and Saves a File Asset to contain the the Uploaded file + def attach_captions + captions = nil + if params[:id].blank? || (not MasterFile.exists?(params[:id])) + flash[:notice] = "MasterFile #{params[:id]} does not exist" + end + @masterfile = MasterFile.find(params[:id]) + unless flash.empty? and MediaObject.exists?(@masterfile.mediaobject_id) + flash[:notice] = "MediaObject #{@masterfile.mediaobject_id} does not exist" + end + if flash.empty? + media_object = MediaObject.find(@masterfile.mediaobject_id) + authorize! :edit, media_object, message: "You do not have sufficient privileges to add files" + if params[:master_file].present? && params[:master_file][:captions].present? + captions = params[:master_file][:captions].open.read + end + if captions.present? + @masterfile.captions.content = captions + @masterfile.captions.mimeType = params[:master_file][:captions].content_type + @masterfile.captions.dsLabel = params[:master_file][:captions].original_filename + flash[:success] = "Captions file succesfully added." + else + @masterfile.captions.delete + flash[:success] = "Captions file succesfully removed." + end + @masterfile.save + end + respond_to do |format| + format.html { redirect_to edit_media_object_path(@masterfile.mediaobject_id, step: 'structure') } + format.json { render json: {captions: captions, flash: flash} } + end + end + + # Creates and Saves a File Asset to contain the the Uploaded file # If container_id is provided: # * the File Asset will use RELS-EXT to assert that it's a part of the specified container # * the method will redirect to the container object's edit view after saving def create if params[:container_id].blank? || (not MediaObject.exists?(params[:container_id])) flash[:notice] = "MediaObject #{params[:container_id]} does not exist" - redirect_to :back + redirect_to :back return end media_object = MediaObject.find(params[:container_id]) authorize! :edit, media_object, message: "You do not have sufficient privileges to add files" - + format_errors = "The file was not recognized as audio or video - " - + if params.has_key?(:Filedata) and params.has_key?(:original) @master_files = [] params[:Filedata].each do |file| @@ -157,7 +202,7 @@ def create else flash[:notice] = create_upload_notice(master_file.file_format) end - + unless master_file.save flash[:error] = "There was a problem storing the file" else @@ -165,7 +210,7 @@ def create master_file.process @master_files << master_file end - + end elsif params.has_key?(:selected_files) @master_files = [] @@ -176,7 +221,7 @@ def create master_file.mediaobject = media_object master_file.setContent(File.open(file_path, 'rb')) master_file.set_workflow(params[:workflow]) - + unless master_file.save flash[:error] = "There was a problem storing the file" else @@ -188,7 +233,7 @@ def create else flash[:notice] = "You must specify a file to upload" end - + respond_to do |format| format.html { redirect_to edit_media_object_path(params[:container_id], step: 'file-upload') } format.js { } @@ -199,7 +244,7 @@ def create def destroy master_file = MasterFile.find(params[:id]) media_object = master_file.mediaobject - + authorize! :edit, media_object, message: "You do not have sufficient privileges to delete files" filename = File.basename(master_file.file_location) @@ -208,16 +253,16 @@ def destroy media_object.set_media_types! media_object.set_duration! media_object.save( validate: false ) - + flash[:notice] = "#{filename} has been deleted from the system" redirect_to edit_media_object_path(media_object.pid, step: "file-upload") end - + def set_frame master_file = MasterFile.find(params[:id]) parent = master_file.mediaobject - + authorize! :read, parent, message: "You do not have sufficient privileges to edit this file" opts = { :type => params[:type], :size => params[:size], :offset => params[:offset].to_f*1000, :preview => params[:preview] } respond_to do |format| @@ -253,15 +298,15 @@ def get_frame end protected - def create_upload_notice(format) + def create_upload_notice(format) case format when /^Sound$/ text = 'The uploaded content appears to be audio'; - when /^Moving image$/ + when /^Moving image$/ text = 'The uploaded content appears to be video'; else text = 'The uploaded content could not be identified'; - end + end return text end diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 48e94bf17c..2adc64fed8 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -19,9 +19,12 @@ class MediaObjectsController < ApplicationController include Avalon::Controller::ControllerBehavior include ConditionalPartials -# before_filter :enforce_access_controls - before_filter :inject_workflow_steps, only: [:edit, :update] - before_filter :load_player_context, only: [:show, :show_progress] + before_filter :authenticate_user!, except: [:show, :set_session_quality] + before_filter :authenticate_api!, only: [:show], if: proc{|c| request.format.json?} + load_and_authorize_resource instance_name: 'mediaobject', except: [:destroy, :update_status, :set_session_quality, :tree, :deliver_content] + + before_filter :inject_workflow_steps, only: [:edit, :update], unless: proc{|c| request.format.json?} + before_filter :load_player_context, only: [:show] def self.is_editor ctx ctx.current_ability.is_editor_of?(ctx.instance_variable_get('@mediaobject').collection) @@ -37,18 +40,14 @@ def self.is_lti_session ctx add_conditional_partial :share, :embed, partial: 'embed_resource', if: is_editor_or_not_lti add_conditional_partial :share, :lti_url, partial: 'lti_url', if: is_editor_or_lti - - # Catch exceptions when you try to reference an object that doesn't exist. - # Attempt to resolve it to a close match if one exists and offer a link to - # the show page for that item. Otherwise ... nothing! - rescue_from ActiveFedora::ObjectNotFoundError do |exception| - render '/errors/unknown_pid', status: 404 - end - def can_embed? params[:action] == 'show' end + def authenticate_api! + return head :unauthorized if !signed_in? + end + def new collection = Admin::Collection.find(params[:collection_id]) authorize! :read, collection @@ -61,8 +60,117 @@ def new redirect_to edit_media_object_path(@mediaobject) end + # POST /media_objects + def create + update_mediaobject + end + + # PUT /media_objects/avalon:1.json + def json_update + update_mediaobject + end + + def update_mediaobject + begin + collection = Admin::Collection.find(params[:collection_id]) + rescue ActiveFedora::ObjectNotFoundError + render json: {errors: ["Collection not found for #{params[:collection_id]}"]}, status: 422 + return + end + + @mediaobject.collection = collection + @mediaobject.avalon_uploader = 'REST API' + + populate_from_catalog = !!params[:import_bib_record] + if populate_from_catalog and Avalon::BibRetriever.configured? + begin + # Set other identifiers + @mediaobject.update_datastream(:descMetadata, params[:fields].slice(:other_identifier_type, :other_identifier)) + # Try to use Bib Import + @mediaobject.descMetadata.populate_from_catalog!(Array(params[:fields][:bibliographic_id]).first, + Array(params[:fields][:bibliographic_id_label]).first) + rescue + logger.warn "Failed bib import using bibID #{Array(params[:fields][:bibliographic_id]).first}, #{Array(params[:fields][:bibliographic_id_label]).first}" + ensure + if !@mediaobject.valid? + # Fall back to MODS as sent if Bib Import fails + @mediaobject.update_datastream(:descMetadata, params[:fields].slice(*@mediaobject.errors.keys)) if params.has_key?(:fields) and params[:fields].respond_to?(:has_key?) + end + end + else + @mediaobject.update_datastream(:descMetadata, params[:fields]) if params.has_key?(:fields) and params[:fields].respond_to?(:has_key?) + end + + error_messages = [] + + if !@mediaobject.valid? + invalid_fields = @mediaobject.errors.keys + required_fields = [:title, :date_issued] + if !required_fields.any? { |f| invalid_fields.include? f } + invalid_fields.each do |field| + #NOTE this will erase all values for fields with multiple values + logger.warn "Erasing field #{field} with bad value, bibID: #{Array(params[:fields][:bibliographic_id]).first}, avalon ID: #{@mediaobject.pid}" + @mediaobject[field] = nil + end + end + end + if !@mediaobject.save + error_messages += ['Failed to create media object:']+@mediaobject.errors.full_messages + elsif params[:files].respond_to?('each') + oldparts = @mediaobject.parts.collect{|p|p.pid} + params[:files].each do |file_spec| + master_file = MasterFile.new(file_spec.except(:structure, :captions, :captions_type, :files, :other_identifier)) + master_file.mediaobject = @mediaobject + master_file.structuralMetadata.content = file_spec[:structure] if file_spec[:structure].present? + if file_spec[:captions].present? + master_file.captions.content = file_spec[:captions] + master_file.captions.mimeType = file_spec[:captions_type] + master_file.captions.dsLabel = 'ingest.api' + end + master_file.label = file_spec[:label] if file_spec[:label].present? + master_file.date_digitized = DateTime.parse(file_spec[:date_digitized]).to_time.utc.iso8601 if file_spec[:date_digitized].present? + master_file.DC.identifier += Array(file_spec[:other_identifier]) + if master_file.update_derivatives(file_spec[:files], false) + @mediaobject.parts += [master_file] + else + error_messages += ["Problem saving MasterFile for #{file_spec[:file_location] rescue ""}:"]+master_file.errors.full_messages + @mediaobject.destroy + break + end + end + if error_messages.empty? + if params[:replace_masterfiles] + oldparts.each do |mf| + p = MasterFile.find(mf) + @mediaobject.parts.delete(p) + p.destroy + end + end + @mediaobject.parts_with_order = @mediaobject.parts + #Ensure these are set because sometimes there is a timing issue that prevents the masterfile save from doing it + @mediaobject.set_media_types! + @mediaobject.set_resource_types! + @mediaobject.set_duration! + @mediaobject.workflow.last_completed_step = HYDRANT_STEPS.last.step + if !@mediaobject.save + error_messages += ['Failed to create media object:']+@mediaobject.errors.full_messages + @mediaobject.destroy + elsif !!params[:publish] + @mediaobject.publish!('REST API') + @mediaobject.workflow.publish + end + end + end + if error_messages.empty? + render json: {id: @mediaobject.pid}, status: 200 + else + logger.warn "update_mediaobject failed for #{params[:fields][:title] rescue ''}: #{error_messages}" + render json: {errors: error_messages}, status: 422 + @mediaobject.destroy + end + end + def custom_edit - authorize! :update, @mediaobject if ['preview', 'structure', 'file-upload'].include? @active_step @masterFiles = load_master_files end @@ -80,8 +188,13 @@ def custom_edit if 'access-control' == @active_step @groups = @mediaobject.local_read_groups + @group_leases = @mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="local" } @users = @mediaobject.read_users + @user_leases = @mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="user" } @virtual_groups = @mediaobject.virtual_read_groups + @virtual_leases = @mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="external" } + @ip_groups = @mediaobject.ip_read_groups + @ip_leases = @mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="ip" } @visibility = @mediaobject.visibility @addable_groups = Admin::Group.non_system_groups.reject { |g| @groups.include? g.name } @@ -90,32 +203,40 @@ def custom_edit end def custom_update - authorize! :update, @mediaobject flash[:notice] = @notice end + def index + respond_to do |format| + format.json { + paginate json: MediaObject.all + } + end + end + def show - authorize! :read, @mediaobject respond_to do |format| format.html do - if (not @masterFiles.empty? and @currentStream.blank?) then + if (not @masterFiles.empty? and @currentStream.blank?) then redirect_to media_object_path(@mediaobject.pid), flash: { notice: 'That stream was not recognized. Defaulting to the first available stream for the resource' } else render end end + format.js do + render json: @currentStreamInfo + end format.json do - render :json => @currentStreamInfo + render json: @mediaobject.to_json end end end def show_progress - authorize! :read, @mediaobject overall = { :success => 0, :error => 0 } - + result = Hash[ - @masterFiles.collect { |mf| + @mediaobject.parts.collect { |mf| mf_status = { :status => mf.status_code, :complete => mf.percent_complete.to_i, @@ -133,7 +254,12 @@ def show_progress [mf.pid, mf_status] } ] - overall.each { |k,v| overall[k] = [0,[100,v.to_f/@masterFiles.length.to_f].min].max.floor } + parts_count = @mediaobject.parts.count + if parts_count > 0 + overall.each { |k,v| overall[k] = [0,[100,v.to_f/parts_count.to_f].min].max.floor } + else + overall = {success: 0, error: 0} + end if overall[:success]+overall[:error] > 100 overall[:error] = 100-overall[:success] @@ -231,12 +357,12 @@ def set_session_quality protected - def load_master_files - @mediaobject.parts_with_order + def load_master_files(opts = {}) + @mediaobject.parts_with_order opts end def load_player_context - @mediaobject = MediaObject.find(params[:id]) + return if request.format.json? and !params.has_key? :content if params[:part] index = params[:part].to_i-1 @@ -246,14 +372,14 @@ def load_player_context params[:content] = @mediaobject.section_pid[index] end - @masterFiles = load_master_files + @masterFiles = load_master_files load_from_solr: true @currentStream = params[:content] ? set_active_file(params[:content]) : @masterFiles.first @token = @currentStream.nil? ? "" : StreamToken.find_or_create_session_token(session, @currentStream.pid) # This rescue statement seems a bit dodgy because it catches *all* # exceptions. It might be worth refactoring when there are some extra # cycles available. @currentStreamInfo = @currentStream.nil? ? {} : @currentStream.stream_details(@token, default_url_options[:host]) - @currentStreamInfo['t'] = params[:t] # add MediaFragment from params + @currentStreamInfo['t'] = view_context.parse_media_fragment(params[:t]) # add MediaFragment from params end # The goal of this method is to determine which stream to provide to the interface @@ -265,13 +391,8 @@ def load_player_context # return a nil value that needs to be handled appropriately by the calling code # block def set_active_file(file_pid = nil) - unless (@mediaobject.parts.blank? or file_pid.blank?) - @mediaobject.parts.each do |part| - return part if part.pid == file_pid - end - end - - # If you haven't dropped out by this point return an empty item - nil + @masterFiles ||= load_master_files load_from_solr: true + file_pid.nil? ? nil : @masterFiles.find { |mf| mf.pid == file_pid } end + end diff --git a/app/controllers/playlist_items_controller.rb b/app/controllers/playlist_items_controller.rb new file mode 100644 index 0000000000..21296f4baa --- /dev/null +++ b/app/controllers/playlist_items_controller.rb @@ -0,0 +1,79 @@ +class PlaylistItemsController < ApplicationController + before_action :set_playlist, only: [:create, :update] + before_action :authenticate_user! + + # POST /playlists/1/items + def create + unless (can? :create, PlaylistItem) && (can? :edit, @playlist) + render json: { message: 'You are not authorized to perform this action.' }, status: 401 and return + end + title = playlist_item_params[:title] + comment = playlist_item_params[:comment] + start_time = time_str_to_milliseconds playlist_item_params[:start_time] + end_time = time_str_to_milliseconds playlist_item_params[:end_time] + annotation = AvalonAnnotation.new(master_file: MasterFile.find(playlist_item_params[:master_file_id])) + annotation.title = title + annotation.comment = comment + annotation.start_time = start_time + annotation.end_time = end_time + unless annotation.valid? + render json: { message: annotation.errors.full_messages }, status: 400 and return + end + unless annotation.save + render json: { message: "Item was not created: #{annotation.errors.full_messages}" }, status: 500 and return + end + if PlaylistItem.create(playlist: @playlist, annotation: annotation) + render json: { message: "Add to playlist was successful. See it: #{view_context.link_to("here", playlist_url(@playlist))}" }, status: 201 and return + end + rescue StandardError => error + render json: { message: "Item was not created: #{error.message}"}, status: 500 and return + end + + def update + playlist_item = PlaylistItem.find(params['id']) + unless (can? :update, playlist_item) + render json: { message: 'You are not authorized to perform this action.' }, status: 401 and return + end + annotation = AvalonAnnotation.find(playlist_item.annotation.id) + annotation.title = playlist_item_params[:title] + annotation.comment = playlist_item_params[:comment] + annotation.start_time = time_str_to_milliseconds playlist_item_params[:start_time] + annotation.end_time = time_str_to_milliseconds playlist_item_params[:end_time] + if annotation.save + render json: { message: "Item was updated successfully." }, status: 201 and return + else + render json: { message: "Item was not updated: #{annotation.errors.full_messages.join(', ')}" }, status: 500 and return + end + rescue StandardError => error + render json: { message: "Item was not updated: #{error.message}" }, status: 500 and return + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_playlist + @playlist = Playlist.find(params[:playlist_id]) + end + + def playlist_item_params + params.require(:playlist_item).permit(:title, :comment, :master_file_id, :start_time, :end_time) + end + + def time_str_to_milliseconds value + if value.is_a?(Numeric) + value.floor + elsif value.is_a?(String) + result = 0 + segments = value.split(/:/).reverse + begin + segments.each_with_index { |v,i| result += i > 0 ? Float(v) * (60**i) * 1000 : (Float(v) * 1000) } + result.to_i + rescue + return value + end + else + value + end + end + +end diff --git a/app/controllers/playlists_controller.rb b/app/controllers/playlists_controller.rb new file mode 100644 index 0000000000..542505afe4 --- /dev/null +++ b/app/controllers/playlists_controller.rb @@ -0,0 +1,127 @@ +class PlaylistsController < ApplicationController + # TODO: rewrite this to use cancancan's authorize_and_load_resource + before_action :authenticate_user!, except: [:show] + load_and_authorize_resource + + before_action :get_all_playlists, only: [:index, :edit, :update] + + # GET /playlists + def index + end + + # GET /playlists/1 + def show + end + + # GET /playlists/new + def new + @playlist = Playlist.new + end + + # GET /playlists/1/edit + def edit + # We are already editing our playlist, we don't need it to show up in this array as well + @playlists.delete( @playlist ) + end + + # POST /playlists + def create + @playlist = Playlist.new(playlist_params.merge(user: current_user)) + if @playlist.save + redirect_to @playlist, notice: 'Playlist was successfully created.' + else + flash.now[:error] = @playlist.errors.full_messages.to_sentence + render action: 'new' + end + end + + # PATCH/PUT /playlists/1 + def update + if update_playlist(@playlist) + respond_to do |format| + format.html do + redirect_to edit_playlist_path(@playlist), notice: 'Playlist was successfully updated.' + end + format.json do + render json: @playlist + end + end + else + flash.now[:error] = "There are errors with your submission. #{@playlist.errors.full_messages.join(', ')}" + render action: 'edit' + end + end + + def update_multiple + if request.request_method=='DELETE' + PlaylistItem.where(id: params[:annotation_ids]).to_a.map(&:destroy) + elsif params[:new_playlist_id].present? and params[:annotation_ids] + @new_playlist = Playlist.find(params[:new_playlist_id]) + pis = PlaylistItem.where(id: params[:annotation_ids]) + @new_playlist.items += pis + @playlist.items -= pis + @new_playlist.save! + @playlist.save! + end + redirect_to edit_playlist_path(@playlist), notice: 'Playlist was successfully updated.' + end + + # DELETE /playlists/1 + def destroy + @playlist.destroy + redirect_to playlists_url, notice: 'Playlist was successfully destroyed.' + end + + private + + # Use callbacks to share common setup or constraints between actions. + def get_all_playlists + @playlists = Playlist.for_ability(current_ability).to_a + end + + # Only allow a trusted parameter "white list" through. + def playlist_params + params.require(:playlist).permit(:title, :comment, :visibility, :annotation_ids, items_attributes: [:id, :position]) + end + + def update_playlist(playlist) + playlist.assign_attributes(playlist_params) + reorder_items(playlist) + playlist.save + end + + # This updates the positions of the playlist items + def reorder_items(playlist) + # we have to do a sort_by, not order, because the updated attributes have not been saved. + changed_playlist, new, changed_position, unchanged = playlist.items. + sort_by(&:position). + group_by do |item| + if item.playlist_id_was != item.playlist_id + :changed_playlist + elsif item.position_was.nil? + :new + elsif item.position_was != item.position + :changed_position + else + :unchanged + end + end.values_at(:changed_playlist, :new, :changed_position, :unchanged).map(&:to_a) + # items that will be in this playlist + unmoved_items = unchanged + # place items whose positions were specified + changed_position.map {|item| unmoved_items.insert(item.position - 1, item)} + # add new items at the end + unmoved_items = unmoved_items + new + # calculate positions + unmoved_items.compact. + select {|item| item.playlist_id_was == item.playlist_id}. + each_with_index do |item, position| + item.position = position + 1 + end + + # items that have moved to another playlist + changed_playlist.select {|item| item.playlist_id_was != item.playlist_id}.each do |item| + item.position = nil + end + end +end diff --git a/app/controllers/vocabulary_controller.rb b/app/controllers/vocabulary_controller.rb new file mode 100644 index 0000000000..9015462f30 --- /dev/null +++ b/app/controllers/vocabulary_controller.rb @@ -0,0 +1,53 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +class VocabularyController < ApplicationController + before_filter :authenticate_user! + authorize_resource class: Avalon::ControlledVocabulary + respond_to :json + + before_action :verify_vocabulary_exists, except: [:index] + + def index + render json: Avalon::ControlledVocabulary.vocabulary + end + + def show + render json: Avalon::ControlledVocabulary.vocabulary[params[:id].to_sym] + end + + def update + unless params[:entry].present? + render json: {errors: ["No update value sent"]}, status: 422 and return + end + + v = Avalon::ControlledVocabulary.vocabulary + v[params[:id].to_sym] |= Array(params[:entry]) + result = Avalon::ControlledVocabulary.vocabulary = v + if result + render nothing: true, content_type: 'application/json', status: 200 + else + render json: {errors: ["Update failed"]}, status: 422 + end + end + + private + + def verify_vocabulary_exists + if Avalon::ControlledVocabulary.vocabulary[params[:id].to_sym].blank? + render json: {errors: ["Vocabulary not found for #{params[:id]}"]}, status: 404 + end + end + +end diff --git a/app/helpers/access_controls_helper.rb b/app/helpers/access_controls_helper.rb deleted file mode 100644 index b025a1d966..0000000000 --- a/app/helpers/access_controls_helper.rb +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2011-2015, The Trustees of Indiana University and Northwestern -# University. Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# --- END LICENSE_HEADER BLOCK --- - -module AccessControlsHelper - def enforce_create_permissions(opts={}) - if current_user.nil? - flash[:notice] = "You need to login to add resources" - redirect_to new_user_session_path - elsif cannot? :read, Admin::Collection.find(params[:collection_id]) - flash[:notice] = "You do not have sufficient privileges to add resources" - redirect_to root_path - else - session[:viewing_context] = "create" - end - end - - def enforce_new_permissions(opts={}) - enforce_create_permissions(opts) - end - - # Controller "before" filter for enforcing access controls on edit actions - # @param [Hash] opts (optional, not currently used) - def enforce_edit_permissions(opts={}) - #load_permissions_from_solr - if current_user.nil? - flash[:notice] = "You need to login to edit resources" - redirect_to new_user_session_path - elsif cannot? :edit, params[:id] - session[:viewing_context] = "browse" - flash[:notice] = "You do not have sufficient privileges to edit this document. You have been redirected to the read-only view." - redirect_to :action=>:show - else - session[:viewing_context] = "edit" - end - end - - # Controller "before" filter for enforcing access controls on show actions - # @param [Hash] opts (optional, not currently used) - def enforce_show_permissions(opts={}) - return if not MediaObject.exists?(params[:id]) - if cannot? :read, MediaObject.find(params[:id]) - if current_user.nil? - flash[:notice] = "You do not have sufficient privileges to read this document. Try logging in." - redirect_to new_user_session_path - else - flash[:notice] = "You do not have sufficient privileges to read this document." - redirect_to root_path - end - end - end - -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c5754caf84..3c481b94cb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,9 +37,9 @@ def share_link_for(obj) def image_for(document) master_file_id = document["section_pid_tesim"].try :first - - video_count = document["mods_tesim"].count{|m| m.start_with?('moving image') } - audio_count = document["mods_tesim"].count{|m| m.start_with?('sound recording') } + + video_count = document["avalon_resource_type_tesim"].count{|m| m.start_with?('moving image') } rescue 0 + audio_count = document["avalon_resource_type_tesim"].count{|m| m.start_with?('sound recording') } rescue 0 if master_file_id if video_count > 0 @@ -133,6 +133,14 @@ def milliseconds_to_formatted_time( milliseconds ) output end + # display millisecond times in HH:MM:SS format + # @param [Float] milliseconds the time to convert + # @return [String] time in HH:MM:SS + def pretty_time( milliseconds ) + duration = milliseconds/1000 + Time.at(duration).utc.strftime(duration<3600?'%M:%S':'%H:%M:%S') + end + def git_commit_info pattern="%s %s [%s]" begin repo = Grit::Repo.new(Rails.root) diff --git a/app/helpers/playlists_helper.rb b/app/helpers/playlists_helper.rb new file mode 100644 index 0000000000..b8fd9a9ef2 --- /dev/null +++ b/app/helpers/playlists_helper.rb @@ -0,0 +1,6 @@ +module PlaylistsHelper + def human_friendly_visibility(visibility) + icon = visibility == Playlist::PUBLIC ? 'unlock' : 'lock' + safe_join([content_tag(:span, '', class:"glyphicon glyphicon-#{icon}", title: t("playlist.#{icon}AltText")),t("playlist.#{icon}Text")], ' ') + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 2b5b433714..5cd13e800f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,29 +1,30 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- class Ability include CanCan::Ability include Hydra::Ability - include Hydra::PolicyAwareAbility + include Hydra::MultiplePolicyAwareAbility def user_groups return @user_groups if @user_groups - + @user_groups = default_user_groups @user_groups |= current_user.groups if current_user and current_user.respond_to? :groups @user_groups |= ['registered'] unless current_user.new_record? - @user_groups |= @session[:virtual_groups] unless @session.blank? + @user_groups |= @session[:virtual_groups] if @session.present? and @session.has_key? :virtual_groups + @user_groups |= [@session[:remote_ip]] if @session.present? and @session.has_key? :remote_ip @user_groups end @@ -36,7 +37,7 @@ def create_permissions(user=nil, session=nil) can :manage, Admin::Group can :manage, Admin::Collection end - + if @user_groups.include? "group_manager" can :manage, Admin::Group do |group| group.nil? or !['administrator','group_manager'].include?(group.name) @@ -54,13 +55,14 @@ def create_permissions(user=nil, session=nil) end def custom_permissions(user=nil, session=nil) + playlist_permissions unless full_login? and @user_groups.include? "administrator" cannot :read, MediaObject do |mediaobject| - !mediaobject.published? && !test_edit(mediaobject.pid) + !(test_read(mediaobject.pid) && mediaobject.published?) && !test_edit(mediaobject.pid) end can :read, MasterFile do |master_file| - can? :read, masterfile.mediaobject + can? :read, master_file.mediaobject end can :read, Derivative do |derivative| @@ -77,18 +79,18 @@ def custom_permissions(user=nil, session=nil) unless (is_member_of_any_collection? or @user_groups.include? 'manager') cannot :read, Admin::Collection end - + can :update_access_control, MediaObject do |mediaobject| - @user.in?(mediaobject.collection.managers) || + @user.in?(mediaobject.collection.managers) || (is_editor_of?(mediaobject.collection) && !mediaobject.published?) end can :unpublish, MediaObject do |mediaobject| - @user.in?(mediaobject.collection.managers) + @user.in?(mediaobject.collection.managers) end can :update, Admin::Collection do |collection| - is_editor_of?(collection) + is_editor_of?(collection) end can :update_unit, Admin::Collection do |collection| @@ -108,25 +110,31 @@ def custom_permissions(user=nil, session=nil) end can :update_depositors, Admin::Collection do |collection| - is_editor_of?(collection) + is_editor_of?(collection) + end + + can :inspect, MediaObject do |mediaobject| + is_member_of?(mediaobject.collection) end - - can :inspect, MediaObject do |mediaobject| - is_member_of?(mediaobject.collection) - end # Users logged in through LTI cannot share can :share, MediaObject end + if is_api_request? + can :manage, MediaObject + can :manage, Admin::Collection + can :manage, Avalon::ControlledVocabulary + end + cannot :update, MediaObject do |mediaobject| - (not full_login?) || (!is_member_of?(mediaobject.collection)) || + (not full_login?) || (!is_member_of?(mediaobject.collection)) || ( mediaobject.published? && !@user.in?(mediaobject.collection.managers) ) end cannot :destroy, MediaObject do |mediaobject| - # non-managers can only destroy mediaobject if it's unpublished - (not full_login?) || (!is_member_of?(mediaobject.collection)) || + # non-managers can only destroy mediaobject if it's unpublished + (not full_login?) || (!is_member_of?(mediaobject.collection)) || ( mediaobject.published? && !@user.in?(mediaobject.collection.managers) ) end @@ -136,13 +144,35 @@ def custom_permissions(user=nil, session=nil) end end + def playlist_permissions + if @user.id.present? + can :manage, Playlist, user: @user + # can :create, Playlist + end + can :read, Playlist, visibility: Playlist::PUBLIC + + playlist_item_permissions + end + + def playlist_item_permissions + if @user.id.present? + can [:create, :update, :delete], PlaylistItem do |playlist_item| + can? :manage, playlist_item.playlist + end + can :read, PlaylistItem do |playlist_item| + (can? :read, playlist_item.playlist) && + (can? :read, playlist_item.master_file) + end + end + end + def is_member_of?(collection) - @user_groups.include?("administrator") || + @user_groups.include?("administrator") || @user.in?(collection.managers, collection.editors, collection.depositors) end - def is_editor_of?(collection) - @user_groups.include?("administrator") || + def is_editor_of?(collection) + @user_groups.include?("administrator") || @user.in?(collection.managers, collection.editors) end @@ -152,7 +182,14 @@ def is_member_of_any_collection? def full_login? return @full_login unless @full_login.nil? - @full_login = @session.present? ? @session[:full_login] : true + @full_login = ( @session.present? and @session.has_key? :full_login ) ? @session[:full_login] : true @full_login end + + def is_api_request? + @json_api_login ||= !!@session[:json_api_login] if @session.present? + @json_api_login ||= false + @json_api_login + end + end diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index e2e65d8727..fe070a552b 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -31,42 +31,87 @@ def execute context end # Limited access stuff - ["group", "class", "user"].each do |title| + limited_access_submit = false + ["group", "class", "user", "ipaddress"].each do |title| if context["submit_add_#{title}"].present? + limited_access_submit = true + begin_time = context["add_#{title}_begin"].blank? ? nil : context["add_#{title}_begin"] + end_time = context["add_#{title}_end"].blank? ? nil : context["add_#{title}_end"] + create_lease = begin_time.present? || end_time.present? if context["add_#{title}"].present? - if ["group", "class"].include? title - mediaobject.read_groups += [context["add_#{title}"].strip] + val = context["add_#{title}"].strip + if title=='user' + if create_lease + begin + mediaobject.governing_policies += [ Lease.create(begin_time: begin_time, end_time: end_time, read_users: [val]) ] + rescue Exception => e + context[:error] = e.message + end + else + mediaobject.read_users += [val] + end + elsif title=='ipaddress' + if ( IPAddr.new(val) rescue false ) + if create_lease + begin + mediaobject.governing_policies += [ Lease.create(begin_time: begin_time, end_time: end_time, read_groups: [val]) ] + rescue Exception => e + context[:error] = e.message + end + else + mediaobject.read_groups += [val] + end + else + context[:error] = "IP Address #{val} is invalid. Valid examples: 124.124.10.10, 124.124.0.0/16, 124.124.0.0/255.255.0.0" + end else - mediaobject.read_users += [context["add_#{title}"].strip] + if create_lease + begin + mediaobject.governing_policies += [ Lease.create(begin_time: begin_time, end_time: end_time, read_groups: [val]) ] + rescue Exception => e + context[:error] = e.message + end + else + mediaobject.read_groups += [val] + end end else context[:error] = "#{title.titleize} can't be blank." end end - if context["remove_#{title}"].present? - if ["group", "class"].include? title + limited_access_submit = true + if ["group", "class", "ipaddress"].include? title mediaobject.read_groups -= [context["remove_#{title}"]] else mediaobject.read_users -= [context["remove_#{title}"]] end end end - - mediaobject.visibility = context[:visibility] unless context[:visibility].blank? - - mediaobject.hidden = context[:hidden] == "1" - - mediaobject.save + if context['remove_lease'].present? + limited_access_submit = true + lease = Lease.find( context['remove_lease'] ) + mediaobject.governing_policies.delete( lease ) + lease.destroy + end + unless limited_access_submit + mediaobject.visibility = context[:visibility] unless context[:visibility].blank? + mediaobject.hidden = context[:hidden] == "1" + end + mediaobject.save! #Setup these values in the context because the edit partial is being rendered without running the controller's #edit (VOV-2978) mediaobject.reload context[:users] = mediaobject.read_users context[:groups] = mediaobject.read_groups context[:virtual_groups] = mediaobject.virtual_read_groups + context[:ip_groups] = mediaobject.ip_read_groups + context[:group_leases] = mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="local" } + context[:user_leases] = mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="user" } + context[:virtual_leases] = mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="external" } + context[:ip_leases] = mediaobject.governing_policies.to_a.select { |p| p.class==Lease && p.lease_type=="ip" } context[:addable_groups] = Admin::Group.non_system_groups.reject { |g| context[:groups].include? g.name } context[:addable_courses] = Course.all.reject { |c| context[:virtual_groups].include? c.context_id } - context end end diff --git a/app/models/admin/collection.rb b/app/models/admin/collection.rb index 6cb6d10893..00ea2a440e 100644 --- a/app/models/admin/collection.rb +++ b/app/models/admin/collection.rb @@ -31,12 +31,12 @@ class Admin::Collection < ActiveFedora::Base sds.field :description, :string sds.field :dropbox_directory_name end - has_metadata name: 'inheritedRights', type: Hydra::Datastream::InheritableRightsMetadata + has_metadata name: 'inheritedRights', type: Hydra::Datastream::InheritableRightsMetadata, autocreate: true has_metadata name: 'defaultRights', type: Hydra::Datastream::NonIndexedRightsMetadata, autocreate: true validates :name, :uniqueness => { :solr_name => 'name_sim'}, presence: true validates :unit, presence: true, inclusion: { in: Proc.new{ Admin::Collection.units } } - validates :managers, length: {minimum: 1, message: 'Collection requires at least one manager'} + validates :managers, length: {minimum: 1, message: "list can't be empty."} has_attributes :name, datastream: :descMetadata, multiple: false has_attributes :unit, datastream: :descMetadata, multiple: false @@ -45,7 +45,7 @@ class Admin::Collection < ActiveFedora::Base delegate :read_groups, :read_groups=, :read_users, :read_users=, :visibility, :visibility=, :hidden?, :hidden=, - :local_read_groups, :virtual_read_groups, + :local_read_groups, :virtual_read_groups, :ip_read_groups, to: :defaultRights, prefix: :default around_save :reindex_members, if: Proc.new{ |c| c.name_changed? or c.unit_changed? } @@ -121,7 +121,7 @@ def add_depositor user self.read_users += [user] self.inherited_edit_users += [user] else - raise "UserIsEditor" + raise ArgumentError.new("UserIsEditor") end end @@ -178,6 +178,25 @@ def to_solr(solr_doc=Hash.new, *args) solr_doc end + def as_json(options={}) + { + id: pid, + name: name, + unit: unit, + description: description, + object_count: { + total: media_objects.count, + published: media_objects.reject{|mo| !mo.published?}.count, + unpublished: media_objects.reject{|mo| mo.published?}.count + }, + roles: { + managers: managers, + editors: editors, + depositors: depositors + } + } + end + def dropbox Avalon::Dropbox.new( dropbox_absolute_path, self ) end @@ -186,6 +205,10 @@ def dropbox_absolute_path( name = nil ) File.join(Avalon::Configuration.lookup('dropbox.path'), name || dropbox_directory_name) end + def media_objects_to_json + media_objects.collect{|mo| [mo.pid, mo.to_json] }.to_h + end + private diff --git a/app/models/avalon_annotation.rb b/app/models/avalon_annotation.rb new file mode 100644 index 0000000000..b98aa6c8e4 --- /dev/null +++ b/app/models/avalon_annotation.rb @@ -0,0 +1,126 @@ +# An extension of the ActiveAnnotations gem to include Avalon specific information in the Annotation +# Sets defaults for the annotation using information from the master_file and includes solrization of the annotation +# @since 5.0.0 +class AvalonAnnotation < ActiveAnnotations::Annotation + after_save :post_to_solr + after_destroy :delete_from_solr + + alias_method :comment, :content + alias_method :comment=, :content= + + alias_method :title, :label + alias_method :title=, :label= + + validates :master_file, :title, :start_time, presence: true + validates :start_time, numericality: { greater_than_or_equal_to: 0, message: "must be greater than or equal to 0" } + validates :end_time, numericality: { + greater_than: Proc.new { |a| Float(a.start_time) rescue 0 }, + less_than_or_equal_to: Proc.new { |a| a.max_time }, + message: "must be between start time and end of section" + } + + after_initialize do + selector_default! + title_default! + end + + # This function determines the max time for a masterfile and its derivatives + # Used for determining the maximum possible end time for an annotation + # @return [float] the largest end time or -1 if no durations present + def max_time + max = -1 + max = master_file.duration.to_f if master_file.duration.present? + master_file.derivatives.each do |derivative| + max = derivative.duration.to_f if derivative.duration.present? && derivative.duration.to_f > max + end + max + end + + # Mixs in in Avalon specific information from the master_file to a rdf annonation prior and creates a solr document + # @return [Hash] a hash capable of submission to solr + def to_solr + solr_hash = {} + # TODO: User Key via parsing of User URI + #byebug + solr_hash[:id] = solr_id + solr_hash[:title_ssi] = title + solr_hash[:master_file_uri_ssi] = master_file.rdf_uri + solr_hash[:master_file_rdf_type_ssi] = master_file.rdf_type + solr_hash[:start_time_fsi] = start_time + solr_hash[:end_time_fsi] = end_time + solr_hash[:mediafragment_uri_ssi] = mediafragment_uri + solr_hash[:comment_ssi] = comment unless comment.nil? + solr_hash[:referenced_source_type_ssi] = 'MasterFile' + solr_hash[:reference_type_ssi] = 'MediaFragment' + solr_hash + end + + # Solrize the Avalon Annotation in the application's solr core + def post_to_solr + ActiveFedora::SolrService.add(to_solr, softCommit: true) + end + + # Delete the solr document of an Avalon Annotation that has been deleted + def delete_from_solr + ActiveFedora::SolrService.instance.conn.delete_by_id(solr_id, softCommit: true) + end + + # Find the annotation's position on a playlist + # This returns with 1, not 0, as the array start point due to the acts as order gems used on playlist item + # @param [Int] playlist_id The ID of the playlist + # @return [Int] the position + # @return [Nil] if the annotation is not on the specified playlist + def playlist_position(playlist_id) + p_item = PlaylistItem.where(playlist_id: playlist_id, annotation_id: id)[0] + return p_item if p_item.nil? + p_item['position'] + end + + # Return the uuid of an active annotaton, with the urn:uuid removed + # @return [String] the uuid of the annotation + def solr_id + uuid.split(':').last + end + + # Sets the default selector to a start time of 0 and an end time of the master file length + def selector_default! + self.start_time = 0 if self.start_time.nil? + if self.end_time.nil? + if master_file.present? && master_file.duration.present? + self.end_time = master_file.duration + else + self.end_time = 1 + end + end + end + + # Set the default title to be the label of the master_file + def title_default! + self.title = master_file.embed_title if self.title.nil? && master_file.present? + end + + # Sets the class variable @master_file by finding the master referenced in the source uri + def master_file + @master_file ||= MasterFile.find(self.source.split('/').last) if self.source + end + + def master_file=(value) + @master_file = value + self.source = @master_file + @master_file + end + + # Calcuates the mediafragment_uri based on either the internal fragment value or start and end times + # @return [String] the uri with time bounding + def mediafragment_uri + master_file.rdf_uri + "?#{internal.fragment_value.object}" + rescue + master_file.rdf_uri + "?t=#{start_time},#{end_time}" + end + + def duration + duration = (end_time-start_time)/1000 + Time.at(duration).utc.strftime(duration<3600?'%M:%S':'%H:%M:%S') + end + +end diff --git a/app/models/caching_simple_datastream.rb b/app/models/caching_simple_datastream.rb new file mode 100644 index 0000000000..b56dc832bc --- /dev/null +++ b/app/models/caching_simple_datastream.rb @@ -0,0 +1,57 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +class CachingSimpleDatastream + class FieldError < Exception; end + + class FieldCollector + attr :fields + def field(name, type) + @fields ||= {} + fields[name] = type + end + end + + def self.create(klass) + cache_class = Class.new(ActiveFedora::SimpleDatastream) do + class_attribute :owner_class + + def self.defined_attributes(ds) + @defined_attributes ||= {} + if @defined_attributes[ds].nil? + fc = FieldCollector.new + self.owner_class.ds_specs[ds][:block].call(fc) + @defined_attributes[ds] = fc.fields + end + @defined_attributes[ds] + end + + def self.type(field) + field_def = self.owner_class.defined_attributes[field.to_s] + raise FieldError, "Unknown field `#{field}` for #{self.owner_class.name}" if field_def.nil? + self.defined_attributes(field_def.dsid)[field.to_sym] + end + + def primary_solr_name(field) + ActiveFedora::SolrService.solr_name(field, type: self.type(field)) + end + + def type(field) + self.class.type(field) + end + end + cache_class.owner_class = klass + cache_class + end +end diff --git a/app/models/concerns/virtual_groups.rb b/app/models/concerns/virtual_groups.rb index 838e73a0e9..b4fde60fbc 100644 --- a/app/models/concerns/virtual_groups.rb +++ b/app/models/concerns/virtual_groups.rb @@ -21,8 +21,12 @@ def local_read_groups self.read_groups.select {|g| Admin::Group.exists? g} end + def ip_read_groups + self.read_groups.select {|g| IPAddr.new(g) rescue false } + end + def virtual_read_groups - self.read_groups - ["public", "registered"] - local_read_groups + self.read_groups - ["public", "registered"] - local_read_groups - ip_read_groups end end end diff --git a/app/models/derivative.rb b/app/models/derivative.rb index f525314e06..97cb6948b6 100644 --- a/app/models/derivative.rb +++ b/app/models/derivative.rb @@ -29,36 +29,36 @@ class Derivative < ActiveFedora::Base # The only meaningful value at the moment is the url, which points to # the stream location. The other two are just stored until a migration # strategy is required. - has_metadata name: "descMetadata", :type => ActiveFedora::SimpleDatastream do |d| + has_metadata name: "descMetadata", :type => CachingSimpleDatastream.create(self), autocreate: true do |d| d.field :location_url, :string d.field :hls_url, :string d.field :duration, :string d.field :track_id, :string d.field :hls_track_id, :string + d.field :managed, :boolean end has_metadata name: 'derivativeFile', type: UrlDatastream - has_attributes :location_url, :hls_url, :duration, :track_id, :hls_track_id, datastream: :descMetadata, multiple: false + has_attributes :location_url, :hls_url, :duration, :track_id, :hls_track_id, :managed, datastream: :descMetadata, multiple: false has_metadata name: 'encoding', type: EncodingProfileDocument - before_destroy do - begin - encode = masterfile.encoder_class.find(masterfile.workflow_id) - encode.remove_output!(track_id) if track_id.present? - encode.remove_output!(hls_track_id) if hls_track_id.present? && track_id != hls_track_id - rescue Exception => e - logger.warn "Error deleting derivative: #{e.message}" - end + before_destroy :retract_distributed_files! + + def initialize(attrs = nil) + attrs ||= {} + attrs[:managed] = true + super(attrs) end - def self.from_output(dists) + def self.from_output(dists, managed=true) #output is an array of 1 or more distributions of the same derivative (e.g. file and HLS segmented file) - hls_output = dists.delete(dists.find {|o| o[:url].ends_with? "m3u8" }) + hls_output = dists.delete(dists.find {|o| o[:url].ends_with? "m3u8" or ( o[:hls_url].present? and o[:hls_url].ends_with? "m3u8" ) }) output = dists.first || hls_output derivative = Derivative.new + derivative.managed = managed derivative.track_id = output[:id] derivative.duration = output[:duration] derivative.encoding.mime_type = output[:mime_type] @@ -72,17 +72,20 @@ def self.from_output(dists) if hls_output derivative.hls_track_id = hls_output[:id] - derivative.hls_url = hls_output[:url] + derivative.hls_url = hls_output[:hls_url].present? ? hls_output[:hls_url] : hls_output[:url] end - + derivative.location_url = output[:url] derivative.absolute_location = output[:url] + derivative end def set_streaming_locations! - path = URI.parse(absolute_location).path - self.location_url = Avalon::StreamMapper.map(path,'rtmp',self.format) - self.hls_url = Avalon::StreamMapper.map(path,'http',self.format) + if !!self.managed + path = URI.parse(absolute_location).path + self.location_url = Avalon::StreamMapper.map(path,'rtmp',self.format) + self.hls_url = Avalon::StreamMapper.map(path,'http',self.format) + end self end @@ -114,7 +117,7 @@ def format "audio" else "other" - end + end end def to_solr(solr_doc = Hash.new) @@ -122,4 +125,16 @@ def to_solr(solr_doc = Hash.new) solr_doc['stream_path_ssi'] = location_url.split(/:/).last if location_url.present? solr_doc end + + private + def retract_distributed_files! + begin + encode = masterfile.encoder_class.find(masterfile.workflow_id) + encode.remove_output!(track_id) if track_id.present? + encode.remove_output!(hls_track_id) if hls_track_id.present? && track_id != hls_track_id + rescue Exception => e + logger.warn "Error deleting derivative: #{e.message}" + end + end + end diff --git a/app/models/file_upload_step.rb b/app/models/file_upload_step.rb index 493e32952a..82a07cfcab 100644 --- a/app/models/file_upload_step.rb +++ b/app/models/file_upload_step.rb @@ -1,22 +1,22 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- require 'avalon/dropbox' class FileUploadStep < Avalon::Workflow::BasicStep - def initialize(step = 'file-upload', - title = "Manage file(s)", + def initialize(step = 'file-upload', + title = "Manage file(s)", summary = "Associated bitstreams", template = 'file_upload') super @@ -27,7 +27,7 @@ def initialize(step = 'file-upload', # them into a variable that can be referred to later def before_step context dropbox_files = context[:mediaobject].collection.dropbox.all - context[:dropbox_files] = dropbox_files + context[:dropbox_files] = dropbox_files context end @@ -38,8 +38,8 @@ def after_step context def execute context deleted_parts = update_master_files context context[:notice] = "Several clean up jobs have been sent out. Their statuses can be viewed by your sysadmin at #{ Avalon::Configuration.lookup('matterhorn.cleanup_log') }" unless deleted_parts.empty? - - # Reloads mediaobject.parts, should use .reload when we update hydra-head + + # Reloads mediaobject.parts, should use .reload when we update hydra-head media = MediaObject.find(context[:mediaobject].pid) unless media.parts.empty? media.save(validate: false) @@ -71,6 +71,7 @@ def update_master_files(context) selected_part.label = part[:label] unless part[:label].nil? selected_part.permalink = part[:permalink] unless part[:permalink].nil? selected_part.poster_offset = part[:poster_offset] unless part[:poster_offset].nil? + selected_part.date_digitized = part[:date_digitized].blank? ? nil : part[:date_digitized] unless part[:date_digitized].nil? unless selected_part.save context[:error] ||= [] context[:error] << "#{selected_part.pid}: #{selected_part.errors.to_a.first.gsub(/(\d+)/) { |m| m.to_i.to_hms }}" diff --git a/app/models/lease.rb b/app/models/lease.rb new file mode 100644 index 0000000000..e679eda91c --- /dev/null +++ b/app/models/lease.rb @@ -0,0 +1,106 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +# Base Class for controlling access to Avalon content or groups via date restricted groups (Lease) +# @since 5.0.0 +# @attr [DateTime] begin_time The date and time a lease begins +# Set to today on save if it is has not been set +# Always set to the supplied date (or default of today) and 00:00:00 UTC for the time on save (start of that day UTC time) +# @attr [DateTime] end_time The date and time a lease ends +# Must be set to save the object +# Always set to the supplied date and 23:59:59 UTC for the time on save (end of the day). +class Lease < ActiveFedora::Base + before_save :apply_default_begin_time, :ensure_end_time_present, :validate_dates, :format_times + + has_and_belongs_to_many :media_objects, class_name: 'MediaObject', property: :has_member, inverse_of: :is_governed_by + + has_metadata name: 'inheritedRights', type: Hydra::Datastream::InheritableRightsMetadata + has_metadata name: 'descMetadata', type: ActiveFedora::SimpleDatastream do |sds| + sds.field :begin_time, :time + sds.field :end_time, :time + end + has_attributes :begin_time, :end_time, datastream: :descMetadata, multiple: false + + delegate :read_groups, :read_groups=, :read_users, :read_users=, to: :inheritedRights + + # Determines if the lease is currently active (today is between the begin and end time) + # @return [Boolean] returns true if the lease is active + def lease_is_active? + Date.today >= begin_time && Date.today <= end_time + end + + # Converts the object to a solr hash for indexing + # @param [Hash] solr_doc A hash passable to solr, an empty one is created by default_field_mapper + # + def to_solr(solr_doc = {}, *args) + solr_doc = super(solr_doc) + Solrizer.insert_field(solr_doc, 'begin_time', begin_time, :sortable) + Solrizer.insert_field(solr_doc, 'end_time', end_time, :sortable) + # Clear out incorrectly named dynamic fields + solr_doc.delete('begin_time_dtsim') + solr_doc.delete('end_time_dtsim') + solr_doc + end + + # A before_save action that sets the times to the start and end of the day in the UTC timezome and formats them as is8061 + def format_times + self.begin_time = start_of_day(begin_time) + self.end_time = end_of_day(end_time) + end + + # A before_save action that sets begin_time to Date.today if a begin_time has not been supplied + def apply_default_begin_time + self.begin_time = start_of_day(Date.today) if begin_time.nil? + end + + # A before_save action that raises an ArgumentError if end_time is not set + # @raise [ArgumentError] raised if end_time is nil + def ensure_end_time_present + fail ArgumentError, 'No end_time supplied' if end_time.nil? + end + + # A before_save action that ensures begin_time preceeds end_time + # @raise [ArgumentError] raised if end_time is before or equal to begin_time + def validate_dates + apply_default_begin_time + # We use <= since we have times on our dates, so if they're equal it means a lease with a duration of 0 seconds + fail ArgumentError, 'end_time predates begin_time' if end_time <= begin_time + end + + # Take a supplied date and format it in iso8601 with the time portion set to 00:00:00 UTC + # @param [Date, Time, String] a date or time object or a string that DateTime can parts_with_order + # @return [String] supplied date and time with the passed date and time portion set to 00:00:00 UTC time in the iso8601 format + def start_of_day(time) + DateTime.parse(time.to_s).utc.beginning_of_day.iso8601 + end + + # Take a supplied date and format it in iso8601 with the time portion set to 11:59:59 UTC + # @param [Date, Time, String] a date or time object or a string that DateTime can parts_with_order + # @return [String] supplied date and time with the passed date and time portion set to 11:59:59 UTC time in the iso8601 format + def end_of_day(time) + DateTime.parse(time.to_s).utc.end_of_day.iso8601 + end + + # Calculate lease type by inspecting read_users and read_groups values, each of which are assumed to contain a single value + # @return [String] "user" for user lease, "ip" for ip group lease, "local" for local group lease, "external" for external group lease, nil for others + def lease_type + return "user" if self.read_users.present? + group = self.read_groups.first + return nil if group.nil? + return "ip" if IPAddr.new(group) rescue false + return "local" if Admin::Group.exists? group + return "external" + end + +end diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 34c1f279ba..f6c9103512 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -19,6 +19,8 @@ require 'avalon/m3u8_reader' class MasterFile < ActiveFedora::Base + + include ActiveFedora::Associations include Hydra::ModelMethods include Hydra::AccessControls::Permissions @@ -28,13 +30,13 @@ class MasterFile < ActiveFedora::Base include VersionableModel has_metadata name: "structuralMetadata", :type => StructuralMetadata - + WORKFLOWS = ['fullaudio', 'avalon', 'avalon-skip-transcoding', 'avalon-skip-transcoding-audio'] belongs_to :mediaobject, :class_name=>'MediaObject', :property=>:is_part_of has_many :derivatives, :class_name=>'Derivative', :property=>:is_derivation_of - has_metadata name: 'descMetadata', :type => ActiveFedora::SimpleDatastream do |d| + has_metadata name: 'descMetadata', :type => CachingSimpleDatastream.create(self) do |d| d.field :file_location, :string d.field :file_checksum, :string d.field :file_size, :string @@ -44,9 +46,11 @@ class MasterFile < ActiveFedora::Base d.field :file_format, :string d.field :poster_offset, :string d.field :thumbnail_offset, :string + d.field :date_digitized, :string + d.field :physical_description, :string end - has_metadata name: 'mhMetadata', :type => ActiveFedora::SimpleDatastream do |d| + has_metadata name: 'mhMetadata', :type => CachingSimpleDatastream.create(self) do |d| d.field :workflow_id, :string d.field :workflow_name, :string d.field :percent_complete, :string @@ -61,13 +65,23 @@ class MasterFile < ActiveFedora::Base has_metadata name: 'masterFile', type: UrlDatastream - has_attributes :file_location, :file_checksum, :file_size, :duration, :display_aspect_ratio, :original_frame_size, :file_format, :poster_offset, :thumbnail_offset, datastream: :descMetadata, multiple: false + has_attributes :file_location, :physical_description, :file_checksum, :file_size, :duration, :display_aspect_ratio, :original_frame_size, :file_format, :poster_offset, :thumbnail_offset, :date_digitized, datastream: :descMetadata, multiple: false has_attributes :workflow_id, :workflow_name, :encoder_classname, :percent_complete, :percent_succeeded, :percent_failed, :status_code, :operation, :error, :failures, datastream: :mhMetadata, multiple: false has_file_datastream name: 'thumbnail' has_file_datastream name: 'poster' + has_file_datastream name: 'captions' validates :workflow_name, presence: true, inclusion: { in: Proc.new{ WORKFLOWS } } + validates_each :date_digitized do |record, attr, value| + unless value.nil? + begin + Time.parse(value) + rescue Exception => err + record.errors.add attr, err.message + end + end + end validates_each :poster_offset, :thumbnail_offset do |record, attr, value| unless value.nil? or value.to_i.between?(0,record.duration.to_i) record.errors.add attr, "must be between 0 and #{record.duration}" @@ -76,11 +90,10 @@ class MasterFile < ActiveFedora::Base #validates :file_format, presence: true, exclusion: { in: ['Unknown'], message: "The file was not recognized as audio or video." } has_model_version 'R3' - before_save 'update_stills_from_offset!' - around_save :update_media_object!, if: Proc.new { |mf| mf.duration_changed? or mf.file_location_changed? or mf.file_format_changed? } + before_save :update_stills_from_offset! define_hooks :after_processing - + after_processing :post_processing_file_management after_processing :update_ingest_batch @@ -96,22 +109,13 @@ class MasterFile < ActiveFedora::Base UNKNOWN_TYPES = ["application/octet-stream", "application/x-upload-data"] QUALITY_ORDER = { "high" => 1, "medium" => 2, "low" => 3 } END_STATES = ['CANCELLED', 'COMPLETED', 'FAILED'] - + EMBED_SIZE = {:medium => 600} AUDIO_HEIGHT = 50 - def update_media_object! - yield - return if mediaobject.nil? - mediaobject.set_duration! - mediaobject.set_media_types! - mediaobject.set_resource_types! - mediaobject.save(validate: false) - end - def save_parent unless mediaobject.nil? - mediaobject.save(validate: false) + mediaobject.save(validate: false) end end @@ -133,7 +137,7 @@ def set_workflow( workflow = nil ) workflow = case self.file_format when 'Moving image' 'avalon-skip-transcoding' - when 'Sound' + when 'Sound' 'avalon-skip-transcoding-audio' else nil @@ -165,7 +169,7 @@ def mediaobject=(mo) end end - def destroy + def destroy mo = self.mediaobject self.mediaobject = nil @@ -176,7 +180,7 @@ def destroy self.derivatives.map(&:destroy) clear_association_cache - + super #Only save the media object if the master file was successfully deleted @@ -219,12 +223,12 @@ def succeeded? def stream_details(token,host=nil) flash, hls = [], [] - derivatives.each do |d| + ActiveFedora::SolrService.reify_solr_results(derivatives.load_from_solr, load_from_solr: true).each do |d| common = { quality: d.encoding.quality.first, mimetype: d.encoding.mime_type.first, - format: d.format } + format: d.format } flash << common.merge(url: Avalon.rehost(d.tokenized_url(token, false),host)) - hls << common.merge(url: Avalon.rehost(d.tokenized_url(token, true),host)) + hls << common.merge(url: Avalon.rehost(d.tokenized_url(token, true),host)) end # Sorts the streams in order of quality, note: Hash order only works in Ruby 1.9 or later @@ -232,6 +236,8 @@ def stream_details(token,host=nil) hls = sort_streams hls poster_path = Rails.application.routes.url_helpers.poster_master_file_path(self) unless poster.new? + captions_path = Rails.application.routes.url_helpers.captions_master_file_path(self) unless captions.empty? + captions_format = self.captions.mimeType # Returns the hash { @@ -239,10 +245,13 @@ def stream_details(token,host=nil) label: label, is_video: is_video?, poster_image: poster_path, - embed_code: embed_code(EMBED_SIZE[:medium], {urlappend: '/embed'}), - stream_flash: flash, + embed_code: embed_code(EMBED_SIZE[:medium], {urlappend: '/embed'}), + stream_flash: flash, stream_hls: hls, - duration: (duration.to_f / 1000).round + captions_path: captions_path, + captions_format: captions_format, + duration: (duration.to_f / 1000).round, + embed_title: embed_title } end @@ -259,7 +268,7 @@ def embed_code(width, permalink_opts = {}) end height = is_video? ? (width/display_aspect_ratio.to_f).floor : AUDIO_HEIGHT "" - rescue + rescue "" end end @@ -306,11 +315,20 @@ def update_progress_with_encode!(encode) end def update_progress_on_success!(encode) - outputs_by_quality = encode.output.group_by {|o| o[:label]} - + #Set date ingested to now if it wasn't preset (by batch, for example) + #TODO pull this from the encode + self.date_digitized ||= Time.now.utc.iso8601 + + update_derivatives(encode.output) + run_hook :after_processing + end + + def update_derivatives(output,managed=true) + outputs_by_quality = output.group_by {|o| o[:label]} + outputs_by_quality.each_pair do |quality, outputs| existing = derivatives.to_a.find {|d| d.encoding.quality.first == quality} - d = Derivative.from_output(outputs) + d = Derivative.from_output(outputs,managed) d.masterfile = self if d.save && existing existing.delete @@ -318,8 +336,6 @@ def update_progress_on_success!(encode) end save - - run_hook :after_processing end alias_method :'_poster_offset=', :'poster_offset=' @@ -345,7 +361,7 @@ def set_image_offset(type, value) else value.to_i end - + return milliseconds if milliseconds == self.send("#{type}_offset").to_i @stills_to_update ||= [] @@ -423,7 +439,7 @@ def file_location=(value) def encoder_class find_encoder_class(encoder_classname) || find_encoder_class(workflow_name.to_s.classify) || ActiveEncode::Base end - + def encoder_class=(value) if value.nil? mhMetadata.encoder_classname = nil @@ -433,7 +449,25 @@ def encoder_class=(value) raise ArgumentError, '#encoder_class must be a descendant of ActiveEncode::Base' end end - + + def structural_metadata_labels + structuralMetadata.xpath('//@label').collect{|a|a.value} + end + + # Supplies the route to the master_file as an rdf formatted URI + # @return [String] the route as a uri + # @example uri for a mf on avalon.iu.edu with a pid of: avalon:1820 + # "my_masterfile.rdf_uri" #=> "https://www.avalon.iu.edu/master_files/avalon:1820" + def rdf_uri + master_file_url(pid) + end + + # Returns the dctype of the master_file + # @return [String] either 'dctypes:MovingImage' or 'dctypes:Sound' + def rdf_type + is_video? ? 'dctypes:MovingImage' : 'dctypes:Sound' + end + protected def mediainfo @@ -531,13 +565,13 @@ def calculate_percent_complete matterhorn_response when /^Distributing/ then :distribution else :other end - { :description => op['description'], :state => op['state'], :type => type } + { :description => op['description'], :state => op['state'], :type => type } } result = Hash.new { |h,k| h[k] = 0 } operations.each { |op| op[:pct] = (totals[op[:type]].to_f / operations.select { |o| o[:type] == op[:type] }.count.to_f) - state = op[:state].downcase.to_sym + state = op[:state].downcase.to_sym result[state] += op[:pct] result[:complete] += op[:pct] if END_STATES.include?(op[:state]) } @@ -559,7 +593,7 @@ def saveOriginal(file, original_name=nil) File.rename(realpath, newpath) end self.file_location = newpath - else + else self.file_location = realpath end self.file_size = file.size.to_s @@ -586,7 +620,7 @@ def reloadTechnicalMetadata! rescue nil end - + unless mediainfo.video.streams.empty? display_aspect_ratio_s = mediainfo.video.streams.first.display_aspect_ratio if ':'.in? display_aspect_ratio_s @@ -619,7 +653,7 @@ def post_processing_move_filename(oldpath, options={}) prefix = options[:pid].gsub(":","_") if oldpath.start_with?(prefix) oldpath - else + else "#{prefix}-#{File.basename(oldpath)}" end end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 5af4d84845..0cc7969f19 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -23,41 +23,40 @@ class MediaObject < ActiveFedora::Base include VersionableModel include Permalink require 'avalon/controlled_vocabulary' - + + include Kaminari::ActiveFedoraModelExtension + # has_relationship "parts", :has_part has_many :parts, :class_name=>'MasterFile', :property=>:is_part_of - belongs_to :governing_policy, :class_name=>'Admin::Collection', :property=>:is_governed_by + has_and_belongs_to_many :governing_policies, :class_name=>'ActiveFedora::Base', :property=>:is_governed_by belongs_to :collection, :class_name=>'Admin::Collection', :property=>:is_member_of_collection - has_metadata name: "descMetadata", type: ModsDocument + has_metadata name: "descMetadata", type: ModsDocument after_create :after_create - - # Before saving put the pieces into the right order and validate to make sure that - # there are no syntactic errors - before_save 'descMetadata.ensure_identifier_exists!' - before_save 'descMetadata.update_change_date!' - before_save 'descMetadata.reorder_elements!' - before_save 'descMetadata.remove_empty_nodes!' - before_save 'update_permalink_and_dependents' + before_save :normalize_desc_metadata! + before_save :update_dependent_properties! + before_save :update_permalink, if: Proc.new { |mo| mo.persisted? && mo.published? } + after_save :update_dependent_permalinks, if: Proc.new { |mo| mo.persisted? && mo.published? } + after_save :remove_bookmarks + has_model_version 'R4' # Call custom validation methods to ensure that required fields are present and # that preferred controlled vocabulary standards are used - + # Guarantees that the record is minimally complete - ie that within the descriptive - # metadata the title, creator, date of creation, and identifier fields are not + # metadata the title, creator, date of creation, and identifier fields are not # blank. Since identifier is set automatically we only need to worry about creator, # title, and date of creation. validates :title, :presence => true - validate :validate_creator validate :validate_language validates :date_issued, :presence => true validate :report_missing_attributes validates :collection, presence: true - validates :governing_policy, presence: true + validates :governing_policies, presence: true validate :validate_related_items validate :validate_dates validate :validate_note_type @@ -74,12 +73,6 @@ def validate_related_items Array(related_item_url).each{|i|errors.add(:related_item_url, "Bad URL") unless i[:url] =~ URI::regexp(%w(http https))} end - def validate_creator - if Array(creator).select { |c| c.present? }.empty? - errors.add(:creator, I18n.t("errors.messages.blank")) - end - end - def validate_dates [:date_created, :date_issued, :copyright_date].each do |d| if self.send(d).present? && Date.edtf(self.send(d)).nil? @@ -124,7 +117,7 @@ def klass_attribute_to_metadata_attribute_map } end - + has_attributes :avalon_uploader, datastream: :DC, at: [:creator], multiple: false has_attributes :avalon_publisher, datastream: :DC, at: [:publisher], multiple: false # Delegate variables to expose them for the forms @@ -156,12 +149,13 @@ def klass_attribute_to_metadata_attribute_map has_attributes :language, datastream: :descMetadata, at: [:language], multiple: true has_attributes :terms_of_use, datastream: :descMetadata, at: [:terms_of_use], multiple: false has_attributes :table_of_contents, datastream: :descMetadata, at: [:table_of_contents], multiple: true - has_attributes :physical_description, datastream: :descMetadata, at: [:physical_description], multiple: false + has_attributes :physical_description, datastream: :descMetadata, at: [:physical_description], multiple: true has_attributes :other_identifier, datastream: :descMetadata, at: [:other_identifier], multiple: true has_attributes :record_identifier, datastream: :descMetadata, at: [:record_identifier], multiple: true - + has_metadata name:'displayMetadata', :type => ActiveFedora::SimpleDatastream do |sds| sds.field :duration, :string + sds.field :avalon_resource_type, :string end has_metadata name:'sectionsMetadata', :type => ActiveFedora::SimpleDatastream do |sds| @@ -169,6 +163,7 @@ def klass_attribute_to_metadata_attribute_map end has_attributes :duration, datastream: :displayMetadata, multiple: false + has_attributes :avalon_resource_type, datastream: :displayMetadata, multiple: true has_attributes :section_pid, datastream: :sectionsMetadata, multiple: true accepts_nested_attributes_for :parts, :allow_destroy => true @@ -192,13 +187,36 @@ def parts_with_order_remove part def parts_with_order= master_files self.section_pid = master_files.map(&:pid) + self.sectionsMetadata.save if self.persisted? + self.section_pid + end + + def parts_with_order(opts = {}) + pids_with_order = section_pid + if !!opts[:load_from_solr] + return [] if pids_with_order.empty? + pid_list = pids_with_order.uniq.map { |pid| RSolr.solr_escape(pid) }.join ' OR ' + solr_results = ActiveFedora::SolrService.query("id\:(#{pid_list})", rows: pids_with_order.length) + mfs = ActiveFedora::SolrService.reify_solr_results(solr_results, load_from_solr: true) + pids_with_order.map { |pid| mfs.find { |mf| mf.pid == pid }} + else + pids_with_order.map{|pid| MasterFile.find(pid)} + end + end + + def _section_pid + self.sectionsMetadata.get_values(:section_pid) end - def parts_with_order - self.section_pid.map{|pid| MasterFile.find(pid)} + def section_pid + ordered_pids = self._section_pid + missing_from_order = self.part_ids - ordered_pids + missing_parts = ordered_pids - self.part_ids + ordered_pids + missing_from_order - missing_parts end def section_pid=( pids ) + self.section_pid_will_change! self.sectionsMetadata.find_by_terms(:section_pid).each &:remove self.sectionsMetadata.update_values(['section_pid'] => pids) end @@ -210,8 +228,9 @@ def collection= co # TODO: Removes existing association self._collection= co - self.governing_policy = co - if (self.read_groups + self.read_users + self.discover_groups + self.discover_users).empty? + self.governing_policies -= [self.governing_policies.to_a.find {|gp| gp.is_a? Admin::Collection }] + self.governing_policies += [co] + if (self.read_groups + self.read_users + self.discover_groups + self.discover_users).empty? self.rightsMetadata.content = co.defaultRights.content unless co.nil? end end @@ -220,7 +239,7 @@ def collection= co # omit the status which will default to unpublished. This makes the act # of publishing _explicit_ instead of an accidental side effect. def publish!(user_key) - self.avalon_publisher = user_key.blank? ? nil : user_key + self.avalon_publisher = user_key.blank? ? nil : user_key self.save(validate: false) end @@ -251,18 +270,23 @@ def find_metadata_attribute(attribute) def update_datastream(datastream = :descMetadata, values = {}) missing_attributes.clear # Special case the identifiers and their types - if values[:bibliographic_id] - values[:bibliographic_id] = {value: values[:bibliographic_id], attributes: values.delete(:bibliographic_id_label)} + if values[:bibliographic_id].present? and values[:bibliographic_id_label].present? + values[:bibliographic_id] = {value: values[:bibliographic_id], attributes: values[:bibliographic_id_label]} end - if values[:related_item_url] and values[:related_item_label] - values[:related_item_url] = values[:related_item_url].zip(values.delete(:related_item_label)) + values.delete(:bibliographic_id_label) + if values[:related_item_url].present? and values[:related_item_label].present? + values[:related_item_url] = values[:related_item_url].zip(values[:related_item_label]) end - if values[:note] - values[:note]=values[:note].zip(values.delete(:note_type)).map{|v| {value: v[0], attributes: v[1]}} + values.delete(:related_item_label) + if values[:note].present? and values[:note_type].present? + values[:note]=values[:note].zip(values[:note_type]).map{|v| {value: v[0], attributes: v[1]}} end - if values[:other_identifier] - values[:other_identifier]=values[:other_identifier].zip(values.delete(:other_identifier_type)).map{|v| {value: v[0], attributes: v[1]}} + values.delete(:note_type) + if values[:other_identifier].present? and values[:other_identifier_type].present? + values[:other_identifier]=values[:other_identifier].zip(values[:other_identifier_type]).map{|v| {value: v[0], attributes: v[1]}} end + values.delete(:other_identifier_type) + values.each do |k, v| # First remove all blank attributes in arrays v.keep_if { |item| not item.blank? } if v.instance_of?(Array) @@ -279,7 +303,7 @@ def update_datastream(datastream = :descMetadata, values = {}) elsif v.is_a?(Array) and v.first.is_a?(Hash) vals = [] attrs = [] - + v.each do |entry| vals << entry[:value] attrs << entry[:attributes] @@ -353,14 +377,14 @@ def update_attribute_in_metadata(attribute, value = [], attributes = []) end def set_media_types! - mime_types = parts.reject {|mf| mf.file_location.blank? }.collect { |mf| - Rack::Mime.mime_type(File.extname(mf.file_location)) + mime_types = parts.reject {|mf| mf.file_location.blank? }.collect { |mf| + Rack::Mime.mime_type(File.extname(mf.file_location)) }.uniq update_attribute_in_metadata(:format, mime_types.empty? ? nil : mime_types) end - def set_resource_types! - resource_types = parts.reject {|mf| mf.file_format.blank? }.collect{ |mf| + def set_resource_types! + self.avalon_resource_type = parts.reject {|mf| mf.file_format.blank? }.collect{ |mf| case mf.file_format when 'Moving image' 'moving image' @@ -370,9 +394,29 @@ def set_resource_types! mf.file_format.downcase end }.uniq - update_attribute_in_metadata(:resource_type, resource_types.empty? ? nil : resource_types) end - + + def update_dependent_properties! + self.set_duration! + self.set_media_types! + self.set_resource_types! + end + + def section_labels + all_labels = parts.collect{|mf|mf.structural_metadata_labels << mf.label} + all_labels.flatten.uniq.compact + end + + # Gets all physical descriptions from master files and returns a uniq array + # @return [Array] A unique list of all physical descriptions for the media object + def section_physical_descriptions + all_pds = [] + self.parts.each do |master_file| + all_pds += master_file.descMetadata.physical_description unless master_file.descMetadata.physical_description.nil? + end + all_pds.uniq + end + def to_solr(solr_doc = Hash.new, opts = {}) solr_doc = super(solr_doc, opts) solr_doc[Solrizer.default_field_mapper.solr_name("created_by", :facetable, type: :string)] = self.DC.creator @@ -382,10 +426,21 @@ def to_solr(solr_doc = Hash.new, opts = {}) solr_doc[Solrizer.default_field_mapper.solr_name("unit", :symbol, type: :string)] = collection.unit if collection.present? indexer = Solrizer::Descriptor.new(:string, :stored, :indexed, :multivalued) solr_doc[Solrizer.default_field_mapper.solr_name("read_access_virtual_group", indexer)] = virtual_read_groups + solr_doc[Solrizer.default_field_mapper.solr_name("read_access_ip_group", indexer)] = collect_ips_for_index(ip_read_groups) + solr_doc[Hydra.config.permissions.read.group] ||= [] + solr_doc[Hydra.config.permissions.read.group] += solr_doc[Solrizer.default_field_mapper.solr_name("read_access_ip_group", indexer)] solr_doc["dc_creator_tesim"] = self.creator solr_doc["dc_publisher_tesim"] = self.publisher solr_doc["title_ssort"] = self.title solr_doc["creator_ssort"] = Array(self.creator).join(', ') + solr_doc["date_digitized_sim"] = parts.collect {|mf| mf.date_digitized }.compact.map {|t| Time.parse(t).strftime "%F" } + solr_doc["date_ingested_sim"] = Time.parse(self.create_date).strftime "%F" + #include identifiers for parts + solr_doc["other_identifier_sim"] += parts.collect {|mf| mf.DC.identifier }.flatten + #include labels for parts and their structural metadata + solr_doc["section_label_tesim"] = section_labels + solr_doc['section_physical_description_ssim'] = section_physical_descriptions + #Add all searchable fields to the all_text_timv field all_text_values = [] all_text_values << solr_doc["title_tesi"] @@ -400,7 +455,7 @@ def to_solr(solr_doc = Hash.new, opts = {}) all_text_values << solr_doc["subject_temporal_sim"] all_text_values << solr_doc["genre_sim"] all_text_values << solr_doc["language_sim"] - all_text_values << solr_doc["physical_description_si"] + all_text_values << solr_doc["physical_description_sim"] all_text_values << solr_doc["date_sim"] all_text_values << solr_doc["notes_sim"] all_text_values << solr_doc["table_of_contents_sim"] @@ -410,6 +465,19 @@ def to_solr(solr_doc = Hash.new, opts = {}) return solr_doc end + def as_json(options={}) + { + id: pid, + title: title, + collection: collection.name, + main_contributors: creator, + publication_date: date_created, + published_by: avalon_publisher, + published: published?, + summary: abstract + } + end + # Other validation to consider adding into future iterations is the ability to # validate against a known controlled vocabulary. This one will take some thought # and research as opposed to being able to just throw something together in an ad hoc @@ -424,27 +492,74 @@ def access_control_bulk documents, params media_object.hidden = params[:hidden] if !params[:hidden].nil? media_object.visibility = params[:visibility] unless params[:visibility].blank? # Limited access stuff - ["group", "class", "user"].each do |title| + ["group", "class", "user", "ipaddress"].each do |title| if params["submit_add_#{title}"].present? - if params["#{title}"].present? - if ["group", "class"].include? title - media_object.read_groups += [params["#{title}"].strip] + begin_time = params["add_#{title}_begin"].blank? ? nil : params["add_#{title}_begin"] + end_time = params["add_#{title}_end"].blank? ? nil : params["add_#{title}_end"] + create_lease = begin_time.present? || end_time.present? + + if params[title].present? + val = params[title].strip + if title=='user' + if create_lease + begin + media_object.governing_policies += [ Lease.create(begin_time: begin_time, end_time: end_time, read_users: [val]) ] + rescue Exception => e + errors += [media_object] + end + else + media_object.read_users += [val] + end + elsif title=='ipaddress' + if ( IPAddr.new(val) rescue false ) + if create_lease + begin + media_object.governing_policies += [ Lease.create(begin_time: begin_time, end_time: end_time, read_groups: [val]) ] + rescue Exception => e + errors += [media_object] + end + else + media_object.read_groups += [val] + end + else + context[:error] = "IP Address #{val} is invalid. Valid examples: 124.124.10.10, 124.124.0.0/16, 124.124.0.0/255.255.0.0" + end else - media_object.read_users += [params["#{title}"].strip] + if create_lease + begin + media_object.governing_policies += [ Lease.create(begin_time: begin_time, end_time: end_time, read_groups: [val]) ] + rescue Exception => e + errors += [media_object] + end + else + media_object.read_groups += [val] + end end end end if params["submit_remove_#{title}"].present? - if params["#{title}"].present? - if ["group", "class"].include? title - media_object.read_groups -= [params["#{title}"]] + if params[title].present? + if ["group", "class", "ipaddress"].include? title + media_object.read_groups -= [params[title]] + media_object.governing_policies.each do |policy| + if policy.class==Lease && policy.read_groups.include?(params[title]) + media_object.governing_policies.delete policy + policy.destroy + end + end else - media_object.read_users -= [params["#{title}"]] + media_object.read_users -= [params[title]] + media_object.governing_policies.each do |policy| + if policy.class==Lease && policy.read_users.include?(params[title]) + media_object.governing_policies.delete policy + policy.destroy + end + end end end end end - if media_object.save + if errors.empty? && media_object.save successes += [media_object] else errors += [media_object] @@ -453,7 +568,7 @@ def access_control_bulk documents, params return successes, errors end handle_asynchronously :access_control_bulk - + def update_status_bulk documents, user_key, params errors = [] successes = [] @@ -480,7 +595,7 @@ def update_status_bulk documents, user_key, params return successes, errors end handle_asynchronously :update_status_bulk - + def delete_bulk documents, params errors = [] successes = [] @@ -495,7 +610,7 @@ def delete_bulk documents, params return successes, errors end handle_asynchronously :delete_bulk - + def move_bulk documents, params collection = Admin::Collection.find( params[:target_collection_id] ) errors = [] @@ -508,42 +623,79 @@ def move_bulk documents, params else errors += [media_object] end - end + end return successes, errors end handle_asynchronously :move_bulk end + def update_permalink + ensure_permalink! + unless self.descMetadata.permalink.include? self.permalink + self.descMetadata.permalink = self.permalink + end + end + + class << self + def update_dependent_permalinks pid + mo = self.find(pid) + mo._update_dependent_permalinks + end + handle_asynchronously :update_dependent_permalinks + end + + def _update_dependent_permalinks + self.parts.each do |master_file| + begin + updated = master_file.ensure_permalink! + master_file.save( validate: false ) if updated + rescue + # no-op + # Save is called (uncharacteristically) during a destroy. + end + end + end + + def update_dependent_permalinks + self.class.update_dependent_permalinks self.pid + end + + def _remove_bookmarks + Bookmark.where(document_id: self.pid).each do |b| + b.destroy if ( !User.exists? b.user_id ) or ( Ability.new( User.find b.user_id ).cannot? :read, self ) + end + end + + def remove_bookmarks + self._remove_bookmarks + end + private + # Put the pieces into the right order and validate to make sure that there are no + # syntactic errors + def normalize_desc_metadata! + descMetadata.ensure_identifier_exists! + descMetadata.update_change_date! + descMetadata.reorder_elements! + descMetadata.remove_empty_nodes! + end + def after_create self.DC.identifier = pid save end - + def calculate_duration self.parts.map{|mf| mf.duration.to_i }.compact.sum end - def update_permalink_and_dependents - if self.persisted? && self.published? - ensure_permalink! - self.parts.each do |master_file| - begin - master_file.ensure_permalink! - master_file.save( validate: false ) - rescue - # no-op - # Save is called (uncharacteristically) during a destroy. - end - end - - unless self.descMetadata.permalink.include? self.permalink - self.descMetadata.permalink = self.permalink - end + def collect_ips_for_index ip_strings + ips = ip_strings.collect do |ip| + addr = IPAddr.new(ip) rescue next + addr.to_range.map(&:to_s) end - - true + ips.flatten.compact.uniq || [] end - + end diff --git a/app/models/mods_behaviors.rb b/app/models/mods_behaviors.rb index b3f0dc5177..e4ba700eeb 100644 --- a/app/models/mods_behaviors.rb +++ b/app/models/mods_behaviors.rb @@ -71,7 +71,7 @@ def to_solr(solr_doc=SolrDocument.new) # Right now, everything's English. solr_doc['language_sim'] = gather_terms(self.find_by_terms(:language_text)) solr_doc['language_code_sim'] = gather_terms(self.find_by_terms(:language_code)) - solr_doc['physical_description_si'] = self.find_by_terms(:physical_description).text + solr_doc['physical_description_sim'] = gather_terms(self.find_by_terms(:physical_description)) solr_doc['related_item_url_sim'] = gather_terms(self.find_by_terms(:related_item_url)) solr_doc['related_item_label_sim'] = gather_terms(self.find_by_terms(:related_item_label)) solr_doc['terms_of_use_si'] = self.find_by_terms(:terms_of_use).text @@ -174,7 +174,10 @@ def gather_terms(terms) def gather_years(date) parsed = Date.edtf(date) return Array.new if parsed.nil? - years = if parsed.respond_to?(:map) + years = + if parsed.respond_to?(:unknown?) && parsed.unknown? + ['Unknown'] + elsif parsed.respond_to?(:map) parsed.map(&:year_precision!) parsed.map(&:year) elsif parsed.unspecified?(:year) diff --git a/app/models/mods_document.rb b/app/models/mods_document.rb index a27b07de97..2c2d513df0 100644 --- a/app/models/mods_document.rb +++ b/app/models/mods_document.rb @@ -212,6 +212,7 @@ def populate_from_catalog! bib_id, bib_id_label = nil if new_record.present? old_resource_type = self.resource_type.dup old_media_type = self.media_type.dup + old_other_identifier = self.other_identifier.type.zip(self.other_identifier) self.ng_xml = Nokogiri::XML(new_record) [:genre, :topical_subject, :geographic_subject, :temporal_subject, :occupation_subject, :person_subject, :corporate_subject, :family_subject, @@ -225,11 +226,16 @@ def populate_from_catalog! bib_id, bib_id_label = nil languages = self.language.collect &:strip self.language = nil languages.each { |lang| self.add_language(lang) } + new_other_identifier = self.other_identifier.type.zip(self.other_identifier) + self.other_identifier = nil + (old_other_identifier | new_other_identifier).each do |id_pair| + self.add_other_identifier(id_pair[1], id_pair[0]) + end end + self.add_other_identifier(bib_id, bib_id_label) unless self.other_identifier.type.zip(self.other_identifier).include?([bib_id_label, bib_id]) + self.bibliographic_id = nil + self.add_bibliographic_id(bib_id, bib_id_label) end - self.bibliographic_id = nil - self.add_bibliographic_id(bib_id, bib_id_label) - self.add_other_identifier(bib_id, bib_id_label) # Filter out notes that are not in the configured controlled vocabulary notezip = note.zip note.type diff --git a/app/models/mods_templates.rb b/app/models/mods_templates.rb index fddd33866f..4f1a5a86d2 100644 --- a/app/models/mods_templates.rb +++ b/app/models/mods_templates.rb @@ -51,8 +51,8 @@ def add_uniform_title(title, attrs={}); add_title(title, attrs, type: :unifo end def get_origin_info - node = find_by_terms(:origin_info) - if node.empty? + node = find_by_terms(:origin_info).first + if node.nil? node = ng_xml.root.add_child('') end node diff --git a/app/models/playlist.rb b/app/models/playlist.rb new file mode 100644 index 0000000000..91d6b54e69 --- /dev/null +++ b/app/models/playlist.rb @@ -0,0 +1,73 @@ +class Playlist < ActiveRecord::Base + belongs_to :user + validates :user, presence: true + validates :title, presence: true + validates :comment, length: { maximum: 255 } + validates :visibility, presence: true + validates :visibility, inclusion: { in: proc { [PUBLIC, PRIVATE] } } + + after_initialize :default_values + + has_many :items, -> { order('position ASC') }, class_name: PlaylistItem, dependent: :destroy + has_many :annotations, -> { order('playlist_items.position ASC') }, class_name: AvalonAnnotation, through: :items + accepts_nested_attributes_for :items, allow_destroy: true + + # visibility + PUBLIC = 'public' + PRIVATE = 'private' + + # Default values to be applied after initialization + def default_values + self.visibility ||= Playlist::PRIVATE + end + + # Returns all other playlist items on the same playlist that share a master file + # @param [PlaylistItem] current_item The playlist item you want to find matches for + # @return [Array ] an array of all other playlist items that reference the same master file + def related_items(current_item) + uri = AvalonAnnotation.where(id: current_item.annotation_id)[0].source + items = PlaylistItem.joins(:annotation).where('annotations.source_uri' => uri).where(playlist: self) + # remove the current item + return_items = [] + items.each do |item| + return_items << item unless item.annotation_id == current_item.annotation_id + end + return_items + end + + # Returns all other annotations on the same playlist that share a master file + # @param [PlaylistItem] current_item The playlist item you want to find matches for + # @return [Array ] an array of all other annotations items that reference the same master file + def related_annotations(current_item) + annotations = [] + related_items(current_item).each do |item| + annotations << AvalonAnnotation.find(item.annotation_id) + end + annotations + end + + # Returns a list of annotations who are on the same playlist, share the same masterfile, and whose start time falls within the start and end time of the current playlist item + # @param [PlaylistItem] current_item The playlist item to match against + # @return [Array ] all annotations matching the constraints + def related_annotations_time_contrained(current_item) + current_anno = AvalonAnnotation.where(id: current_item.annotation_id)[0] + annos = [] + related_items(current_item).each do |item| + anno = AvalonAnnotation.where(id: item.annotation_id)[0] + annos << anno if anno.start_time <= current_anno.end_time && anno.start_time >= current_anno.start_time + end + annos + end + + class << self + # Find the playlists that belong to this user/ability + def for_ability(ability) + accessible_by(ability, :update).order('playlists.created_at DESC') + end + + # Find the i18n default playlist name + def default_folder_name + I18n.translate(:'playlists.default_playlist_name') + end + end # class << self +end diff --git a/app/models/playlist_item.rb b/app/models/playlist_item.rb new file mode 100644 index 0000000000..f01cdadf30 --- /dev/null +++ b/app/models/playlist_item.rb @@ -0,0 +1,15 @@ +require 'acts_as_list' + +class PlaylistItem < ActiveRecord::Base +# after_save :recount_folders + belongs_to :playlist, touch: true + validates :playlist, presence: true + acts_as_list scope: :playlist + + belongs_to :annotation, class_name: AvalonAnnotation, dependent: :destroy + validates :annotation, presence: true + delegate :title, :comment, :start_time, :end_time, :title=, :comment=, :start_time=, :end_time=, :master_file, to: :annotation + before_save do + annotation.save + end +end diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index 6763f50d69..4b0de042c5 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -1,5 +1,5 @@ class SearchBuilder < Hydra::SearchBuilder - include Hydra::PolicyAwareAccessControlsEnforcement + include Hydra::MultiplePolicyAwareAccessControlsEnforcement def only_wanted_models(solr_parameters) solr_parameters[:fq] ||= [] @@ -14,11 +14,11 @@ def only_published_items(solr_parameters) end def limit_to_non_hidden_items(solr_parameters) - if current_ability.cannot? :create, MediaObject + if current_ability.cannot? :discover_everything, MediaObject solr_parameters[:fq] ||= [] - solr_parameters[:fq] << '!hidden_bsi:true' - end + solr_parameters[:fq] << [policy_clauses,"(*:* NOT hidden_bsi:true)"].compact.join(" OR ") end + end def add_access_controls_to_solr_params_if_not_admin(solr_parameters) if current_ability.cannot? :discover_everything, MediaObject diff --git a/app/models/stream_token.rb b/app/models/stream_token.rb index c1092123a6..1cc81847c0 100644 --- a/app/models/stream_token.rb +++ b/app/models/stream_token.rb @@ -48,7 +48,7 @@ def self.validate_token(value) token = self.find_by_token(value) if token.present? and token.expires > Time.now token.renew! - valid_streams = ActiveFedora::SolrService.query(%{is_derivation_of_ssim: "info:fedora/#{token.target}"}, fl: 'stream_path_ssi') + valid_streams = ActiveFedora::SolrService.query(%{is_derivation_of_ssim:"info:fedora/#{token.target}"}, fl: 'stream_path_ssi') return valid_streams.collect { |d| d['stream_path_ssi'] } else raise Unauthorized, "Unauthorized" diff --git a/app/models/user.rb b/app/models/user.rb index 3cf060bdf1..23f0ad6ff6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -57,6 +57,12 @@ def self.find_for_lti(auth_hash, signed_in_resource=nil) User.create(:username => auth_hash.uid, :email => auth_hash.info.email) end +# def self.find_for_api(username, email, signed_in_resource=nil) +# User.find_by_username(username) || +# User.find_by_email(email) || +# User.create(:username => username, :email => email) +# end + def self.autocomplete(query) self.where("username LIKE :q OR email LIKE :q", q: "%#{query}%").collect { |user| { id: user.user_key, display: user.user_key } @@ -90,4 +96,9 @@ def self.walk_ldap_groups(groups, seen) seen end + def destroy + Bookmark.where(user_id: self.id).destroy_all + super + end + end diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index 4ea6074637..225fc9f758 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -34,6 +34,11 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> <% end %> + <% if current_ability.can? :create, Playlist %> +
  • > + <%= link_to 'Playlists', playlists_path, id:'playlists_nav' %> +
  • + <% end %>
  • <%= link_to_if user_signed_in?, 'Sign out', destroy_user_session_path do %> <%# Fallback if the test above fails %> diff --git a/app/views/admin/collections/_form.html.erb b/app/views/admin/collections/_form.html.erb index d957412d91..0b1d3d37dd 100644 --- a/app/views/admin/collections/_form.html.erb +++ b/app/views/admin/collections/_form.html.erb @@ -13,12 +13,43 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> -<%= bootstrap_form_for @collection, remote: true, html: { modal: true } do |f| %> - <%= f.text_field :name, class: 'col-md-5' %> - <%= f.text_area :description, rows: 3, class: 'col-md-5' %> - <% if @collection.new_record? || can?(:update_unit, @collection)%> - <%= f.select(:unit, Admin::Collection.units) %> - <% end %> - <%= f.submit class: 'btn btn-primary btn-stateful-loading', data: {loading_text: 'Saving...'} %> + + + + +<% content_for :page_scripts do %> + <% end %> diff --git a/app/views/admin/collections/index.html.erb b/app/views/admin/collections/index.html.erb index 238d41e797..980f87566d 100644 --- a/app/views/admin/collections/index.html.erb +++ b/app/views/admin/collections/index.html.erb @@ -44,7 +44,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> - <%= link_to('Create Collection', new_admin_collection_path, remote: true, modal: true, class: 'btn btn-primary') unless cannot? :create, Admin::Collection %> + <%= button_tag("Create Collection", class: 'btn btn-primary btn-large', data: {toggle:"modal", target:"#new_collection"}) unless cannot? :create, Admin::Collection %> <% else %>

    You don't have any collections yet

    @@ -53,7 +53,7 @@ Unless required by applicable law or agreed to in writing, software distributed

    Would you like to create one?

    - <%= link_to('Create Collection', new_admin_collection_path, remote: true, modal: true, class: 'btn btn-primary btn-large') %> + <%= button_tag("Create Collection", class: 'btn btn-primary btn-large', data: {toggle:"modal", target:"#new_collection"}) %>

    <% else %>

    You'll need to be assigned to one

    @@ -64,8 +64,6 @@ Unless required by applicable law or agreed to in writing, software distributed
    -<% content_for :page_scripts do %> - -<% end %> +<% @collection = Admin::Collection.new %> +<%= render "form" %> + diff --git a/app/views/admin/collections/show.html.erb b/app/views/admin/collections/show.html.erb index e5ca284b90..50f8d1a2a3 100644 --- a/app/views/admin/collections/show.html.erb +++ b/app/views/admin/collections/show.html.erb @@ -22,7 +22,7 @@ Unless required by applicable law or agreed to in writing, software distributed

    Unit: <%= @collection.unit %>

    <%= link_to('Create An Item', new_media_object_path(collection_id: @collection.pid), class: 'btn btn-primary') if can? :create, MediaObject %> <%= link_to('List All Items', catalog_index_path('f[collection_ssim][]' => @collection.name), class: 'btn btn-default') %> - <%= link_to('Edit Collection Info', edit_admin_collection_path(@collection), class: 'btn btn-default', remote: true, modal: true) if can? :update, @collection %> + <%= button_tag('Edit Collection Info', class: 'btn btn-default', data: {toggle:"modal", target:"#new_collection"}) if can? :update, @collection %>

    Or learn how to create items using batch upload. @@ -73,9 +73,8 @@ Unless required by applicable law or agreed to in writing, software distributed [name='save_access'] { margin-left: 20px; } +<%= render "form" %> + <% content_for :page_scripts do %> <%= javascript_include_tag 'autocomplete' %> - <% end %> diff --git a/app/views/bookmarks/index.html.erb b/app/views/bookmarks/index.html.erb index 4a44c376bb..3c66a69154 100644 --- a/app/views/bookmarks/index.html.erb +++ b/app/views/bookmarks/index.html.erb @@ -32,3 +32,11 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render 'results_pagination' %> <% end %> + +<% content_for :page_styles do %> + <%= stylesheet_link_tag 'jquery-ui/datepicker', media: 'all' %> +<% end %> +<% content_for :page_scripts do %> + <%= javascript_include_tag 'access_control_step' %> + <%= javascript_include_tag 'autocomplete' %> +<% end %> diff --git a/app/views/bookmarks/update_access_control.html.erb b/app/views/bookmarks/update_access_control.html.erb index 905375eed5..c1fd54d817 100644 --- a/app/views/bookmarks/update_access_control.html.erb +++ b/app/views/bookmarks/update_access_control.html.erb @@ -64,14 +64,16 @@ Unless required by applicable law or agreed to in writing, software distributed Special Access <%= label_tag "user" do %> <%= render partial: "modules/tooltip", locals: { form: form, field: 'user', tooltip: t("access_control.user"), options: {display_label: t("access_control.userlabel").html_safe} } %> -
    +
    <%= hidden_field_tag "user" %> <%= text_field_tag "user_display", nil, class: "typeahead from-model form-control", - data: { model: 'user', target: "user" } %> -
    + data: { model: 'user', target: "user" } %>
    + <%= text_field_tag "add_user_begin", nil, placeholder: 'Begin Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %>
    + <%= text_field_tag "add_user_end", nil, placeholder: 'End Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %> +
    <%= button_tag "Add", name: 'submit_add_user', class:'btn btn-default', value: 'Add' %> - <%= button_tag "Remove", name: 'submit_remove_user', class:'btn btn-default', value: 'Remove' %> + <%= button_tag "Remove", name: 'submit_remove_user', class:'btn btn-default remove_access', value: 'Remove' %> <% end %> <%= label_tag "group" do %> <%= render partial: "modules/tooltip", locals: { form: form, field: 'group', tooltip: t("access_control.group"), options: {display_label: t("access_control.grouplabel").html_safe} } %> @@ -79,21 +81,37 @@ Unless required by applicable law or agreed to in writing, software distributed <% dropdown_values = options_from_collection_for_select(*dropdown_values) %> <%= select_tag "group", dropdown_values, - { include_blank: true, class: "form-control"}%> + { include_blank: true, class: "form-control"}%>
    + <%= text_field_tag "add_group_begin", nil, placeholder: 'Begin Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %>
    + <%= text_field_tag "add_group_end", nil, placeholder: 'End Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %> <%= button_tag "Add", name: 'submit_add_group', class:'btn btn-default', value: 'Add' %> - <%= button_tag "Remove", name: 'submit_remove_group', class:'btn btn-default', value: 'Remove' %> + <%= button_tag "Remove", name: 'submit_remove_group', class:'btn btn-default remove_access', value: 'Remove' %> <% end %> <%= label_tag "class" do %> <%= render partial: "modules/tooltip", locals: { form: form, field: 'class', tooltip: t("access_control.class"), options: {display_label: t("access_control.classlabel").html_safe} } %> -
    +
    <%= hidden_field_tag "class" %> <%= text_field_tag "class_display", nil, class: "typeahead from-model form-control", - data: { model: 'externalGroup', target: "class" } %> + data: { model: 'externalGroup', target: "class" } %>
    + <%= text_field_tag "add_class_begin", nil, placeholder: 'Begin Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %>
    + <%= text_field_tag "add_class_end", nil, placeholder: 'End Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %>
    <%= button_tag "Add", name: 'submit_add_class', class:'btn btn-default', value: 'Add' %> - <%= button_tag "Remove", name: 'submit_remove_class', class:'btn btn-default', value: 'Remove' %> + <%= button_tag "Remove", name: 'submit_remove_class', class:'btn btn-default remove_access', value: 'Remove' %> + <% end %> + <%= label_tag "ipaddress" do %> + <%= render partial: "modules/tooltip", locals: { form: form, field: 'ipaddress', tooltip: t("access_control.ipaddress"), options: {display_label: t("access_control.ipaddresslabel").html_safe} } %> +
    + <%= text_field_tag "ipaddress", nil, class: "form-control" %>
    + <%= text_field_tag "add_ipaddress_begin", nil, placeholder: 'Begin Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %>
    + <%= text_field_tag "add_ipaddress_end", nil, placeholder: 'End Date (yyyy-mm-dd)', class: 'form-control date-input access_date' %> +
    + <%= button_tag "Add", name: 'submit_add_ipaddress', class:'btn btn-default', value: 'Add' %> + <%= button_tag "Remove", name: 'submit_remove_ipaddress', class:'btn btn-default remove_access', value: 'Remove' %> <% end %> + + <% end %> -<%# content_for :page_scripts do %> - <%= javascript_include_tag 'autocomplete' %> -<%# end %> - <% end %> diff --git a/app/views/media_objects/_access_control.html.erb b/app/views/media_objects/_access_control.html.erb index 8a7747cce3..28474fd014 100644 --- a/app/views/media_objects/_access_control.html.erb +++ b/app/views/media_objects/_access_control.html.erb @@ -14,9 +14,17 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> - <%= render "modules/access_control", {object: @mediaobject, visibility: @mediaobject.visibility, hidden: @mediaobject.hidden?} %> - <%= render "workflow_buttons", form: 'access_control_form' %> + <%= render 'modules/access_control', {object: @mediaobject, visibility: @mediaobject.visibility, hidden: @mediaobject.hidden?} %> + <%= render 'workflow_buttons', form: 'access_control_form' %> <% content_for :page_scripts do %> + +<% end %> +<% content_for :page_styles do %> + <%= stylesheet_link_tag 'jquery-ui/datepicker', media: 'all' %> +<% end %> + +<% content_for :page_scripts do %> + <%= javascript_include_tag 'access_control_step' %> <%= javascript_include_tag 'autocomplete' %> <% end %> diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index 1f5e105c6e..8ac458ccc5 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -7,9 +7,9 @@ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the + CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> @@ -26,24 +26,28 @@ Unless required by applicable law or agreed to in writing, software distributed +
    +

    <%= t("file_upload_tip.datedigitized").html_safe %>

    +
    <%= t("file_upload_tip.thumbnail").html_safe %>

    - + - - - + + + - + + @@ -67,9 +71,12 @@ Unless required by applicable law or agreed to in writing, software distributed - + + @@ -120,14 +128,14 @@ Unless required by applicable law or agreed to in writing, software distributed
    - +
    Upload - + Select file Change @@ -137,18 +145,18 @@ Unless required by applicable law or agreed to in writing, software distributed <%= check_box_tag(:workflow, 'skip_transcoding', false)%> <%= label_tag(:skip_transcoding) do %>
    - Skip transcoding + Skip transcoding
    <% end %> -
    +
    - + <%= hidden_field_tag(:new_asset, true, :id => "files_new_asset") if params[:new_asset] %> <%= hidden_field_tag("id",params[:id], :id => "file_upload_id") if params[:id] %> <%= hidden_field_tag(:original, params[:original], :id => "files_original") %> <% end %> - +

    <%= t("file_upload_tip.skip_transcoding").html_safe %>

    @@ -174,6 +182,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% content_for :page_styles do %> <%= stylesheet_link_tag "browse_everything", media: "rel" %> <%= stylesheet_link_tag "jasny-bootstrap.min", media: "all" %> + <%= stylesheet_link_tag "jquery-ui/datepicker", media: "all" %> <% end %> <% content_for :page_scripts do %> diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index 1a925cf424..cfcbb0c081 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -7,9 +7,9 @@ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the + CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> @@ -23,8 +23,8 @@ Unless required by applicable law or agreed to in writing, software distributed
    + + <%= render file: '_add_to_playlist.html.erb' if current_user.present? %> <%= render 'workflow_progress' %> <%= render 'share' if will_partial_list_render? :share %> - <%= render partial: 'sections', + <%= render partial: 'sections', locals: {mediaobject: @mediaobject, sections: @masterFiles, activeStream: @currentStream} %> @@ -68,24 +73,43 @@ Unless required by applicable law or agreed to in writing, software distributed <% content_for :page_scripts do %> <%= javascript_include_tag "mediaelement_rails/mediaelement-and-player" =%> - <%= javascript_include_tag 'android_pre_play' %> - <%= javascript_include_tag 'mediaelement-qualityselector/mep-feature-qualities' %> + <%= javascript_include_tag 'android_pre_play' %> + <%= javascript_include_tag 'mediaelement-qualityselector/mep-feature-qualities' %> <%= javascript_include_tag 'me-thumb-selector' %> <%= javascript_include_tag 'mediaelement-skin-avalon/mep-feature-responsive' %> <%= javascript_include_tag 'avalon_player' %> + <%= javascript_include_tag 'me-add-to-playlist' %> - <%= stylesheet_link_tag "mediaelement-qualityselector/mep-feature-qualities" =%> + <%= stylesheet_link_tag "mediaelement-qualityselector/mep-feature-qualities" =%> <%= stylesheet_link_tag 'me-thumb-selector' %> <%= stylesheet_link_tag "mediaelement-skin-avalon/mejs-skin-avalon" =%> + <%= stylesheet_link_tag 'me-add-to-playlist' %> <% if @currentStream.present? and @currentStream.derivatives.present? %> <% end %> diff --git a/app/views/media_objects/_metadata_display.html.erb b/app/views/media_objects/_metadata_display.html.erb index 6798d1edc3..604904bc74 100644 --- a/app/views/media_objects/_metadata_display.html.erb +++ b/app/views/media_objects/_metadata_display.html.erb @@ -25,8 +25,8 @@ Unless required by applicable law or agreed to in writing, software distributed
    - <%= display_metadata('Main contributor', @mediaobject.creator, 'Not provided') %> <%= display_metadata('Date', combined_display_date(@mediaobject), 'Not provided') %> + <%= display_metadata('Main contributor', @mediaobject.creator) %> <% unless @mediaobject.abstract.blank? %>
    Summary
    diff --git a/app/views/media_objects/_resource_description.html.erb b/app/views/media_objects/_resource_description.html.erb index a67184fe96..3f95449793 100644 --- a/app/views/media_objects/_resource_description.html.erb +++ b/app/views/media_objects/_resource_description.html.erb @@ -29,14 +29,14 @@ Unless required by applicable law or agreed to in writing, software distributed locals: {form: form, field: :title, options: {required: true}} %> - <%= render partial: 'text_field', - locals: {form: form, field: :creator, - options: {display_label: 'Main contributor(s)', required: true, multivalued: true}} %> - <%= render partial: 'text_field', locals: {form: form, field: :date_issued, options: {display_label: 'Publication date', required: true}} %> + <%= render partial: 'text_field', + locals: {form: form, field: :creator, + options: {display_label: 'Main contributor(s)', multivalued: true}} %> + <%= render partial: 'text_field', locals: {form: form, field: :date_created, options: {display_label: 'Creation date'}} %> @@ -64,7 +64,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render partial: 'text_field', locals: {form: form, field: :physical_description, options: {display_label: "Physical Description", - multivalued: false}} %> + multivalued: true}} %> <%= render partial: 'text_field', locals: {form: form, field: :related_item_url, diff --git a/app/views/media_objects/_structure.html.erb b/app/views/media_objects/_structure.html.erb index 186e4553a8..1467d31010 100644 --- a/app/views/media_objects/_structure.html.erb +++ b/app/views/media_objects/_structure.html.erb @@ -7,9 +7,9 @@ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the + CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> @@ -24,7 +24,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% else %>
      -
    • Type
    • Section label
    • File name
    • Size
    • Structure
    • +
    • Type
    • Section label
    • File name
    • Size
    • Structure
    • Captions
      <% @masterFiles.each_with_index do |section, index| %> @@ -50,10 +50,16 @@ Unless required by applicable law or agreed to in writing, software distributed +
    • + + + +
    -
    Type
    Section label
    Permalink
    Date Digitized
    Thumbnail
    File name Size <%= text_field_tag "parts[#{part.pid}][label]", part.label, class: 'form-control' %> + <%= text_field_tag "parts[#{part.pid}][permalink]", part.permalink, class: 'form-control' %> - + <%= text_field_tag "parts[#{part.pid}][date_digitized]", part.date_digitized, class: 'form-control date-input' %> + <% if part.is_video? %> <%= text_field_tag "parts[#{part.pid}][poster_offset]", @@ -90,10 +97,11 @@ Unless required by applicable law or agreed to in writing, software distributed <%# On a Rails level this needs to be folded into the masterfiles # controller's destroy method to help remove more vestiges of the # catalog controller %> - <%= link_to '×'.html_safe, + <%= link_to '×'.html_safe, master_file_path(part.pid), title: 'Delete', class: 'btn btn-xs btn-danger btn-confirmation', + data: { placement: 'left' }, method: :delete %> <% end %>
    - <% if members.present? %> - - - + <% if members.present? %> + <% members.each do |member_object| %> + + + + <% if !input_disabled %> + <% end %> - - <% if !input_disabled %> - + + <% end %> + <% end %> + <% if is_leasable and defined?(leases) and leases.present? %> + <% leases.each do |lease_object| %> + + + + <% if !input_disabled %> + + <% end %> + <% end %> - <% end %> <% end %>
    - <%= label_tag do %> - <% if defined?(display_helper) && display_helper.present? %> - <%= self.send(display_helper, member_object) %> - <% else %> - <%= member_object %> + <% if members.present? || defined?(leases) && leases.present? %> +
    + <%= label_tag do %> + <% if defined?(display_helper) && display_helper.present? %> + <%= self.send(display_helper, member_object) %> + <% else %> + <%= member_object %> + <% end %> <% end %> + + <%= link_to "×", + polymorphic_path(object, "remove_#{access_object}".to_sym => member_object, step: @active_step, donot_advance: true), + method: "put", + class: "btn btn-xs close remove" %> + - <%= link_to "×", - polymorphic_path(object, - "remove_#{access_object}".to_sym => member_object, - step: @active_step, donot_advance: true), method: "put", - class: "btn btn-xs close remove" %> -
    + <%= label_tag do %> + <% if defined?(display_helper) && display_helper.present? %> + <%= self.send(display_helper, (lease_object.read_groups+lease_object.read_users).first) %> + <% else %> + <%= (lease_object.read_groups+lease_object.read_users).first %> + <% end %> + <% end %> + + Effective <%= lease_object.begin_time.strftime('%F') %> to <%= lease_object.end_time.strftime('%F') %> + + <%= link_to "×", + polymorphic_path(object, "remove_lease".to_sym => lease_object, step: @active_step, donot_advance: true), + method: "put", + class: "btn btn-xs close remove" %> +
    - -<% end %> + <% end %>
    diff --git a/app/views/playlists/_current_item.html.erb b/app/views/playlists/_current_item.html.erb new file mode 100644 index 0000000000..d368b698f1 --- /dev/null +++ b/app/views/playlists/_current_item.html.erb @@ -0,0 +1,55 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> +
    +
    +

    + <%= link_to media_object_path(@current_mediaobject) do %> + <% unless @current_mediaobject.title.blank? %> + <%= @current_mediaobject.title %> + <% else %> + <%= @current_mediaobject.pid %> + <% end %> + <%= "/ #{@current_mediaobject.statement_of_responsibility}" if @current_mediaobject.statement_of_responsibility.present? %> + <% end %> +

    +
    + +
    + <%= display_metadata('Date', combined_display_date(@current_mediaobject), 'Not provided') %> + <%= display_metadata('Main contributor', @current_mediaobject.creator) %> + <% unless @current_mediaobject.abstract.blank? %> +
    Summary
    +
    +
    <%= @current_mediaobject.abstract %>
    +
    + <% end %> + <%= display_metadata('Contributor', @current_mediaobject.contributor) %> + <%= display_metadata('Publisher', @current_mediaobject.publisher) %> + <%= display_metadata('Genre', @current_mediaobject.genre) %> + <%= display_metadata('Subject', @current_mediaobject.subject) %> + <%= display_metadata('Time period', @current_mediaobject.temporal_subject) %> + <%= display_metadata('Location', @current_mediaobject.geographic_subject) %> + <%= display_metadata('Collection', @current_mediaobject.collection.name) %> + <%= display_metadata('Unit', @current_mediaobject.collection.unit) %> + <%= display_metadata('Language', display_language(@current_mediaobject)) %> + <%= display_metadata('Terms of Use', @current_mediaobject.terms_of_use) %> + <%= display_metadata('Physical Description', @current_mediaobject.physical_description) %> + <%= display_metadata('Related Item', display_related_item(@current_mediaobject)) %> + <%= display_metadata('Notes', display_notes(@current_mediaobject)) %> + <%= display_metadata('Other Identifier', display_other_identifiers(@current_mediaobject)) %> + +
    +
    diff --git a/app/views/playlists/_current_masterfile.html.erb b/app/views/playlists/_current_masterfile.html.erb new file mode 100644 index 0000000000..f8de5e9973 --- /dev/null +++ b/app/views/playlists/_current_masterfile.html.erb @@ -0,0 +1,82 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + + +
    + +<% content_for :page_scripts do %> + +<% end %> diff --git a/app/views/playlists/_edit_form.html.erb b/app/views/playlists/_edit_form.html.erb new file mode 100644 index 0000000000..a94007a31e --- /dev/null +++ b/app/views/playlists/_edit_form.html.erb @@ -0,0 +1,255 @@ +
    +
    +
    + <%= link_to "View Playlist", @playlist, { class: 'btn btn-xs btn-primary'} %> + <% if can?(:destroy, @playlist) %> + <%= link_to "Delete Playlist", @playlist, method: :delete, class: 'btn btn-xs btn-danger btn-confirmation', data: {placement: 'bottom'} %> + <% end %> +
    +
    +
    +
    +

    Playlist Details

    + +
    +
    +
    +
    +
    Name
    +
    <%= @playlist.title %>
    +
    +
    +
    Description
    +
    <%= @playlist.comment %>
    +
    +
    +
    <%= t("blacklight/folders/folder.visibility", scope: "helpers.label") %>
    +
    + <% if @playlist.visibility==Playlist::PUBLIC %> + <%= human_friendly_visibility Playlist::PUBLIC %> + <% else %> + <%= human_friendly_visibility Playlist::PRIVATE %> + <% end %> +
    +
    +
    + + <%= render 'form' %> + +
    + <% if @playlist.items.empty? %> +

    There are currently no items in this playlist.

    + <% else %> + <%= form_for(@playlist,url: { action: "update_multiple" }, html: { class: 'form-horizontal playlist_actions' }) do |f| %> +
    +
    +

    Playlist Items

    +
    +
    +
    + <% if @playlists.present? %> +
    + <%= hidden_field_tag "new_playlist_id", @playlist.id, form:"edit_playlist_#{@playlist.id}" %> + <%= button_tag( { type: 'button', class: "btn btn-default btn-xs dropdown-toggle", data: { toggle: "dropdown"}, disabled:'disabled' } ) do %> + Move to... + <% end # button_tag %> + +
    + <% end # playlists.prsent?%> + <%= f.submit "Delete Selected", class: 'btn btn-danger btn-confirmation btn-xs', form:"edit_playlist_#{@playlist.id}", data: { placement: 'bottom' }, disabled:'disabled' %> +
    +
    +
    +
    + <%= check_box_tag 'select_all', "Select All", false, form:"edit_playlist_#{@playlist.id}" %> + Select All +
    +
    +
    + <% end #form_for update_multiple %> + + <%= form_for(@playlist, html: { id: 'playlist_sort_form', class: 'form-horizontal playlist_actions' }) do |fs| %> +
    +
      + <% @playlist.items.each_with_index do |i, index| %> +
    1. +
      +
      + + <%= text_field_tag "playlist[items_attributes[#{index}[position]]]", i.position, class: 'form-control position-input', form: 'playlist_sort_form' %> + <%= hidden_field_tag "playlist[items_attributes[#{index}[id]]]", i.id, form: 'playlist_sort_form' %> +
      + <% if can? :read, i.annotation.master_file %> +
      + <%= link_to i.annotation.title, i.annotation.mediafragment_uri, id: "playlist_item_title_label_#{i.id}" %> +
      +
      + +
      +
      + +
      + <% else %> +
      + + [inaccessible item] <%= i.annotation.master_file.mediaobject.pid %> +
      +
      + +
      + <% end %> +
      +
      +
      + <%= bootstrap_form_for i, remote: true, html: { id: "playlist_item_form_#{i.id}", class: "playlist_item_edit_form" }, format: 'json' do |pif| %> + <%= hidden_field_tag "playlist_id", @playlist.id, form: "playlist_item_form_#{i.id}" %> +
      + <%= pif.label :title, class: 'col-sm-2 control-label' %> +
      + <%= text_field_tag :title, i.title, class: 'form-control', id: "avalon_annotation_title_#{i.id}", form:"playlist_item_form_#{i.id}" %> + +
      +
      +
      +
      <%= pif.label :start_time, class: 'control-label' %>
      +
      + <%= text_field_tag :start_time, pretty_time(i.start_time), class: 'form-control', id: "avalon_annotation_start_time_#{i.id}", form:"playlist_item_form_#{i.id}" %> + +
      +
      + <%= pif.label :end_time, class: 'control-label' %> +
      +
      + <%= text_field_tag :end_time, pretty_time(i.end_time), class: 'form-control', id: "avalon_annotation_end_time_#{i.id}", form:"playlist_item_form_#{i.id}" %> + +
      +
      +
      + <%= pif.label :comment, class: 'col-sm-2 control-label' %> +
      + <%= text_area_tag :comment, i.comment, class: 'form-control', id: "avalon_annotation_comment_#{i.id}", form:"playlist_item_form_#{i.id}" %> + +
      +
      +
      +
      + + Cancel +
      +
      +
      + <% end #bootstrap_form_for playlist_item_edit %> +
      +
      +
    2. + <% end #playlist.items.each %> +
    +
    + <%= fs.submit class: 'btn btn-primary btn-xs', value: 'Save Changes', form: 'playlist_sort_form', style: 'visibility:hidden' %> + <% end #form_for playlist_sort_form %> + <% end #playlist empty else%> +
    + +<% content_for :page_scripts do %> + + +<% end %> diff --git a/app/views/playlists/_form.html.erb b/app/views/playlists/_form.html.erb new file mode 100644 index 0000000000..8535f34f26 --- /dev/null +++ b/app/views/playlists/_form.html.erb @@ -0,0 +1,35 @@ +
    + <%= form_for(@playlist, html: { id: 'playlist_form', class: 'form-horizontal playlist_actions' }) do |f| %> +
    + <%= f.label "Name", class: 'col-sm-2 control-label' %> +
    <%= f.text_field :title, class: 'form-control' %>
    +
    +
    + <%= f.label :comment, 'Description', class: 'col-sm-2 control-label' %> +
    <%= f.text_area :comment, class: 'form-control', rows: '4' %>
    +
    +
    + <%= label_tag nil, t("blacklight/folders/folder.visibility", scope: "helpers.label"), class: 'col-sm-2 control-label' %> +
    + + +
    +
    +
    +
    +
    + <%= f.submit class: 'btn btn-primary btn-xs', value: t("playlist.#{params[:action]}.action") %> + <% if params[:action] == "edit" || params[:action] == "update" %> + Cancel + <% end %> +
    +
    +
    + <% end # form_for playlist_form%> +
    diff --git a/app/views/playlists/_item_list.html.erb b/app/views/playlists/_item_list.html.erb new file mode 100644 index 0000000000..d9e0a99b7a --- /dev/null +++ b/app/views/playlists/_item_list.html.erb @@ -0,0 +1,40 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> +<% if current_ability.can? :edit, @playlist %> +
    + <%= link_to edit_playlist_path(@playlist) do %> + + <% end %> +
    +<% end %> +

    + <% if @playlist.visibility==Playlist::PRIVATE %> + + <% else %> + + <% end %> + <%= @playlist.title %> +

    +<% if @playlist.comment.present? %> +
    + <%= @playlist.comment %> +
    +<% end %> +
      + <%= render partial: 'playlist_item', collection: @playlist.items, locals: { annotations: @playlist.annotations } %> +
    diff --git a/app/views/playlists/_player.html.erb b/app/views/playlists/_player.html.erb new file mode 100644 index 0000000000..40b1cfd383 --- /dev/null +++ b/app/views/playlists/_player.html.erb @@ -0,0 +1,111 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + +<% f_start = @current_annotation.start_time / 1000.0 %> +<% f_end = @current_annotation.end_time / 1000.0 %> +<% @currentStream = @current_masterfile %> +<% @token = StreamToken.find_or_create_session_token(session, @currentStream.pid) %> +<% @currentStreamInfo = @currentStream.stream_details(@token, ApplicationController.default_url_options[:host]) %> +<% @currentStreamInfo['t'] = "#{f_start},#{f_end}" %> +<% if can? :read, @current_masterfile %> + +<% end %> +<% unless can? :read, @current_masterfile %> +

    Restricted Access

    + You do not have permission to playback item <%= @current_masterfile.mediaobject_id %> +<% end %> +<% content_for :page_scripts do %> + <%= javascript_include_tag "mediaelement_rails/mediaelement-and-player" =%> + <%= javascript_include_tag 'android_pre_play' %> + <%= javascript_include_tag 'mediaelement-qualityselector/mep-feature-qualities' %> + <%= javascript_include_tag 'me-thumb-selector' %> + <%= javascript_include_tag 'mediaelement-skin-avalon/mep-feature-responsive' %> + <%= javascript_include_tag 'avalon_player' %> + + <%= stylesheet_link_tag "mediaelement-qualityselector/mep-feature-qualities" =%> + <%= stylesheet_link_tag 'me-thumb-selector' %> + <%= stylesheet_link_tag "mediaelement-skin-avalon/mejs-skin-avalon" =%> + + <% if @currentStream.present? and @currentStream.derivatives.present? %> + + <% end %> +<% end %> diff --git a/app/views/playlists/_playlist_item.html.erb b/app/views/playlists/_playlist_item.html.erb new file mode 100644 index 0000000000..e79c3e04d7 --- /dev/null +++ b/app/views/playlists/_playlist_item.html.erb @@ -0,0 +1,26 @@ +<% annotation = AvalonAnnotation.find(@playlist.items[playlist_item_counter].annotation_id) %> +<% item = @playlist.items[playlist_item_counter] %> +<% item_class = (item == @current_playlist_item)? 'now_playing' : 'queue' %> +<% item_type = (annotation.master_file.file_format=='Moving image')? 'fa-film' : 'fa-music' %> +<% if can? :read, item %> +
  • + <% if item_class == 'now_playing' %> + + <% end %> + <%= link_to playlist_path(@playlist, position: item.position) do %> + <%= annotation.title %> + <% end %> + <%= annotation.duration %> +
  • +<% else %> + <% denied_class = item_class %> + <% denied_class = 'denied_item' unless denied_class == 'now_playing'%> +
  • + <% if item_class == 'now_playing' %> + + <% end %> + + [Inaccessible Item] <%=annotation.master_file.mediaobject_id%> + <%= annotation.duration %> +
  • +<% end %> diff --git a/app/views/playlists/_related_items.html.erb b/app/views/playlists/_related_items.html.erb new file mode 100644 index 0000000000..d1b9d1f537 --- /dev/null +++ b/app/views/playlists/_related_items.html.erb @@ -0,0 +1,34 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + diff --git a/app/views/playlists/edit.html.erb b/app/views/playlists/edit.html.erb new file mode 100644 index 0000000000..9b65b16697 --- /dev/null +++ b/app/views/playlists/edit.html.erb @@ -0,0 +1,9 @@ +
    +

    Editing playlist

    + +<%= render 'edit_form' %> +
    + +<% content_for :page_scripts do %> + <%= javascript_include_tag "avalon_playlists/playlist_items" =%> +<% end %> diff --git a/app/views/playlists/index.html.erb b/app/views/playlists/index.html.erb new file mode 100644 index 0000000000..0efa3c2fcf --- /dev/null +++ b/app/views/playlists/index.html.erb @@ -0,0 +1,93 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + +<% if current_user.nil? %> +<%= link_to 'Please login to view your playlists.', new_user_session_path %> +<% end %> +<% unless current_user.nil? %> + <% playlists = Playlist.where(user_id: current_user.id) %> +
    +
    +
    +
    +
    +
    +

    Playlists (<%= playlists.size %> total)

    + <% unless playlists.empty? %> + + + + + + + + + + + + + <% playlists.each do |playlist| %> + + + + + + + + + <%end%> + +
    NameSizeVisibilityCreatedUpdatedActions
    + <%= link_to(playlist.title, playlist_path(playlist), title: playlist.comment)%> + + <%= PlaylistItem.where(playlist_id: playlist.id).size %> items + + <% if playlist.visibility =='private' %> + Only me + <% end %> + <% if playlist.visibility !='private' %> + Public + <% end %> + + ><%= time_ago_in_words(playlist.created_at)%> + + ><%= time_ago_in_words(playlist.updated_at)%> + + <%= link_to(edit_playlist_path(playlist), class: 'btn btn-default btn-xs') do %> + Edit + <% end %> + <%= link_to(playlist_path(playlist), method: :delete, class: 'btn btn-xs btn-danger btn-confirmation', data: {placement: 'bottom'}) do %> + Delete + <% end %> +
    +
    + <% end %> +
    + <%= link_to(new_playlist_path) do %> + + Create New Playlist + + <% end %> +
    +
    +
    +
    +
    +
    +
    + <% unless playlists.empty? %> + <% end %> +<% end %> diff --git a/app/views/playlists/new.html.erb b/app/views/playlists/new.html.erb new file mode 100644 index 0000000000..2ddfcc1994 --- /dev/null +++ b/app/views/playlists/new.html.erb @@ -0,0 +1,5 @@ +

    New playlist

    + +
    + <%= render 'form' %> +
    diff --git a/app/views/playlists/show.html.erb b/app/views/playlists/show.html.erb new file mode 100644 index 0000000000..970d8ca5e5 --- /dev/null +++ b/app/views/playlists/show.html.erb @@ -0,0 +1,82 @@ +<%# +Copyright 2011-2015, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> +<% @page_title = t('media_objects.show.title', :media_object_title => @playlist.title, :application_name => application_name) %> +<% position = params.has_key?(:position)? params[:position].to_i : 1 %> +
    +
    +
    +<% if @playlist.items.empty? %> +
    + <%= render 'item_list' %> + This playlist currently has no items. +
    +<% else %> + <% @current_playlist_item = @playlist.items.where(position: position).first %> + <% @current_annotation = AvalonAnnotation.find(@current_playlist_item.annotation_id) %> + <% @current_masterfile = MasterFile.find(@current_playlist_item.annotation.source.split('/').last) %> + <% @current_mediaobject = MediaObject.find(@current_masterfile.mediaobject_id) %> +
    + <%= render 'player' %> + <% if can? :read, @current_masterfile %> +
    +
    + + + + <% @related_annotations = @playlist.related_annotations(@current_playlist_item)%> + <% unless @related_annotations.empty? %> + +
    +
    + <%= render 'related_items' %> +
    +
    + <% end %> +
    +
    + <% end %> +
    +
    + <%= render 'item_list' %> +
    +<% end %> + +
    +
    +
    diff --git a/config/application.rb b/config/application.rb index e584db735b..7501138fe8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,7 +11,7 @@ end module Avalon - VERSION = '4.0.1' + VERSION = '5.0' class MissingUserId < Exception; end class Application < Rails::Application diff --git a/config/authentication.yml.example b/config/authentication.yml.example index 969673b09a..0ffcb13ca8 100644 --- a/config/authentication.yml.example +++ b/config/authentication.yml.example @@ -5,6 +5,13 @@ :fields: - :email +#- :name: Avalon JSON Api +# :id: :api +# :tokens: +# token: +# :username: system_account_name +# :email: system@example.edu +# #- :name: Avalon Lti OAuth # :provider: :lti # :hidden: true diff --git a/config/initializers/active_fedora.rb b/config/initializers/active_fedora.rb index d83af3b3fb..4367528845 100644 --- a/config/initializers/active_fedora.rb +++ b/config/initializers/active_fedora.rb @@ -1,3 +1,22 @@ ActiveFedora::Base.class_eval do has_metadata name: 'DC', type: DublinCoreDocument end + +#Added for Kaminari paging +ActiveFedora::QueryMethods.module_eval do + def extending(*modules, &block) + if modules.any? || block + spawn.extending!(*modules, &block) + else + self + end + end +end + +#Cherry-picked 9cf316b71e78e65d95bfe1fa8ce93373da1a7305 +ActiveFedora::Associations::CollectionProxy.class_eval do + def scope + @association.scope + end + alias spawn scope +end diff --git a/config/initializers/authn_providers.rb b/config/initializers/authn_providers.rb index bc412d66e9..81d97c0231 100644 --- a/config/initializers/authn_providers.rb +++ b/config/initializers/authn_providers.rb @@ -1,7 +1,8 @@ module Avalon - module Authentication - Providers = YAML.load(File.read(File.expand_path('../../authentication.yml',__FILE__))) - VisibleProviders = Providers.reject {|provider| provider[:hidden]} + module Authentication + Config = YAML.load(File.read(File.expand_path('../../authentication.yml',__FILE__))) + Providers = Config.reject {|provider| provider[:provider].blank? } + VisibleProviders = Providers.reject {|provider| provider[:hidden]} HiddenProviders = Providers - VisibleProviders end end diff --git a/config/initializers/hydra_config.rb b/config/initializers/hydra_config.rb index ccf79ee96b..2786a4a151 100644 --- a/config/initializers/hydra_config.rb +++ b/config/initializers/hydra_config.rb @@ -1,4 +1,6 @@ require 'hydra/head' unless defined? Hydra +require 'hydra/multiple_policy_aware_access_controls_enforcement' +require 'hydra/multiple_policy_aware_ability' Hydra.configure do |config| @@ -32,6 +34,6 @@ config.permissions.inheritable.read.individual = ActiveFedora::SolrService.solr_name("inheritable_read_access_person", :symbol) config.permissions.inheritable.edit.group = ActiveFedora::SolrService.solr_name("inheritable_edit_access_group", :symbol) config.permissions.inheritable.edit.individual = ActiveFedora::SolrService.solr_name("inheritable_edit_access_person", :symbol) - config.permissions.policy_class = Admin::Collection - + #config.permissions.policy_class = Admin::Collection + config.permissions.policy_class = {Admin::Collection => {}, Lease => {clause: " AND begin_time_dti:[* TO NOW] AND end_time_dti:[NOW TO *]"}} end diff --git a/config/initializers/kaminari_active_fedora_extension.rb b/config/initializers/kaminari_active_fedora_extension.rb new file mode 100644 index 0000000000..8a6676b1ed --- /dev/null +++ b/config/initializers/kaminari_active_fedora_extension.rb @@ -0,0 +1,118 @@ +module Kaminari +# module ActiveFedoraExtension +# extend ActiveSupport::Concern +# +# module ClassMethods +# # Future subclasses will pick up the model extension +# def inherited(kls) #:nodoc: +# super +# kls.send(:include, Kaminari::ActiveFedoraModelExtension) if kls.superclass == ::ActiveFedora::Base +# end +# end +# +# included do +# # Existing subclasses pick up the model extension as well +# self.descendants.each do |kls| +# kls.send(:include, Kaminari::ActiveFedoraModelExtension) if kls.superclass == ::ActiveFedora::Base +# end +# end +# end +# + module ActiveFedoraModelExtension + extend ActiveSupport::Concern + + included do + self.send(:include, Kaminari::ConfigurationMethods) + + # Fetch the values at the specified page number + # Model.page(5) + eval <<-RUBY + def self.#{Kaminari.config.page_method_name}(num = nil) + limit(default_per_page).offset(default_per_page * ((num = num.to_i - 1) < 0 ? 0 : num)).extending do + include Kaminari::ActiveFedoraRelationMethods + include Kaminari::PageScopeMethods + end + end + RUBY + end + end + + module ActiveFedoraRelationMethods + def entry_name + model_name.human.downcase + end + + def reset #:nodoc: + @total_count = nil + super + end + + def total_count(column_name = :all, options = {}) #:nodoc: + # #count overrides the #select which could include generated columns referenced in #order, so skip #order here, where it's irrelevant to the result anyway + @total_count ||= begin +# c = except(:offset, :limit, :order) + + # Remove includes only if they are irrelevant + # c = c.except(:includes) unless references_eager_loaded_tables? + + # Rails 4.1 removes the `options` argument from AR::Relation#count + # args = [column_name] + args = [] + args << options #if ActiveRecord::VERSION::STRING < '4.1.0' + + # .group returns an OrderdHash that responds to #count + c = count(*args) + if c.is_a?(Hash) || c.is_a?(ActiveSupport::OrderedHash) + c.count + else + c.respond_to?(:count) ? c.count(*args) : c + end + end + end + end + + module PageScopeMethods + # Specify the per_page value for the preceding page scope + # Model.page(3).per(10) + def per(num) + if (n = num.to_i) < 0 || !(/^\d/ =~ num.to_s) + self + elsif n.zero? + limit(n) + elsif Kaminari.config.max_per_page && Kaminari.config.max_per_page < n + limit(Kaminari.config.max_per_page).offset(offset_value / limit_value * Kaminari.config.max_per_page) + else + limit(n).offset(offset_value / limit_value * n) + end + end + + # Total number of pages + def total_pages + count_without_padding = total_count + count_without_padding -= @_padding if defined?(@_padding) && @_padding + count_without_padding = 0 if count_without_padding < 0 + + total_pages_count = (count_without_padding.to_f / limit_value).ceil + if Kaminari.config.max_pages.present? && Kaminari.config.max_pages < total_pages_count + Kaminari.config.max_pages + else + total_pages_count + end + rescue FloatDomainError + raise ZeroPerPageOperation, "The number of total pages was incalculable. Perhaps you called .per(0)?" + end + end +end + +ActiveFedora::Relation.class_eval do + include Kaminari::ConfigurationMethods + + def page(num = nil) + limit(Kaminari.config.default_per_page).offset(Kaminari.config.default_per_page * ((num = num.to_i - 1) < 0 ? 0 : num)).extending do + include Kaminari::ActiveFedoraRelationMethods + include Kaminari::PageScopeMethods + end + end + + delegate :to_json, to: :to_a +end diff --git a/config/initializers/uri_parser.rb b/config/initializers/uri_parser.rb new file mode 100644 index 0000000000..c775522fbe --- /dev/null +++ b/config/initializers/uri_parser.rb @@ -0,0 +1,12 @@ +require 'addressable/uri' + +class URI::Parser + def split url + begin + a = Addressable::URI::parse url + [a.scheme, a.userinfo, a.host, a.port, nil, a.path, nil, a.query, a.fragment] + rescue Addressable::URI::InvalidURIError => err + raise URI::InvalidURIError, err.message + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 773c365f3d..03dbe12cc0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -10,8 +10,8 @@ en: release_label: "Release" errors: lti_auth_error: | - If you are an instructor, you cannot view these items in student mode. - Please switch back to instructor mode to view. If you are a student and + If you are an instructor, you cannot view these items in student mode. + Please switch back to instructor mode to view. If you are a student and receiving this message, please contact us at %s. messages: blank: "field is required." @@ -23,16 +23,16 @@ en: empty_share_section_permalink_notice: "After processing has started the section link will be available." metadata_tip: abstract: | - Summary provides a space for describing the contents of the item. Examples - include liner notes, contents list, or an opera scene abstract. This field - is not meant for cataloger's descriptions but for descriptions that - accompany the item. Depending on the length of the summary it may be + Summary provides a space for describing the contents of the item. Examples + include liner notes, contents list, or an opera scene abstract. This field + is not meant for cataloger's descriptions but for descriptions that + accompany the item. Depending on the length of the summary it may be truncated in search results. collection: | - Collection defines the source or host of the item (for example, the source - of the original record or the collection responsible for entering - information about the item). Collections will be aggregated for browsing - access. Examples: Variations, Indiana University Libraries Film Archive, + Collection defines the source or host of the item (for example, the source + of the original record or the collection responsible for entering + information about the item). Collections will be aggregated for browsing + access. Examples: Variations, Indiana University Libraries Film Archive, IUCAT, NUcat. contributor: | Contributors are persons or bodies associated with the item but not considered @@ -42,7 +42,7 @@ en: possible, use the Library of Congress Name Authority File. creator: | - Required field. Main contributors are the primary persons + Main contributors are the primary persons or bodies associated with the creation of the content. Main contributors will be included in search results display and aggregated for browsing access. At this time there is no ability to specify a main contributor as @@ -100,13 +100,13 @@ en: letters within the field will bring up permitted matches. Only terms or codes from the MARC Code List for Languages may be used. This is an - optional field. + optional field. terms_of_use: | - Terms of Use describes the conditions under which content may be used. This is an optional field. + Terms of Use describes the conditions under which content may be used. This is an optional field. physical_description: | - Physical Description is an optional field that will display information about the original resource being described. + Physical Description is an optional field that will display information about the original resource being described. related_item_url: | - The label is the text that will be displayed in the item record and will link to the URL entered. Related Item is an optional field. + The label is the text that will be displayed in the item record and will link to the URL entered. Related Item is an optional field. note: | Note is used to describe aspects of the resource not accounted for in any of the other fields, such as creation or production credits, performers, venue, historical or biographical information, language details, awards given to the performance or the work performed. Recommended use is to provide a separate Contributor field for each person or body associated with the creation of the content and to use a Note to provide more information about such contributions or to provide information about secondary persons or bodies associated with the creation of the content. Type specifies the type of note and is used as a label in the user interface.

    Statement of Responsibility is used to provide information about primary persons or bodies associated with the creation of the content, along with details about their roles. This information can be transcribed from the credits listed in the resource itself or on its packaging. Recommended use is to provide a separate Contributor field for each person or body listed in the Statement of Responsibility. Statement of Responsibility may be left empty if the use of Contributor fields alone is preferred. Statement of Responsibility is displayed in the user interface appended to the Title field, following a ' / '. other_identifier: | @@ -119,6 +119,8 @@ en: The Section Label will be used in an item's listing to identify each file associated with the item. permalink: | The section Permalink is a URL that links to an individual section of an Item. In addition, the entire item will have a distinct permalink that can be assigned in the Resource Description page. If configured to do so, Avalon will automatically generate a permalink if this field is left blank. + datedigitized: | + The Date Digitized field reflects when the original media was initially digitized, not ingested into Avalon. This date automatically will be set to the date the ingest process completes unless a date is provided. thumbnail: | The Thumbnail is a still image that will be displayed in search results. Thumbnails can be grabbed from a video during playback by clicking the 'Create thumbnail' button in the player or by entering a time offset value here in the format 'hh:mm:ss.sss'. skip_transcoding: | @@ -130,29 +132,35 @@ en: userlabel: "Avalon User" grouplabel: "Avalon Group" classlabel: "External Group" + ipaddresslabel: "IP Address or Range" depositor: | - Depositors add media to the collection and describe it with metadata. - They can publish items but not unpublish. They can only modify or + Depositors add media to the collection and describe it with metadata. + They can publish items but not unpublish. They can only modify or delete unpublished items. editor: | - Editors have supervisory responsibility for the collection building — the - ingest and description process. They can assign depositor roles, change - the name or description of the collection, and can modify the access - controls for individual items in the collection. Editors can also do + Editors have supervisory responsibility for the collection building — the + ingest and description process. They can assign depositor roles, change + the name or description of the collection, and can modify the access + controls for individual items in the collection. Editors can also do anything a depositor can do. manager: | - Managers have overall accountability for the collection. Managers can - create collections and assign editor and depositor roles for those - collections. They set the default access controls for items added to - the collection, and they also step in when a published item needs - revising or deleting. Managers can also do anything an editor or + Managers have overall accountability for the collection. Managers can + create collections and assign editor and depositor roles for those + collections. They set the default access controls for items added to + the collection, and they also step in when a published item needs + revising or deleting. Managers can also do anything an editor or depositor can do. user: | - Enter a username to grant them access to an item or collection. + Enter a username to grant them access to an item or collection. Start and end dates are not required; if included, access for the specified user will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified user. group: | - Select an Avalon group from the dropdown list below to grant it access to an item or collection. + Select an Avalon group from the dropdown list below to grant it access to an item or collection. Start and end dates are not required; if included, access for the specified group will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified group. class: | - Enter an external group to grant it access to an item or collection. This group might represent students in a course or members of an institution-specific group. + Enter an external group to grant it access to an item or collection. This group might represent students in a course or members of an institution-specific group. Start and end dates are not required; if included, access for the specified group will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified group. + ipaddress: | + An IP Address or subnet. Eg. 255.0.1.10 or 255.0.1.10/21 or 255.0.1.10/255.255.255.0 + or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777 + or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00 + Start and end dates are not required; if included, access for the specified IP Address(es) will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified IP Address(es). contact: title: 'Contact Us - %{application_name}' @@ -172,3 +180,26 @@ en: title: '%{step_title} - %{media_object_title} - %{application_name}' show: title: '%{media_object_title} - %{application_name}' + player: + customError: 'Your browser requires Adobe Flash Player in order to play this item. For more information, see Adobe Flash Player Help.' + + playlist: + ago: "%{time} ago" + lockAltText: "This playlist is private." + unlockAltText: "This playlist is public." + lockText: "Private" + unlockText: "Public" + new: + action: "Create" + create: + action: "Create" + edit: + action: "Save Changes" + update: + action: "Save Changes" + + activerecord: + attributes: + playlist: + title: "Name" + comment: "Description" diff --git a/config/routes.rb b/config/routes.rb index 38e8e6ccf0..f7604164d8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Avalon::Application.routes.draw do + mount BrowseEverything::Engine => '/browse' # HydraHead.add_routes(self) @@ -21,7 +22,7 @@ root :to => "catalog#index" devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }, format: false - devise_scope :user do + devise_scope :user do match '/users/sign_in', :to => "users/sessions#new", :as => :new_user_session, via: [:get] match '/users/sign_out', :to => "users/sessions#destroy", :as => :destroy_user_session, via: [:get] end @@ -31,8 +32,14 @@ match "/oembed", to: 'master_files#oembed', via: [:get] match "object/:id", to: 'object#show', via: [:get], :as => :object - resources :media_objects, except: [:create] do + + resources :vocabulary, except: [:create, :destroy, :new, :edit] + + resources :media_objects, except: [:create, :update] do member do + put :update, action: :update, defaults: { format: 'html' }, constraints: { format: 'html' } + put :update, action: :json_update, constraints: { format: 'json' } + patch :update, action: :update, defaults: { format: 'html' }, constraints: { format: 'html' } put :update_status get :progress, :action => :show_progress get 'content/:datastream', :action => :deliver_content, :as => :inspect @@ -42,12 +49,11 @@ get :confirm_remove end collection do + post :create, action: :create, constraints: { format: 'json' } get :confirm_remove put :update_status # 'delete' has special signifigance so use 'remove' for now delete :remove, :action => :destroy - get :confirm_reassign_collection - put :reassign_collection end end resources :master_files, except: [:new, :index, :update] do @@ -60,21 +66,34 @@ post 'still', :to => 'master_files#set_frame', :defaults => { :format => 'html' } get :embed post 'attach_structure' + post 'attach_captions' + get :captions end end match '/media_objects/:media_object_id/section/:id/embed' => 'master_files#embed', via: [:get] resources :derivatives, only: [:create] - + resources :playlists do + resources :playlist_items, path: 'items', only: [:create, :update] + member do + patch 'update_multiple' + delete 'update_multiple' + end + end + + resources :avalon_annotation, only: [:create, :show, :update, :destroy] + resources :comments, only: [:index, :create] + resources :playlist_items, only: [:update], :constraints => {:format => /(js|json)/} + #match 'search/index' => 'search#index' #match 'search/facet/:id' => 'search#facet' namespace :admin do - resources :groups, except: [:show] do - collection do + resources :groups, except: [:show] do + collection do put 'update_multiple' end member do @@ -85,6 +104,7 @@ member do get 'edit' get 'remove' + get 'items' end end end @@ -96,7 +116,7 @@ end mount AboutPage::Engine => '/about(.:format)', :as => 'about_page' - + # The priority is based upon order of creation: # first created -> highest priority. diff --git a/db/migrate/20151201164326_add_date_digitized_to_master_file.rb b/db/migrate/20151201164326_add_date_digitized_to_master_file.rb new file mode 100644 index 0000000000..b15fd7c76a --- /dev/null +++ b/db/migrate/20151201164326_add_date_digitized_to_master_file.rb @@ -0,0 +1,19 @@ +class AddDateDigitizedToMasterFile < ActiveRecord::Migration + + def up + MasterFile.find_each({},{batch_size:5}) do |mf| + encode = ActiveEncode::Base.find(mf.workflow_id) + next unless encode.present? + mf.date_digitized = encode.finished_at + mf.save(validate: false) + end + end + + def down + MasterFile.find_each({},{batch_size:5}) do |mf| + mf.date_digitized = nil + mf.save(validate: false) + end + end + +end diff --git a/db/migrate/20160105181819_derivative_add_managed.rb b/db/migrate/20160105181819_derivative_add_managed.rb new file mode 100644 index 0000000000..a95697f833 --- /dev/null +++ b/db/migrate/20160105181819_derivative_add_managed.rb @@ -0,0 +1,21 @@ +class DerivativeAddManaged < ActiveRecord::Migration + + def up + say_with_time("Add Derivative.managed") do + Derivative.find_each({},{batch_size:5}) do |d| + d.managed ||= true; + d.save_as_version('R5'); + end + end + end + + def down + say_with_time("Remove Derivative.managed") do + Derivative.find_each({},{batch_size:5}) do |d| + d.managed = nil; + d.save_as_version('R4'); + end + end + end + +end diff --git a/db/migrate/20160122203634_add_resource_types_to_display_metadata.rb b/db/migrate/20160122203634_add_resource_types_to_display_metadata.rb new file mode 100644 index 0000000000..81476132c7 --- /dev/null +++ b/db/migrate/20160122203634_add_resource_types_to_display_metadata.rb @@ -0,0 +1,16 @@ +class AddResourceTypesToDisplayMetadata < ActiveRecord::Migration + + def up + say_with_time("Add resource types to displaymetadata") do + MediaObject.find_each({},{batch_size:5}) do |mo| + mo.set_resource_types! + mo.save + end + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end + +end diff --git a/db/migrate/20160427165749_create_playlists.rb b/db/migrate/20160427165749_create_playlists.rb new file mode 100644 index 0000000000..68ca2bd45b --- /dev/null +++ b/db/migrate/20160427165749_create_playlists.rb @@ -0,0 +1,12 @@ +class CreatePlaylists < ActiveRecord::Migration + def change + create_table :playlists do |t| + t.string :title + t.references :user, null: false, index: true + t.string :comment + t.string :visibility + + t.timestamps + end + end +end diff --git a/db/migrate/20160428133859_create_playlist_items.rb b/db/migrate/20160428133859_create_playlist_items.rb new file mode 100644 index 0000000000..f3c76a39ea --- /dev/null +++ b/db/migrate/20160428133859_create_playlist_items.rb @@ -0,0 +1,11 @@ +class CreatePlaylistItems < ActiveRecord::Migration + def change + create_table :playlist_items do |t| + t.references :playlist, null: false, index: true + t.references :annotation, null: false, index: true + t.integer :position + + t.timestamps + end + end +end diff --git a/db/migrate/20160511155417_create_annotations.active_annotations.rb b/db/migrate/20160511155417_create_annotations.active_annotations.rb new file mode 100644 index 0000000000..863a45cafc --- /dev/null +++ b/db/migrate/20160511155417_create_annotations.active_annotations.rb @@ -0,0 +1,10 @@ +# This migration comes from active_annotations (originally 20160422052041) +class CreateAnnotations < ActiveRecord::Migration + def change + create_table :annotations do |t| + t.string :uuid + t.string :source_uri + t.text :annotation + end + end +end diff --git a/db/schema.rb b/db/schema.rb index da1831013e..a22603f6cd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,13 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150625121750) do +ActiveRecord::Schema.define(version: 20160511155417) do + + create_table "annotations", force: true do |t| + t.string "uuid" + t.string "source_uri" + t.text "annotation" + end create_table "bookmarks", force: true do |t| t.integer "user_id", null: false @@ -66,6 +72,28 @@ t.string "name", limit: 50 end + create_table "playlist_items", force: true do |t| + t.integer "playlist_id", null: false + t.integer "annotation_id", null: false + t.integer "position" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "playlist_items", ["annotation_id"], name: "index_playlist_items_on_annotation_id" + add_index "playlist_items", ["playlist_id"], name: "index_playlist_items_on_playlist_id" + + create_table "playlists", force: true do |t| + t.string "title" + t.integer "user_id", null: false + t.string "comment" + t.string "visibility" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "playlists", ["user_id"], name: "index_playlists_on_user_id" + create_table "role_maps", force: true do |t| t.string "entry" t.integer "parent_id" @@ -82,8 +110,8 @@ add_index "searches", ["user_id"], name: "index_searches_on_user_id" create_table "sessions", force: true do |t| - t.string "session_id", null: false - t.text "data" + t.string "session_id", null: false + t.text "data", limit: 16777215 t.datetime "created_at" t.datetime "updated_at" end diff --git a/felix b/felix index b34b990852..b5f720c64c 160000 --- a/felix +++ b/felix @@ -1 +1 @@ -Subproject commit b34b9908529bcb7b7b2db9833d7f3ea134d064d1 +Subproject commit b5f720c64cf73985a3ca7bfd22eda55e27496ea0 diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 8f25538889..549d414c96 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -67,6 +67,15 @@ def valid? def file_valid?(file_spec) valid = true + # Check date_digitized for valid format + if file_spec[:date_digitized].present? + begin + DateTime.parse(file_spec[:date_digitized]) + rescue ArgumentError + @errors.add(:date_digitized, "Invalid date_digitized: #{file_spec[:date_digitized]}. Recommended format: yyyy-mm-dd.") + valid = false + end + end # Check file offsets for valid format if file_spec[:offset].present? && !Avalon::Batch::Entry.offset_valid?(file_spec[:offset]) @errors.add(:offset, "Invalid offset: #{file_spec[:offset]}") @@ -110,11 +119,17 @@ def self.offset_valid?( offset ) true end - def self.attach_structure_to_master_file( master_file, filename ) + def self.attach_datastreams_to_master_file( master_file, filename ) structural_file = "#{filename}.structure.xml" if File.exists? structural_file master_file.structuralMetadata.content=File.open(structural_file) end + captions_file = "#{filename}.vtt" + if File.exists? captions_file + master_file.captions.content=File.open(captions_file) + master_file.captions.mimeType='text/vtt' + master_file.captions.dsLabel=captions_file + end end def process! @@ -125,12 +140,13 @@ def process! master_file.save(validate: false) #required: need pid before setting mediaobject master_file.mediaobject = media_object files = self.class.gatherFiles(file_spec[:file]) - self.class.attach_structure_to_master_file(master_file, file_spec[:file]) + self.class.attach_datastreams_to_master_file(master_file, file_spec[:file]) master_file.setContent(files) master_file.absolute_location = file_spec[:absolute_location] if file_spec[:absolute_location].present? master_file.label = file_spec[:label] if file_spec[:label].present? master_file.poster_offset = file_spec[:offset] if file_spec[:offset].present? - + master_file.date_digitized = DateTime.parse(file_spec[:date_digitized]).to_time.utc.iso8601 if file_spec[:date_digitized].present? + #Make sure to set content before setting the workflow master_file.set_workflow(file_spec[:skip_transcoding] ? 'skip_transcoding' : nil) if master_file.save diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index 8ab8a39a5f..a5b27960b6 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -21,7 +21,7 @@ class Manifest extend Forwardable EXTENSIONS = ['csv','xls','xlsx','ods'] - FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location] + FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized] SKIP_FIELDS = [:collection] def_delegators :@entries, :each @@ -166,7 +166,6 @@ def create_entries! opts[opt] = val end } - entries << Entry.new(fields.select { |f| !FILE_FIELDS.include?(f) }, content, opts, index, self) end end diff --git a/lib/avalon/bib_retriever/MARC21slim2MODS3-5-avalon.xsl b/lib/avalon/bib_retriever/MARC21slim2MODS3-5-avalon.xsl index 492373a640..e82ddc9040 100644 --- a/lib/avalon/bib_retriever/MARC21slim2MODS3-5-avalon.xsl +++ b/lib/avalon/bib_retriever/MARC21slim2MODS3-5-avalon.xsl @@ -46,6 +46,8 @@ Removed 041 subfields except for $d and $j. kdm 20150429 Replaced use of 003 value for recordInfo\recordIdentifer@source with "local". kdm 20150430 Added type="other" for 024 ind1=8. kdm 20150501 Removed identifiers except to . kdm 20150514 +Added check that controlField008-35-37 variable is not set to 'N/A' from old cataloging practices. jlh 20151109 +Convert unknown dates (uuuu) to EDTF (unknown/unknown). bwk 20160223 --> + - + + + unknown/unknown + + + + + - + + + unknown/unknown + + + + + @@ -1118,7 +1135,14 @@ Revision 1.02 - Added Log Comment 2003/03/24 19:37:42 ckeith - + + + unknown/unknown + + + + + @@ -1413,7 +1437,7 @@ Revision 1.02 - Added Log Comment 2003/03/24 19:37:42 ckeith + select="normalize-space(translate(substring($controlField008,36,3),'|#','')) != 'N/A'"/> @@ -2095,11 +2119,11 @@ Revision 1.02 - Added Log Comment 2003/03/24 19:37:42 ckeith - + id - all_text_timv + title_tesi^10.0 + section_label_tesim^5.0 + creator_ssim^3.0 + date_sim^3.0 + all_text_timv^1.0 active_fedora_model_ssi object_type_si - all_text_timv^10 + title_tesi^10.0 + section_label_tesim^5.0 + creator_ssim^3.0 + date_sim^3.0 + all_text_timv^1.0 diff --git a/spec/config/authentication.yml b/spec/config/authentication.yml index 96fac0985f..7e67c70532 100644 --- a/spec/config/authentication.yml +++ b/spec/config/authentication.yml @@ -10,3 +10,9 @@ :params: :oauth_credentials: key: 'secret' +- :name: Avalon JSON Api + :id: :api + :tokens: + 'secret_token': + :username: system_account_name + :email: system@example.edu diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index e0f770f458..812a9c6dfc 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -15,5 +15,23 @@ require 'spec_helper' describe ApplicationController do + controller do + def create + render nothing: true + end + end + context "normal auth" do + it "should check for authenticity token" do + expect(controller).to receive(:verify_authenticity_token) + post :create + end + end + context "ingest API" do + it "should not check for authenticity token for API requests" do + request.headers['Avalon-Api-Key'] = 'secret_token' + expect(controller).not_to receive(:verify_authenticity_token) + post :create + end + end end diff --git a/spec/controllers/avalon_annotation_controller_spec.rb b/spec/controllers/avalon_annotation_controller_spec.rb new file mode 100644 index 0000000000..51659eda06 --- /dev/null +++ b/spec/controllers/avalon_annotation_controller_spec.rb @@ -0,0 +1,111 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'spec_helper' + +describe AvalonAnnotationController do + subject(:video_master_file) { FactoryGirl.create(:master_file_with_derivative) } + let(:annotation) { AvalonAnnotation.new(master_file: video_master_file) } + + before :all do + @controller = AvalonAnnotationController.new + end + + describe 'creating an annotation and displaying it' do + it 'can create an annotation and display it as JSON' do + allow(MasterFile).to receive(:find).and_return(video_master_file) + post 'create', master_file: video_master_file.id + expect { JSON.parse(response.body) }.not_to raise_error + end + it 'raises an ArgumentError error when the master file is not supplied' do + expect { post 'create' }.to raise_error(ArgumentError, 'Master File Not Supplied') + end + it 'raises an error when the master fille cannot be found' do + expect { post 'create', master_file: 'OC' }.to raise_error(ActionController::RoutingError, 'Master File Not Found') + end + end + describe 'updating an annotation' do + it 'can update an annotation and display it as JSON' do + annotation.save! + put 'update', id: annotation.uuid, start_time: '60', end_time: '90', title: '30 Seconds of Fun', comment: 'Are we having fun yet?' + expect { JSON.parse(response.body) }.not_to raise_error + end + it 'raises an error when the annotation cannot be found' do + expect { put 'update', id: 'OC' }.to raise_error(ActionController::RoutingError, 'Annotation Not Found') + end + end + + describe 'displaying an annotation' do + it 'can display an annotation as JSON' do + annotation.save! + get 'show', id: annotation.uuid + expect { JSON.parse(response.body) }.not_to raise_error + end + it 'raises an error when it cannot find the annotation' do + expect { get 'show', id: 'OC' }.to raise_error(ActionController::RoutingError, 'Annotation Not Found') + end + end + describe 'destroying an annotation' do + it 'can destroy an annotation and returns the result as JSON' do + annotation.save! + delete 'destroy', id: annotation.uuid + resp = JSON.parse(response.body) + expect(resp['action']).to match('destroy') + expect(resp['id']).to match(annotation.uuid) + expect(resp['success']).to be_truthy + end + it 'raises an error when the annotation is not found to destroy' do + expect { delete 'destroy', id: 'OC' }.to raise_error(ActionController::RoutingError, 'Annotation Not Found') + end + end + + describe 'selected key updates' do + it 'updates multiple fields' do + annotation.save! + @controller.stub(:params).and_return(id: annotation.uuid, start_time: '17', end_time: '17', title: 'Detroit', comment: 'The founding') + @controller.lookup_annotation + expect(@controller.instance_variable_get(:@annotation)).to receive(:update).once + expect { @controller.selected_key_updates }.not_to raise_error + end + it 'does not update the annotation when no valid keys are passed' do + annotation.save! + @controller.stub(:params).and_return(id: annotation.uuid, s_time: '17', e_time: '17', name: 'Detroit', stuff: 'The founding') + @controller.lookup_annotation + expect(@controller.instance_variable_get(:@annotation)).not_to receive(:update) + expect { @controller.selected_key_updates }.not_to raise_error + end + end + describe 'looking up annotations' do + it 'raises an error when it cannot find an annotation' do + @controller.stub(:params).and_return(id: '1817') + expect { @controller.lookup_annotation }.to raise_error + end + it 'sets the annotation class variable when it finds an annotation' do + annotation.save! + @controller.stub(:params).and_return(id: annotation.uuid) + expect { @controller.lookup_annotation }.not_to raise_error + end + end + describe 'raising errors' do + it 'raises ActionController::RoutingError referencing annotation by default' do + expect { @controller.not_found }.to raise_error(ActionController::RoutingError, 'Annotation Not Found') + end + it 'raises ActionController::RoutingError referencing annotation when passed :annotation' do + expect { @controller.not_found(item: :annotation) }.to raise_error(ActionController::RoutingError, 'Annotation Not Found') + end + it 'raises ActionController::RoutingError referencing master_file when passed :master_file' do + expect { @controller.not_found(item: :master_file) }.to raise_error(ActionController::RoutingError, 'Master File Not Found') + end + end +end diff --git a/spec/controllers/bookmarks_controller_spec.rb b/spec/controllers/bookmarks_controller_spec.rb index 2be114a55c..64eebdd246 100644 --- a/spec/controllers/bookmarks_controller_spec.rb +++ b/spec/controllers/bookmarks_controller_spec.rb @@ -150,6 +150,17 @@ expect(mo.read_users).to include 'cjcolvar' end end + it 'adds a time-based user to the selected items' do + post 'update_access_control', submit_add_user: 'Add', add_user_begin: Date.yesterday, add_user_end: Date.today, user: 'cjcolvar' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies[1].read_users).to include 'cjcolvar' + expect(mo.governing_policies[1].begin_time).to eq DateTime.parse(Date.yesterday.to_s).utc.beginning_of_day.iso8601 + expect(mo.governing_policies[1].end_time).to eq DateTime.parse(Date.today.to_s).utc.end_of_day.iso8601 + end + end + it 'removes a user from the selected items' do media_objects.each do |mo| mo.read_users += ["john.doe"] @@ -163,6 +174,19 @@ expect(mo.read_users).not_to include 'john.doe' end end + it 'removes a time-based user from the selected items' do + media_objects.each do |mo| + mo.governing_policies += [Lease.create(begin_time: Date.today-2.day, end_time: Date.yesterday, read_users: ['jane.doe'])] + mo.save + mo.reload + end + post 'update_access_control', submit_remove_user: 'Remove', user: 'john.doe' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies.collect{|p|p.read_users}.flatten.uniq.compact).not_to include 'john.doe' + end + end end context 'groups' do it 'adds a group to the selected items' do @@ -173,6 +197,16 @@ expect(mo.read_groups).to include 'students' end end + it 'adds a time-based group to the selected items' do + post 'update_access_control', submit_add_group: 'Add', add_group_begin: Date.yesterday, add_group_end: Date.today, group: 'students' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies[1].read_groups).to include 'students' + expect(mo.governing_policies[1].begin_time).to eq DateTime.parse(Date.yesterday.to_s).utc.beginning_of_day.iso8601 + expect(mo.governing_policies[1].end_time).to eq DateTime.parse(Date.today.to_s).utc.end_of_day.iso8601 + end + end it 'removes a group from the selected items' do media_objects.each do |mo| mo.read_groups += ["test-group"] @@ -186,6 +220,19 @@ expect(mo.read_groups).not_to include 'test-group' end end + it 'removes a time-based group from the selected items' do + media_objects.each do |mo| + mo.governing_policies += [Lease.create(begin_time: Date.today-2.day, end_time: Date.yesterday, read_groups: ['test-group'])] + mo.save + mo.reload + end + post 'update_access_control', submit_remove_group: 'Remove', group: 'test-group' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies.collect{|p|p.read_groups}.flatten.uniq.compact).not_to include 'test-group' + end + end end context 'external groups' do it 'adds an external group to the selected items' do @@ -196,6 +243,16 @@ expect(mo.read_groups).to include 'ECON-101' end end + it 'adds a time-based external group to the selected items' do + post 'update_access_control', submit_add_class: 'Add', add_class_begin: Date.yesterday, add_class_end: Date.today, class: 'ECON-101' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies[1].read_groups).to include 'ECON-101' + expect(mo.governing_policies[1].begin_time).to eq DateTime.parse(Date.yesterday.to_s).utc.beginning_of_day.iso8601 + expect(mo.governing_policies[1].end_time).to eq DateTime.parse(Date.today.to_s).utc.end_of_day.iso8601 + end + end it 'removes an external group from the selected items' do media_objects.each do |mo| mo.read_groups += ["MUSIC-101"] @@ -209,6 +266,65 @@ expect(mo.read_groups).not_to include 'MUSIC-101' end end + it 'removes a time-based external group from the selected items' do + media_objects.each do |mo| + mo.governing_policies += [Lease.create(begin_time: Date.today-2.day, end_time: Date.yesterday, read_groups: ['MUSIC-101'])] + mo.save + mo.reload + end + post 'update_access_control', submit_remove_class: 'Remove', class: 'MUSIC-101' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies.collect{|p|p.read_groups}.flatten.uniq.compact).not_to include 'MUSIC-101' + end + end + end + context 'ip groups' do + it 'adds an ip group to the selected items' do + post 'update_access_control', submit_add_ipaddress: 'Add', ipaddress: '127.0.0.127' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.read_groups).to include '127.0.0.127' + end + end + it 'adds a time-based ip group to the selected items' do + post 'update_access_control', submit_add_ipaddress: 'Add', add_ipaddress_begin: Date.yesterday, add_ipaddress_end: Date.today, ipaddress: '127.0.0.127' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies[1].read_groups).to include '127.0.0.127' + expect(mo.governing_policies[1].begin_time).to eq DateTime.parse(Date.yesterday.to_s).utc.beginning_of_day.iso8601 + expect(mo.governing_policies[1].end_time).to eq DateTime.parse(Date.today.to_s).utc.end_of_day.iso8601 + end + end + it 'removes an ip group from the selected items' do + media_objects.each do |mo| + mo.read_groups += ["127.0.0.127"] + mo.save + mo.reload + end + post 'update_access_control', submit_remove_ipaddress: 'Remove', ipaddress: '127.0.0.127' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.read_groups).not_to include '127.0.0.127' + end + end + it 'removes a time-based ip group from the selected items' do + media_objects.each do |mo| + mo.governing_policies += [Lease.create(begin_time: Date.today-2.day, end_time: Date.yesterday, read_groups: ['127.0.0.127'])] + mo.save + mo.reload + end + post 'update_access_control', submit_remove_ipaddress: 'Remove', ipaddress: '127.0.0.127' + expect(flash[:success]).to eq( I18n.t("blacklight.update_access_control.success", count: 3)) + media_objects.each do |mo| + mo.reload + expect(mo.governing_policies.collect{|p|p.read_groups}.flatten.uniq.compact).not_to include '127.0.0.127' + end + end end end end diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb index 5e16ae43ca..dd8e85e6e5 100644 --- a/spec/controllers/catalog_controller_spec.rb +++ b/spec/controllers/catalog_controller_spec.rb @@ -20,24 +20,24 @@ it "should show results for items that are public and published" do mo = FactoryGirl.create(:published_media_object, visibility: 'public') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(1) - assigns(:document_list).map(&:id).should == [mo.id] + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(1) + expect(assigns(:document_list).map(&:id)).to eq([mo.id]) end it "should not show results for items that are not public" do mo = FactoryGirl.create(:published_media_object, visibility: 'restricted') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(0) + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(0) end it "should not show results for items that are not published" do mo = FactoryGirl.create(:media_object, visibility: 'public') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(0) + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(0) end end describe "as an authenticated user" do @@ -47,24 +47,24 @@ it "should show results for items that are published and available to registered users" do mo = FactoryGirl.create(:published_media_object, visibility: 'restricted') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(1) - assigns(:document_list).map(&:id).should == [mo.id] + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(1) + expect(assigns(:document_list).map(&:id)).to eq([mo.id]) end it "should not show results for items that are not public or available to registered users" do mo = FactoryGirl.create(:published_media_object, visibility: 'private') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(0) + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(0) end it "should not show results for items that are not published" do mo = FactoryGirl.create(:media_object, visibility: 'public') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(0) + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(0) end end describe "as a manager" do @@ -74,47 +74,106 @@ it "should show results for items that are unpublished, private, and belong to one of my collections" do mo = FactoryGirl.create(:media_object, visibility: 'private', collection: collection) get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(1) - assigns(:document_list).map(&:id).should == [mo.id] + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(1) + expect(assigns(:document_list).map(&:id)).to eq([mo.id]) + end + it "should show results for items that are hidden and belong to one of my collections" do + mo = FactoryGirl.create(:media_object, hidden: true, visibility: 'private', collection: collection) + get 'index', :q => "" + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(1) + expect(assigns(:document_list).map(&:id)).to eq([mo.id]) + end + it "should show results for items that are not hidden and do not belong to one of my collections along with hidden items that belong to my collections" do + mo = FactoryGirl.create(:media_object, hidden: true, visibility: 'private', collection: collection) + mo2 = FactoryGirl.create(:fully_searchable_media_object) + get 'index', :q => "" + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(2) + expect(assigns(:document_list).map(&:id)).to match_array([mo.id, mo2.id]) end it "should not show results for items that do not belong to one of my collections" do mo = FactoryGirl.create(:media_object, visibility: 'private') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(0) + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(0) + end + it "should not show results for hidden items that do not belong to one of my collections" do + mo = FactoryGirl.create(:media_object, hidden: true, visibility: 'private', read_users: [manager.username]) + get 'index', :q => "" + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(0) end end - describe "as an administrator" do - let!(:administrator) {login_as(:administrator)} + describe "as an administrator" do + let!(:administrator) {login_as(:administrator)} - it "should show results for all items" do + it "should show results for all items" do mo = FactoryGirl.create(:media_object, visibility: 'private') get 'index', :q => "" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(1) - assigns(:document_list).map(&:id).should == [mo.id] + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(1) + expect(assigns(:document_list).map(&:id)).to eq([mo.id]) end - end + end describe "as an lti user" do let!(:user) { login_lti 'student' } let!(:lti_group) { @controller.user_session[:virtual_groups].first } it "should show results for items visible to the lti virtual group" do mo = FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) get 'index', :q => "read_access_virtual_group_ssim:#{lti_group}" - response.should be_success - response.should render_template('catalog/index') - assigns(:document_list).count.should eql(1) - assigns(:document_list).map(&:id).should == [mo.id] + expect(response).to be_success + expect(response).to render_template('catalog/index') + expect(assigns(:document_list).count).to eql(1) + expect(assigns(:document_list).map(&:id)).to eq([mo.id]) + end + end + + describe "as an unauthenticated user with a specific IP address" do + before(:each) do + @user = login_as 'public' + @ip_address1 = Faker::Internet.ip_v4_address + @mo = FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [@ip_address1]) + end + it "should show no results when no items are visible to the user's IP address" do + get 'index', :q => "" + expect(assigns(:document_list).count).to eq 0 + end + it "should show results for items visible to the the user's IP address" do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(@ip_address1) + get 'index', :q => "" + expect(assigns(:document_list).count).to eq 1 + expect(assigns(:document_list).map(&:id)).to include @mo.id + end + it "should show results for items visible to the the user's IPv4 subnet" do + ip_address2 = Faker::Internet.ip_v4_address + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(ip_address2) + mo2 = FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [ip_address2+'/30']) + get 'index', :q => "" + expect(assigns(:document_list).count).to be >= 1 + expect(assigns(:document_list).map(&:id)).to include mo2.id + end + it "should show results for items visible to the the user's IPv6 subnet" do + ip_address3 = Faker::Internet.ip_v6_address + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(ip_address3) + mo3 = FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [ip_address3+'/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00']) + get 'index', :q => "" + expect(assigns(:document_list).count).to be >= 1 + expect(assigns(:document_list).map(&:id)).to include mo3.id end end describe "search fields" do let(:media_object) { FactoryGirl.create(:fully_searchable_media_object) } - ["title_tesi", "creator_ssim", "contributor_sim", "unit_ssim", "collection_ssim", "summary_ssi", "publisher_sim", "subject_topic_sim", "subject_geographic_sim", "subject_temporal_sim", "genre_sim", "physical_description_si", "language_sim", "date_sim", "notes_sim", "table_of_contents_sim", "other_identifier_sim" ].each do |field| +# ["title_tesi", "creator_ssim", "contributor_sim", "unit_ssim", "collection_ssim", "summary_ssi", "publisher_sim", "subject_topic_sim", "subject_geographic_sim", "subject_temporal_sim", "genre_sim", "physical_description_sim", "language_sim", "date_sim", "notes_sim", "table_of_contents_sim", "other_identifier_sim", "date_ingested_sim" ].each do |field| + ["title_tesi", "creator_ssim", "contributor_sim", "unit_ssim", "collection_ssim", "summary_ssi", "publisher_sim", "subject_topic_sim", "subject_geographic_sim", "subject_temporal_sim", "genre_sim", "physical_description_sim", "language_sim", "date_sim", "notes_sim", "table_of_contents_sim", "other_identifier_sim" ].each do |field| it "should find results based upon #{field}" do query = Array(media_object.to_solr[field]).first #split on ' ' and only search on the first word of a multiword field value @@ -128,6 +187,30 @@ end end end + describe "search structure" do + before(:each) do + @media_object = FactoryGirl.create(:fully_searchable_media_object) + @master_file = FactoryGirl.create(:master_file_with_structure, mediaobject_id: @media_object.pid, label: 'Test Label') + @media_object.parts += [@master_file] + @media_object.save! + end + it "should find results based upon structure" do + get 'index', q: 'CD 1' + expect(assigns(:document_list).count).to eq 1 + expect(assigns(:document_list).collect(&:id)). to eq [@media_object.id] + end + it 'should find results based upon section labels' do + get 'index', q: 'Test Label' + expect(assigns(:document_list).count).to eq 1 + expect(assigns(:document_list).collect(&:id)). to eq [@media_object.id] + end + it 'should find items in correct relevancy order' do + media_object_1 = FactoryGirl.create(:fully_searchable_media_object, title: 'Test Label') + get 'index', q: 'Test Label' + expect(assigns(:document_list).count).to eq 2 + expect(assigns(:document_list).collect(&:id)).to eq [media_object_1.id, @media_object.id] + end + end describe "sort fields" do let!(:m1) { FactoryGirl.create(:published_media_object, title: 'Yabba', date_issued: '1960', creator: ['Fred'], visibility: 'public') } diff --git a/spec/controllers/collections_controller_spec.rb b/spec/controllers/collections_controller_spec.rb index 343a30ca05..3cc041c7e4 100644 --- a/spec/controllers/collections_controller_spec.rb +++ b/spec/controllers/collections_controller_spec.rb @@ -17,38 +17,78 @@ describe Admin::CollectionsController, type: :controller do render_views + describe 'security' do + let(:collection) { FactoryGirl.create(:collection) } + describe 'ingest api' do + it "all routes should return 401 when no token is present" do + expect(get :index, format: 'json').to have_http_status(401) + expect(get :show, id: collection.id, format: 'json').to have_http_status(401) + expect(get :items, id: collection.id, format: 'json').to have_http_status(401) + expect(post :create, format: 'json').to have_http_status(401) + expect(put :update, id: collection.id, format: 'json').to have_http_status(401) + end + it "all routes should return 403 when a bad token in present" do + request.headers['Avalon-Api-Key'] = 'badtoken' + expect(get :index, format: 'json').to have_http_status(403) + expect(get :show, id: collection.id, format: 'json').to have_http_status(403) + expect(get :items, id: collection.id, format: 'json').to have_http_status(403) + expect(post :create, format: 'json').to have_http_status(403) + expect(put :update, id: collection.id, format: 'json').to have_http_status(403) + end + end + describe 'normal auth' do + context 'with end-user' do + before do + login_as :user + end + #New is isolated here due to issues caused by the controller instance not being regenerated + it "should redirect to /" do + expect(get :new).to redirect_to(root_path) + end + it "all routes should redirect to /" do + expect(get :index).to redirect_to(root_path) + expect(get :show, id: collection.id).to redirect_to(root_path) + expect(get :edit, id: collection.id).to redirect_to(root_path) + expect(get :remove, id: collection.id).to redirect_to(root_path) + expect(post :create).to redirect_to(root_path) + expect(put :update, id: collection.id).to redirect_to(root_path) + expect(patch :update, id: collection.id).to redirect_to(root_path) + expect(delete :destroy, id: collection.id).to redirect_to(root_path) + end + end + end + end + describe "#manage" do let!(:collection) { FactoryGirl.create(:collection) } before(:each) do request.env["HTTP_REFERER"] = '/' + login_as(:administrator) end it "should add users to manager role" do - login_as(:administrator) manager = FactoryGirl.create(:manager) put 'update', id: collection.id, submit_add_manager: 'Add', add_manager: manager.username collection.reload - manager.should be_in(collection.managers) + expect(manager).to be_in(collection.managers) end it "should not add users to manager role" do - login_as(:administrator) user = FactoryGirl.create(:user) put 'update', id: collection.id, submit_add_manager: 'Add', add_manager: user.username collection.reload - user.should_not be_in(collection.managers) - flash[:notice].should_not be_empty + expect(user).not_to be_in(collection.managers) + expect(flash[:notice]).not_to be_empty end it "should remove users from manager role" do - login_as(:administrator) #initial_manager = FactoryGirl.create(:manager).username collection.managers += [FactoryGirl.create(:manager).username] collection.save! manager = User.where(username: collection.managers.first).first put 'update', id: collection.id, remove_manager: manager.username collection.reload - manager.should_not be_in(collection.managers) + expect(manager).not_to be_in(collection.managers) end end @@ -63,7 +103,7 @@ editor = FactoryGirl.build(:user) put 'update', id: collection.id, submit_add_editor: 'Add', add_editor: editor.username collection.reload - editor.should be_in(collection.editors) + expect(editor).to be_in(collection.editors) end it "should remove users from editor role" do @@ -71,7 +111,7 @@ editor = User.where(username: collection.editors.first).first put 'update', id: collection.id, remove_editor: editor.username collection.reload - editor.should_not be_in(collection.editors) + expect(editor).not_to be_in(collection.editors) end end @@ -86,7 +126,7 @@ depositor = FactoryGirl.build(:user) put 'update', id: collection.id, submit_add_depositor: 'Add', add_depositor: depositor.username collection.reload - depositor.should be_in(collection.depositors) + expect(depositor).to be_in(collection.depositors) end it "should remove users from depositor role" do @@ -94,7 +134,48 @@ depositor = User.where(username: collection.depositors.first).first put 'update', id: collection.id, remove_depositor: depositor.username collection.reload - depositor.should_not be_in(collection.depositors) + expect(depositor).not_to be_in(collection.depositors) + end + end + + describe "#index" do + let!(:collection) { FactoryGirl.create(:collection) } + subject(:json) { JSON.parse(response.body) } + + it "should return list of collections" do + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'index', format:'json' + expect(json.count).to eq(1) + expect(json.first['id']).to eq(collection.pid) + expect(json.first['name']).to eq(collection.name) + expect(json.first['unit']).to eq(collection.unit) + expect(json.first['description']).to eq(collection.description) + expect(json.first['object_count']['total']).to eq(collection.media_objects.count) + expect(json.first['object_count']['published']).to eq(collection.media_objects.reject{|mo| !mo.published?}.count) + expect(json.first['object_count']['unpublished']).to eq(collection.media_objects.reject{|mo| mo.published?}.count) + expect(json.first['roles']['managers']).to eq(collection.managers) + expect(json.first['roles']['editors']).to eq(collection.editors) + expect(json.first['roles']['depositors']).to eq(collection.depositors) + end + end + + describe 'pagination' do + subject(:json) { JSON.parse(response.body) } + it 'should paginate index' do + 5.times { FactoryGirl.create(:collection) } + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'index', format:'json', per_page: '2' + expect(json.count).to eq(2) + expect(response.headers['Per-Page']).to eq('2') + expect(response.headers['Total']).to eq('5') + end + it 'should paginate collection/items' do + collection = FactoryGirl.create(:collection, items: 5) + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'items', id: collection.pid, format: 'json', per_page: '2' + expect(json.count).to eq(2) + expect(response.headers['Per-Page']).to eq('2') + expect(response.headers['Total']).to eq('5') end end @@ -104,43 +185,112 @@ it "should allow access to managers" do login_user(collection.managers.first) get 'show', id: collection.id - response.should be_ok + expect(response).to be_ok end - it "should redirect to collections index when manager doesn't have access" do - login_as(:manager) - get 'show', id: collection.id - response.should redirect_to(admin_collections_path) + context "with json format" do + subject(:json) { JSON.parse(response.body) } + + it "should return json for specific collection" do + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'show', id: collection.pid, format:'json' + expect(json['id']).to eq(collection.pid) + expect(json['name']).to eq(collection.name) + expect(json['unit']).to eq(collection.unit) + expect(json['description']).to eq(collection.description) + expect(json['object_count']['total']).to eq(collection.media_objects.count) + expect(json['object_count']['published']).to eq(collection.media_objects.reject{|mo| !mo.published?}.count) + expect(json['object_count']['unpublished']).to eq(collection.media_objects.reject{|mo| mo.published?}.count) + expect(json['roles']['managers']).to eq(collection.managers) + expect(json['roles']['editors']).to eq(collection.editors) + expect(json['roles']['depositors']).to eq(collection.depositors) + end + + it "should return 404 if requested collection not present" do + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'show', id: 'avalon:doesnt_exist', format: 'json' + expect(response.status).to eq(404) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end + end + + describe "#items" do + let!(:collection) { FactoryGirl.create(:collection, items: 2) } + + it "should return json for specific collection's media objects" do + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'items', id: collection.pid, format: 'json' + expect(JSON.parse(response.body)).to include(collection.media_objects[0].pid,collection.media_objects[1].pid) + #TODO add check that mediaobject is serialized to json properly end + end describe "#create" do + let!(:collection) { FactoryGirl.build(:collection) } + it "should notify administrators" do - login_as(:administrator) #Login as admin so there will be at least one administrator to get an email + login_as(:administrator) #otherwise, there are no administrators to mail mock_delay = double('mock_delay').as_null_object - NotificationsMailer.stub(:delay).and_return(mock_delay) - mock_delay.should_receive(:new_collection) - @collection = FactoryGirl.build(:collection) - post 'create', admin_collection: {name: @collection.name, description: @collection.description, unit: @collection.unit} + allow(NotificationsMailer).to receive(:delay).and_return(mock_delay) + expect(mock_delay).to receive(:new_collection) + request.headers['Avalon-Api-Key'] = 'secret_token' + post 'create', format:'json', admin_collection: {name: collection.name, description: collection.description, unit: collection.unit, managers: collection.managers} end + it "should create a new collection" do + request.headers['Avalon-Api-Key'] = 'secret_token' + post 'create', format:'json', admin_collection: {name: collection.name, description: collection.description, unit: collection.unit, managers: collection.managers} + expect(JSON.parse(response.body)['id'].class).to eq String + expect(JSON.parse(response.body)).not_to include('errors') + end + it "should return 422 if collection creation failed" do + request.headers['Avalon-Api-Key'] = 'secret_token' + post 'create', format:'json', admin_collection: {name: collection.name, description: collection.description, unit: collection.unit} + expect(response.status).to eq(422) + expect(JSON.parse(response.body)).to include('errors') + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end describe "#update" do it "should notify administrators if name changed" do - login_as(:administrator) #Login as admin so there will be at least one administrator to get an email + login_as(:administrator) #otherwise, there are no administrators to mail mock_delay = double('mock_delay').as_null_object - NotificationsMailer.stub(:delay).and_return(mock_delay) - mock_delay.should_receive(:update_collection) + allow(NotificationsMailer).to receive(:delay).and_return(mock_delay) + expect(mock_delay).to receive(:update_collection) @collection = FactoryGirl.create(:collection) put 'update', id: @collection.pid, admin_collection: {name: "#{@collection.name}-new", description: @collection.description, unit: @collection.unit} end - context "access controls" do + context "update REST API" do let!(:collection) { FactoryGirl.create(:collection)} - before(:each) do - login_as(:administrator) - end + it "should update a collection via API" do + old_description = collection.description + request.headers['Avalon-Api-Key'] = 'secret_token' + put 'update', format: 'json', id: collection.pid, admin_collection: {description: collection.description+'new'} + expect(JSON.parse(response.body)['id'].class).to eq String + expect(JSON.parse(response.body)).not_to include('errors') + collection.reload + expect(collection.description).to eq old_description+'new' + end + it "should return 422 if collection update via API failed" do + allow_any_instance_of(Admin::Collection).to receive(:save).and_return false + request.headers['Avalon-Api-Key'] = 'secret_token' + put 'update', format: 'json', id: collection.pid, admin_collection: {description: collection.description+'new'} + expect(response.status).to eq(422) + expect(JSON.parse(response.body)).to include('errors') + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end + + context "access controls" do + let!(:collection) { FactoryGirl.create(:collection)} it "should not allow empty user" do expect{ put 'update', id: collection.pid, submit_add_user: "Add", add_user: "", add_user_display: ""}.not_to change{ collection.reload.default_read_users.size } diff --git a/spec/controllers/dropbox_controller_spec.rb b/spec/controllers/dropbox_controller_spec.rb index acf7be61f2..e35f31c90a 100644 --- a/spec/controllers/dropbox_controller_spec.rb +++ b/spec/controllers/dropbox_controller_spec.rb @@ -26,18 +26,18 @@ @collection = FactoryGirl.create(:collection) @temp_files = (0..20).map{|index| { name: "a_movie_#{index}.mov" } } @dropbox = double(Avalon::Dropbox) - @dropbox.stub(:all).and_return @temp_files - Avalon::Dropbox.stub(:new).and_return(@dropbox) + allow(@dropbox).to receive(:all).and_return @temp_files + allow(Avalon::Dropbox).to receive(:new).and_return(@dropbox) end it 'deletes video/audio files' do - @dropbox.should_receive(:delete).exactly(@temp_files.count).times + expect(@dropbox).to receive(:delete).exactly(@temp_files.count).times delete :bulk_delete, { :collection_id => @collection.pid, :filenames => @temp_files.map{|f| f[:name] } } end it "should allow the collection manager to delete" do login_user @collection.managers.first - @dropbox.should_receive(:delete).exactly(@temp_files.count).times + expect(@dropbox).to receive(:delete).exactly(@temp_files.count).times delete :bulk_delete, {:collection_id => @collection.pid, :filenames => @temp_files.map{|f| f[:name]}} expect(response.status).to be(200) end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index b9267987a2..7b3365748c 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -23,8 +23,8 @@ describe "creating a new group" do it "should redirect to sign in page with a notice when unauthenticated" do expect { get 'new' }.not_to change { Admin::Group.all.count } - flash[:notice].should_not be_nil - response.should redirect_to(new_user_session_path) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(new_user_session_path) end it "should redirect to home page with a notice when authenticated but unauthorized" do @@ -38,8 +38,8 @@ group = FactoryGirl.create(:group) login_as('policy_editor') expect { post 'create', admin_group: group.name }.not_to change {Admin::Group.all.count } - flash[:error].should_not be_nil - response.should redirect_to(admin_groups_path) + expect(flash[:error]).not_to be_nil + expect(response).to redirect_to(admin_groups_path) end context "Default permissions should be applied" do @@ -47,8 +47,8 @@ login_as('policy_editor') expect { post 'create', admin_group: test_group }.to change { Admin::Group.all.count } g = Admin::Group.find(test_group) - g.should_not be_nil - response.should redirect_to(edit_admin_group_path(g)) + expect(g).not_to be_nil + expect(response).to redirect_to(edit_admin_group_path(g)) end end @@ -64,16 +64,16 @@ context "editing a group" do it "should redirect to sign in page with a notice on when unauthenticated" do get 'edit', id: group.name - flash[:notice].should_not be_nil - response.should redirect_to(new_user_session_path) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(new_user_session_path) end it "should redirect to home page with a notice when authenticated but unauthorized" do login_as('student') get 'edit', id: group.name - flash[:notice].should_not be_nil - response.should redirect_to(root_path) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(root_path) end it "should be able to change group users when authenticated and authorized" do @@ -84,9 +84,9 @@ group_name = group.name group = Admin::Group.find(group_name) - group.users.should include(new_user) - flash[:notice].should_not be_nil - response.should redirect_to(edit_admin_group_path(Admin::Group.find(group.name))) + expect(group.users).to include(new_user) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(edit_admin_group_path(Admin::Group.find(group.name))) end it "should be able to change group name when authenticated and authorized" do @@ -96,18 +96,18 @@ put 'update', group_name: new_group_name, id: group.name new_group = Admin::Group.find(new_group_name) - new_group.should_not be_nil - new_group.users.should == group.users - flash[:notice].should_not be_nil - response.should redirect_to(edit_admin_group_path(new_group)) + expect(new_group).not_to be_nil + expect(new_group.users).to eq(group.users) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(edit_admin_group_path(new_group)) end it "should not be able to rename system groups" do login_as('administrator') put 'update', group_name: Faker::Lorem.word, id: 'manager' - Admin::Group.find('manager').should_not be_nil - flash[:error].should_not be_nil + expect(Admin::Group.find('manager')).not_to be_nil + expect(flash[:error]).not_to be_nil end it "should be able to remove users from a group" do @@ -116,8 +116,8 @@ put 'update_users', id: group.name, user_ids: Admin::Group.find(group.name).users - Admin::Group.find(group.name).users.should be_empty - flash[:error].should be_nil + expect(Admin::Group.find(group.name).users).to be_empty + expect(flash[:error]).to be_nil end it "should not remove users from the manager group if they are sole managers of a collection" do @@ -129,7 +129,7 @@ put 'update_users', id: 'manager', user_ids: [manager_name] expect(Admin::Group.find('manager').users).to include(manager_name) - flash[:error].should_not be_nil + expect(flash[:error]).not_to be_nil end ['administrator','group_manager'].each do |g| @@ -139,9 +139,9 @@ put 'update', id: g, new_user: new_user group = Admin::Group.find(g) - group.users.should include(new_user) - flash[:notice].should_not be_nil - response.should redirect_to(edit_admin_group_path(Admin::Group.find(group.name))) + expect(group.users).to include(new_user) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(edit_admin_group_path(Admin::Group.find(group.name))) end it "should not be able to manage #{g} group as a group_manager" do @@ -150,9 +150,9 @@ put 'update', id: g, new_user: new_user group = Admin::Group.find(g) - group.users.should_not include(new_user) - flash[:error].should_not be_nil - response.should redirect_to(admin_groups_path) + expect(group.users).not_to include(new_user) + expect(flash[:error]).not_to be_nil + expect(response).to redirect_to(admin_groups_path) end end end @@ -161,24 +161,24 @@ it "should redirect to sign in page with a notice on when unauthenticated" do expect { put 'update_multiple', group_ids: [group.name] }.not_to change { RoleControls.users(group.name) } - flash[:notice].should_not be_nil - response.should redirect_to(new_user_session_path) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(new_user_session_path) end it "should redirect to home page with a notice when authenticated but unauthorized" do login_as('student') expect { put 'update_multiple', group_ids: [group.name] }.not_to change { RoleControls.users(group.name) } - flash[:notice].should_not be_nil - response.should redirect_to(root_path) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(root_path) end it "should be able to change group users when authenticated and authorized" do login_as('policy_editor') expect { put 'update_multiple', group_ids: [group.name] }.to change { RoleControls.users(group.name) } - flash[:notice].should_not be_nil - response.should redirect_to(admin_groups_path) + expect(flash[:notice]).not_to be_nil + expect(response).to redirect_to(admin_groups_path) end end end diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index a05438f858..3ae91bb949 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -33,11 +33,11 @@ request.env["HTTP_REFERER"] = "/" @file = fixture_file_upload('/videoshort.mp4', 'video/mp4') - @file.stub(:size).and_return(MasterFile::MAXIMUM_UPLOAD_SIZE + 2^21) + allow(@file).to receive(:size).and_return(MasterFile::MAXIMUM_UPLOAD_SIZE + 2^21) expect { post :create, Filedata: [@file], original: 'any', container_id: media_object.pid}.not_to change { MasterFile.count } - flash[:error].should_not be_nil + expect(flash[:error]).not_to be_nil end end @@ -50,9 +50,9 @@ container_id: media_object.pid master_file = media_object.reload.parts.first - master_file.file_format.should eq "Moving image" + expect(master_file.file_format).to eq "Moving image" - flash[:errors].should be_nil + expect(flash[:errors]).to be_nil end it "should recognize an audio format" do @@ -63,7 +63,7 @@ container_id: media_object.pid master_file = media_object.reload.parts.first - master_file.file_format.should eq "Sound" + expect(master_file.file_format).to eq "Sound" end it "should reject non audio/video format" do @@ -73,7 +73,7 @@ expect { post :create, Filedata: [@file], original: 'any', container_id: media_object.pid }.not_to change { MasterFile.count } - flash[:error].should_not be_nil + expect(flash[:error]).not_to be_nil end it "should recognize audio/video based on extension when MIMETYPE is of unknown format" do @@ -84,9 +84,9 @@ original: 'any', container_id: media_object.pid master_file = MasterFile.all.last - master_file.file_format.should eq "Moving image" + expect(master_file.file_format).to eq "Moving image" - flash[:errors].should be_nil + expect(flash[:errors]).to be_nil end end @@ -101,22 +101,39 @@ class << @file post :create, Filedata: [@file], original: 'any', container_id: media_object.pid master_file = MasterFile.all.last - media_object.reload.parts.should include master_file - master_file.mediaobject.pid.should eq(media_object.pid) + expect(media_object.reload.parts).to include master_file + expect(master_file.mediaobject.pid).to eq(media_object.pid) - flash[:errors].should be_nil + expect(flash[:errors]).to be_nil end it "should associate a dropbox file" do skip - Avalon::Dropbox.any_instance.stub(:find).and_return "spec/fixtures/videoshort.mp4" + allow_any_instance_of(Avalon::Dropbox).to receive(:find).and_return "spec/fixtures/videoshort.mp4" post :create, dropbox: [{id: 1}], original: 'any', container_id: media_object.pid master_file = MasterFile.all.last media_object.reload - media_object.parts.should include master_file - master_file.mediaobject.pid.should eq(media_object.pid) + expect(media_object.parts).to include master_file + expect(master_file.mediaobject.pid).to eq(media_object.pid) - flash[:errors].should be_nil + expect(flash[:errors]).to be_nil + end + it "should not fail when associating with a published mediaobject" do + media_object = FactoryGirl.create(:published_media_object) + login_user media_object.collection.managers.first + @file = fixture_file_upload('/videoshort.mp4', 'video/mp4') + #Work-around for a Rails bug + class << @file + attr_reader :tempfile + end + + post :create, Filedata: [@file], original: 'any', container_id: media_object.pid + + master_file = MasterFile.all.last + expect(media_object.reload.parts).to include master_file + expect(master_file.mediaobject.pid).to eq(media_object.pid) + + expect(flash[:errors]).to be_nil end end end @@ -137,7 +154,7 @@ class << @file context "should no longer be associated with its parent object" do it "should create then remove a file from a video object" do expect { post :destroy, id: master_file.pid }.to change { MasterFile.count }.by(-1) - master_file.mediaobject.reload.parts.should_not include master_file + expect(master_file.mediaobject.reload.parts).not_to include master_file end end end @@ -146,7 +163,7 @@ class << @file let!(:master_file) {FactoryGirl.create(:master_file)} it "should redirect you to the media object page with the correct section" do get :show, id: master_file.pid, t:'10' - response.should redirect_to("#{pid_section_media_object_path(master_file.mediaobject.pid, master_file.pid)}?t=10") + expect(response).to redirect_to("#{pid_section_media_object_path(master_file.mediaobject.pid, master_file.pid)}?t=10") end end @@ -238,5 +255,34 @@ class << @file expect(flash[:notice]).to be_nil end end + describe "#attach_captions" do + let!(:media_object) {FactoryGirl.create(:media_object_with_master_file)} + let!(:content_provider) {login_user media_object.collection.managers.first} + let!(:master_file) {media_object.parts.first} + + before(:each) do + login_user media_object.collection.managers.first + end + + it "should populate captions datastream with text" do + # populate the captions datastream with an uploaded vtt file + file = fixture_file_upload('/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt', 'text/vtt') + post 'attach_captions', master_file: {captions: file}, id: master_file.id + master_file.reload + expect(master_file.captions.has_content?).to be_truthy + expect(master_file.captions.label).to eq('sheephead_mountain.mov.vtt') + expect(master_file.captions.mimeType).to eq('text/vtt') + expect(flash[:errors]).to be_nil + expect(flash[:notice]).to be_nil + end + it "should remove contents of captions datastream" do + # remove the contents of the datastream + post 'attach_captions', id: master_file.id + master_file.reload + expect(master_file.captions.empty?).to be true + expect(flash[:errors]).to be_nil + expect(flash[:notice]).to be_nil + end + end end diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 3c25beb734..199a9be065 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -17,24 +17,311 @@ describe MediaObjectsController, type: :controller do render_views - describe "#new" do - let!(:collection) { FactoryGirl.create(:collection) } - before(:each) do - request.env["HTTP_REFERER"] = '/' + before(:each) do + request.env["HTTP_REFERER"] = '/' + end + + describe 'security' do + let(:media_object) { FactoryGirl.create(:media_object) } + let(:collection) { FactoryGirl.create(:collection) } + describe 'ingest api' do + it "all routes should return 401 when no token is present" do + expect(get :index, format: 'json').to have_http_status(401) + expect(get :show, id: media_object.id, format: 'json').to have_http_status(401) + expect(post :create, format: 'json').to have_http_status(401) + expect(put :update, id: media_object.id, format: 'json').to have_http_status(401) + end + it "all routes should return 403 when a bad token in present" do + request.headers['Avalon-Api-Key'] = 'badtoken' + expect(get :index, format: 'json').to have_http_status(403) + expect(get :show, id: media_object.id, format: 'json').to have_http_status(403) + expect(post :create, format: 'json').to have_http_status(403) + expect(put :update, id: media_object.id, format: 'json').to have_http_status(403) + end end + describe 'normal auth' do + context 'with unauthenticated user' do + #New is isolated here due to issues caused by the controller instance not being regenerated + it "should redirect to sign in" do + expect(get :new).to redirect_to(new_user_session_path) + end + it "all routes should redirect to sign in" do + expect(get :show, id: media_object.id).to redirect_to(new_user_session_path) + expect(get :show_progress, id: media_object.id, format: 'json').to have_http_status(401) + expect(get :edit, id: media_object.id).to redirect_to(new_user_session_path) + expect(get :confirm_remove, id: media_object.id).to redirect_to(new_user_session_path) + expect(post :create).to redirect_to(new_user_session_path) + expect(put :update, id: media_object.id).to redirect_to(new_user_session_path) + expect(put :update_status, id: media_object.id).to redirect_to(new_user_session_path) + expect(get :tree, id: media_object.id).to redirect_to(new_user_session_path) + expect(get :deliver_content, id: media_object.id, datastream: 'descMetadata').to redirect_to(new_user_session_path) + expect(delete :destroy, id: media_object.id).to redirect_to(new_user_session_path) + end + end + context 'with end-user' do + before do + login_as :user + end + #New is isolated here due to issues caused by the controller instance not being regenerated + it "should redirect to /" do + expect(get :new).to redirect_to(root_path) + end + it "all routes should redirect to /" do + expect(get :show, id: media_object.id).to redirect_to(root_path) + expect(get :show_progress, id: media_object.id, format: 'json').to redirect_to(root_path) + expect(get :edit, id: media_object.id).to redirect_to(root_path) + expect(get :confirm_remove, id: media_object.id).to redirect_to(root_path) + expect(post :create).to redirect_to(root_path) + expect(put :update, id: media_object.id).to redirect_to(root_path) + expect(put :update_status, id: media_object.id).to redirect_to(root_path) + expect(get :tree, id: media_object.id).to redirect_to(root_path) + expect(get :deliver_content, id: media_object.id, datastream: 'descMetadata').to redirect_to(root_path) + expect(delete :destroy, id: media_object.id).to redirect_to(root_path) + end + end + end + end - it "should redirect to sign in page with a notice when unauthenticated" do - expect { get 'new', collection_id: collection.pid }.not_to change { MediaObject.count } - flash[:notice].should_not be_nil - response.should redirect_to(new_user_session_path) + context "JSON API methods" do + let!(:collection) { FactoryGirl.create(:collection) } + let!(:testdir) {'spec/fixtures/'} + let!(:filename) {'videoshort.high.mp4'} + let!(:absolute_location) {Rails.root.join(File.join(testdir, filename)).to_s} + let!(:structure) {File.read(File.join(testdir, 'structure.xml'))} + let!(:captions) {File.read(File.join(testdir, 'dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt'))} + let!(:bib_id) { '7763100' } + let!(:sru_url) { "http://zgate.example.edu:9000/db?version=1.1&operation=searchRetrieve&maximumRecords=1&recordSchema=marcxml&query=rec.id=#{bib_id}" } + let!(:sru_response) { File.read(File.expand_path("../../fixtures/#{bib_id}.xml",__FILE__)) } + let!(:masterfile) {{ + file_location: absolute_location, + label: "Part 1", + files: [{label: 'quality-high', + id: 'track-1', + url: absolute_location, + duration: "6315", + mime_type: "video/mp4", + audio_bitrate: "127716.0", + audio_codec: "AAC", + video_bitrate: "1000000.0", + video_codec: "AVC", + width: "640", + height: "480" }, + {label: 'quality-medium', + id: 'track-2', + url: absolute_location, + duration: "6315", + mime_type: "video/mp4", + audio_bitrate: "127716.0", + audio_codec: "AAC", + video_bitrate: "1000000.0", + video_codec: "AVC", + width: "640", + height: "480" } + ], + file_location: absolute_location, + file_checksum: "7ae24368ccb7a6c6422a14ff73f33c9a", + file_size: "199160", + duration: "6315", + display_aspect_ratio: "1.7777777777777777", + original_frame_size: "640x480", + file_format: "Moving image", + poster_offset: "0:02", + thumbnail_offset: "0:02", + date_digitized: "2015-12-31", + workflow_name: "avalon", + percent_complete: "100.0", + percent_succeeded: "100.0", + percent_failed: "0", + status_code: "COMPLETED", + other_identifier: '40000000045312', + structure: structure, + captions: captions, + captions_type: 'text/vtt' + }} + let!(:descMetadata_fields) {[ + :title, + :alternative_title, + :translated_title, + :uniform_title, + :statement_of_responsibility, + :creator, + :date_created, + :date_issued, + :copyright_date, + :abstract, + :note, + :format, + :resource_type, + :contributor, + :publisher, + :genre, + :subject, + :related_item_url, + :geographic_subject, + :temporal_subject, + :topical_subject, + :bibliographic_id, + :language, + :terms_of_use, + :table_of_contents, + :physical_description, + :other_identifier + ]} + describe "#create" do + context 'using api' do + before do + request.headers['Avalon-Api-Key'] = 'secret_token' + end + it "should respond with 422 if collection not found" do + post 'create', format: 'json', collection_id: "avalon:doesnt_exist" + expect(response.status).to eq(422) + expect(JSON.parse(response.body)).to include('errors') + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + it "should create a new mediaobject" do + media_object = FactoryGirl.create(:multiple_entries) + fields = media_object.attributes.select {|k,v| descMetadata_fields.include? k.to_sym } + post 'create', format: 'json', fields: fields, files: [masterfile], collection_id: collection.pid + expect(response.status).to eq(200) + new_media_object = MediaObject.find(JSON.parse(response.body)['id']) + expect(new_media_object.title).to eq media_object.title + expect(new_media_object.creator).to eq media_object.creator + expect(new_media_object.date_issued).to eq media_object.date_issued + expect(new_media_object.parts_with_order).to eq new_media_object.parts + expect(new_media_object.duration).to eq '6315' + expect(new_media_object.format).to eq 'video/mp4' + expect(new_media_object.avalon_resource_type).to eq ['moving image'] + expect(new_media_object.parts.first.date_digitized).to eq('2015-12-31T00:00:00Z') + expect(new_media_object.parts.first.DC.identifier).to include('40000000045312') + expect(new_media_object.parts.first.structuralMetadata.has_content?).to be_truthy + expect(new_media_object.parts.first.captions.has_content?).to be_truthy + expect(new_media_object.parts.first.captions.label).to eq('ingest.api') + expect(new_media_object.parts.first.captions.mimeType).to eq('text/vtt') + expect(new_media_object.parts.first.derivatives.count).to eq(2) + expect(new_media_object.parts.first.derivatives.first.location_url).to eq(absolute_location) + expect(new_media_object.workflow.last_completed_step).to eq([HYDRANT_STEPS.last.step]) + end + it "should create a new published mediaobject" do + media_object = FactoryGirl.create(:multiple_entries) + fields = media_object.attributes.select {|k,v| descMetadata_fields.include? k.to_sym } + post 'create', format: 'json', fields: fields, files: [masterfile], collection_id: collection.pid, publish: true + expect(response.status).to eq(200) + new_media_object = MediaObject.find(JSON.parse(response.body)['id']) + expect(new_media_object.published?).to be_truthy + expect(new_media_object.workflow.last_completed_step).to eq([HYDRANT_STEPS.last.step]) + end + it "should create a new mediaobject with successful bib import" do + Avalon::Configuration['bib_retriever'] = { 'protocol' => 'sru', 'url' => 'http://zgate.example.edu:9000/db' } + FakeWeb.register_uri :get, sru_url, body: sru_response + fields = { bibliographic_id: bib_id } + post 'create', format: 'json', import_bib_record: true, fields: fields, files: [masterfile], collection_id: collection.pid + expect(response.status).to eq(200) + new_media_object = MediaObject.find(JSON.parse(response.body)['id']) + expect(new_media_object.bibliographic_id).to eq(['local', bib_id]) + expect(new_media_object.title).to eq('245 A : B F G K N P S') + end + it "should create a new mediaobject with supplied fields when bib import fails" do + Avalon::Configuration['bib_retriever'] = { 'protocol' => 'sru', 'url' => 'http://zgate.example.edu:9000/db' } + FakeWeb.register_uri :get, sru_url, body: nil + media_object = FactoryGirl.create(:media_object) + fields = media_object.attributes.select {|k,v| descMetadata_fields.include? k.to_sym } + fields = fields.merge({ bibliographic_id: bib_id }) + post 'create', format: 'json', import_bib_record: true, fields: fields, files: [masterfile], collection_id: collection.pid + expect(response.status).to eq(200) + new_media_object = MediaObject.find(JSON.parse(response.body)['id']) + expect(new_media_object.bibliographic_id).to eq(['local', bib_id]) + expect(new_media_object.title).to eq media_object.title + expect(new_media_object.creator).to eq [] #creator no longer required, so supplied value won't be used + expect(new_media_object.date_issued).to eq media_object.date_issued + end + it "should create a new mediaobject, removing invalid data for non-required fields" do + media_object = FactoryGirl.create(:multiple_entries) + fields = media_object.attributes.select {|k,v| descMetadata_fields.include? k.to_sym } + fields[:language] = ['???'] + fields[:related_item_url] = ['???'] + fields[:note] = ['note'] + fields[:note_type] = ['???'] + fields[:date_created] = '???' + fields[:copyright_date] = '???' + post 'create', format: 'json', fields: fields, files: [masterfile], collection_id: collection.pid + expect(response.status).to eq(200) + new_media_object = MediaObject.find(JSON.parse(response.body)['id']) + expect(new_media_object.title).to eq media_object.title + expect(new_media_object.language).to eq [] + expect(new_media_object.related_item_url).to eq [] + expect(new_media_object.note).to eq nil + expect(new_media_object.date_created).to eq nil + expect(new_media_object.copyright_date).to eq nil + end + it "should merge supplied other identifiers after bib import" do + Avalon::Configuration['bib_retriever'] = { 'protocol' => 'sru', 'url' => 'http://zgate.example.edu:9000/db' } + FakeWeb.register_uri :get, sru_url, body: sru_response + fields = { bibliographic_id: bib_id, other_identifier_type: ['other'], other_identifier: ['12345'] } + post 'create', format: 'json', import_bib_record: true, fields: fields, files: [masterfile], collection_id: collection.pid + expect(response.status).to eq(200) + new_media_object = MediaObject.find(JSON.parse(response.body)['id']) + expect(new_media_object.bibliographic_id).to eq(['local', bib_id]) + expect(new_media_object.other_identifier.find {|id_pair| id_pair[0] == 'other'}).not_to be nil + expect(new_media_object.other_identifier.find {|id_pair| id_pair[0] == 'other'}[1]).to eq('12345') + end + end end - - it "should redirect to home page with a notice when authenticated but unauthorized" do - login_as :user - expect { get 'new', collection_id: collection.pid }.not_to change { MediaObject.count } - flash[:notice].should_not be_nil - response.should redirect_to(root_path) + describe "#update" do + context 'using api' do + before do + request.headers['Avalon-Api-Key'] = 'secret_token' + end + let!(:media_object) { FactoryGirl.create(:media_object_with_master_file) } + it "should route json format to #json_update" do + assert_routing({ path: 'media_objects/avalon:1.json', method: :put }, + { controller: 'media_objects', action: 'json_update', id: 'avalon:1', format: 'json' }) + end + it "should route unspecified format to #update" do + assert_routing({ path: 'media_objects/avalon:1', method: :put }, + { controller: 'media_objects', action: 'update', id: 'avalon:1', format: 'html' }) + end + it "should update a mediaobject's metadata" do + old_title = media_object.title + put 'json_update', format: 'json', id: media_object.pid, fields: {title: old_title+'new'}, collection_id: media_object.collection_id + expect(JSON.parse(response.body)['id'].class).to eq String + expect(JSON.parse(response.body)).not_to include('errors') + media_object.reload + expect(media_object.title).to eq old_title+'new' + end + it "should add a masterfile to a mediaobject" do + put 'json_update', format: 'json', id: media_object.pid, files: [masterfile], collection_id: media_object.collection_id + expect(JSON.parse(response.body)['id'].class).to eq String + expect(JSON.parse(response.body)).not_to include('errors') + media_object.reload + expect(media_object.parts.count).to eq 2 + end + it "should delete existing masterfiles and add a new masterfile to a mediaobject" do + put 'json_update', format: 'json', id: media_object.pid, files: [masterfile], collection_id: media_object.collection_id, replace_masterfiles: true + expect(JSON.parse(response.body)['id'].class).to eq String + expect(JSON.parse(response.body)).not_to include('errors') + media_object.reload + expect(media_object.parts.count).to eq 1 + end + it "should return 404 if media object doesn't exist" do + allow_any_instance_of(MediaObject).to receive(:save).and_return false + put 'json_update', format: 'json', id: 'avalon:doesnt_exist', fields: {}, collection_id: media_object.collection_id + expect(response.status).to eq(404) + end + it "should return 422 if media object update failed" do + allow_any_instance_of(MediaObject).to receive(:save).and_return false + put 'json_update', format: 'json', id: media_object.pid, fields: {}, collection_id: media_object.collection_id + expect(response.status).to eq(422) + expect(JSON.parse(response.body)).to include('errors') + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end end + end + + describe "#new" do + let!(:collection) { FactoryGirl.create(:collection) } it "should not let manager of other collections create an item in this collection" do skip @@ -45,14 +332,14 @@ login_user collection.managers.first expect { get 'new', collection_id: collection.pid }.to change { MediaObject.count } pid = MediaObject.all.last.pid - response.should redirect_to(edit_media_object_path(id: pid)) + expect(response).to redirect_to(edit_media_object_path(id: pid)) end it "should copy default permissions from its owning collection" do login_user collection.depositors.first - get 'new', collection_id: collection.pid - + get 'new', collection_id: collection.pid + #MediaObject.all.last.edit_users.should include(collection.managers) #MediaObject.all.last.edit_users.should include(collection.depositors) end @@ -63,28 +350,14 @@ describe "#edit" do let!(:media_object) { FactoryGirl.create(:media_object) } - it "should redirect to sign in page with a notice when unauthenticated" do - get 'edit', id: media_object.pid - flash[:notice].should_not be_nil - response.should redirect_to(new_user_session_path) - end - - it "should redirect to show page with a notice when authenticated but unauthorized" do - login_as :user - - get 'edit', id: media_object.pid - flash[:notice].should_not be_nil - response.should redirect_to(media_object_path(media_object.pid) ) - end - it "should redirect to first workflow step if authorized to edit" do login_user media_object.collection.managers.first get 'edit', id: media_object.pid - response.should be_success - response.should render_template "_#{HYDRANT_STEPS.first.template}" + expect(response).to be_success + expect(response).to render_template "_#{HYDRANT_STEPS.first.template}" end - + it "should not default to the Access Control page" do skip "[VOV-1165] Wait for product owner feedback on which step to default to" end @@ -95,11 +368,11 @@ it "should be able to retrieve an existing record from Fedora" do media_object.workflow.last_completed_step = 'resource-description' media_object.save - + # Set the task so that it can get to the resource-description step login_user media_object.collection.managers.first get :edit, {id: media_object.pid, step: 'resource-description'} - response.response_code.should == 200 + expect(response.response_code).to eq(200) end end @@ -107,16 +380,16 @@ before(:each) { login_user mo.collection.managers.first } context "Persisting Permalinks on unpublished mediaobject" do subject(:mo) { media_object } - it "should persist new permalink on unpublished media_object" do - expect { put 'update', id: mo.pid, step: 'resource-description', - media_object: { permalink: 'newpermalink', title: 'newtitle', + it "should persist new permalink on unpublished media_object" do + expect { put 'update', id: mo.pid, step: 'resource-description', + media_object: { permalink: 'newpermalink', title: 'newtitle', creator: 'newcreator', date_issued: 'newdateissued' }} - .to change { MediaObject.find(mo.pid).permalink } + .to change { MediaObject.find(mo.pid).permalink } .to('newpermalink') end - it "should persist new permalink on unpublished media_object part" do + it "should persist new permalink on unpublished media_object part" do part1 = FactoryGirl.create(:master_file, mediaobject: mo) - expect {put 'update', id: mo.pid, step: 'file-upload', + expect {put 'update', id: mo.pid, step: 'file-upload', parts: { part1.pid => { permalink: 'newpermalinkpart' }}} .to change { MasterFile.find(part1.pid).permalink } .to('newpermalinkpart') @@ -125,21 +398,67 @@ context "Persisting Permalinks on published mediaobject" do subject(:mo) { FactoryGirl.create(:published_media_object, permalink: 'oldpermalink') } it "should persist updated permalink on published media_object" do - expect { put 'update', id: mo.pid, step: 'resource-description', - media_object: { permalink: 'newpermalink', title: mo.title, + expect { put 'update', id: mo.pid, step: 'resource-description', + media_object: { permalink: 'newpermalink', title: mo.title, creator: mo.creator, date_issued: mo.date_issued }} .to change { MediaObject.find(mo.pid).permalink } .to('newpermalink') end it "should persist updated permalink on published media_object part" do part1 = FactoryGirl.create(:master_file, permalink: 'oldpermalinkpart1', mediaobject: mo) - expect { put 'update', id: mo.pid, step: 'file-upload', + expect { put 'update', id: mo.pid, step: 'file-upload', parts: { part1.pid => { permalink: 'newpermalinkpart' }}} .to change { MasterFile.find(part1.pid).permalink } .to('newpermalinkpart') end end end + + context "Hidden Items" do + subject(:mo) { FactoryGirl.create(:media_object_with_completed_workflow, hidden: true) } + let!(:user) { Faker::Internet.email } + before(:each) { login_user mo.collection.managers.first } + + it "should retain the hidden status of an object when other access control settings change" do + expect { put 'update', id: mo.pid, step: 'access-control', donot_advance: 'true', + add_user: user, add_user_display: user, submit_add_user: 'Add' } + .not_to change { MediaObject.find(mo.pid).hidden? } + end + end + end + + describe "#index" do + let!(:media_object) { FactoryGirl.create(:published_media_object, visibility: 'public') } + subject(:json) { JSON.parse(response.body) } + + it "should return list of media_objects" do + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'index', format:'json' + expect(json.count).to eq(1) + expect(json.first['id']).to eq(media_object.pid) + expect(json.first['title']).to eq(media_object.title) + expect(json.first['collection']).to eq(media_object.collection.name) + expect(json.first['main_contributors']).to eq(media_object.creator) + expect(json.first['publication_date']).to eq(media_object.date_created) + expect(json.first['published_by']).to eq(media_object.avalon_publisher) + expect(json.first['published']).to eq(media_object.published?) + expect(json.first['summary']).to eq(media_object.abstract) + end + end + + describe 'pagination' do + let(:collection) { FactoryGirl.create(:collection) } + subject(:json) { JSON.parse(response.body) } + before do + 5.times { FactoryGirl.create(:published_media_object, visibility: 'public', collection: collection) } + request.headers['Avalon-Api-Key'] = 'secret_token' + get 'index', format:'json', per_page: '2' + end + it 'should paginate' do + expect(json.count).to eq(2) + expect(response.headers['Per-Page']).to eq('2') + expect(response.headers['Total']).to eq('5') + end end describe "#show" do @@ -148,19 +467,19 @@ context "Known items should be retrievable" do it "should be accesible by its PID" do get :show, id: media_object.pid - response.response_code.should == 200 + expect(response.response_code).to eq(200) end it "should return an error if the PID does not exist" do expect(MediaObject).to receive(:find).with('no-such-object') { raise ActiveFedora::ObjectNotFoundError } get :show, id: 'no-such-object' - response.response_code.should == 404 + expect(response.response_code).to eq(404) end it "should be available to a manager when unpublished" do login_user media_object.collection.managers.first get 'show', id: media_object.pid - response.should_not redirect_to new_user_session_path + expect(response).not_to redirect_to new_user_session_path end it "should provide a JSON stream description to the client" do @@ -171,13 +490,32 @@ mopid = media_object.pid media_object = MediaObject.find(mopid) - media_object.parts.collect { |part| - get 'show', id: media_object.pid, format: 'json', content: part.pid + media_object.parts.collect { |part| + get 'show', id: media_object.pid, format: 'js', content: part.pid json_obj = JSON.parse(response.body) - json_obj['is_video'].should == part.is_video? + expect(json_obj['is_video']).to eq(part.is_video?) } end end + context "Test lease access control" do + let!(:media_object) { FactoryGirl.create(:published_media_object, visibility: 'private') } + let!(:user) { FactoryGirl.create(:user) } + before :each do + login_user user.username + end + it "should not be available to a user on an inactive lease" do + media_object.governing_policies+=[Lease.create(begin_time: Date.today-2.day, end_time: Date.yesterday, read_users: [user.username])] + media_object.save! + get 'show', id: media_object.pid + expect(response.response_code).not_to eq(200) + end + it "should be available to a user on an active lease" do + media_object.governing_policies+=[Lease.create(begin_time: Date.yesterday, end_time: Date.tomorrow, read_users: [user.username])] + media_object.save! + get 'show', id: media_object.pid + expect(response.response_code).to eq(200) + end + end context "Conditional Share partials should be rendered" do context "Normal login" do @@ -212,18 +550,18 @@ end context "LTI login" do it "administrators/managers/editors: should include lti, embed, and share" do - user = login_lti 'administrator' + login_lti 'administrator' lti_group = @controller.user_session[:virtual_groups].first - mo = FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) + FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) get :show, id: media_object.pid expect(response).to render_template(:_share_resource) expect(response).to render_template(:_embed_resource) expect(response).to render_template(:_lti_url) end it "others: should include only lti" do - user = login_lti 'student' + login_lti 'student' lti_group = @controller.user_session[:virtual_groups].first - mo = FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) + FactoryGirl.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) get :show, id: media_object.pid expect(response).to_not render_template(:_share_resource) expect(response).to_not render_template(:_embed_resource) @@ -232,7 +570,7 @@ end context "No share tabs rendered" do it "should not render Share button" do - @controller.stub(:evaluate_if_unless_configuration).and_return false + allow(@controller).to receive(:evaluate_if_unless_configuration).and_return false expect(response).to_not render_template(:_share) end end @@ -255,16 +593,16 @@ context "correctly handle unfound streams/sections" do subject(:mo){FactoryGirl.create(:media_object_with_master_file)} - before do + before do mo.save(validate: false) - login_user mo.collection.managers.first + login_user mo.collection.managers.first end it "redirects to first stream when currentStream is nil" do - expect(get 'show', id: mo.pid, content: 'foo').to redirect_to(media_object_path(id: mo.pid)) + expect(get 'show', id: mo.pid, content: 'foo').to redirect_to(media_object_path(id: mo.pid)) end it "responds with 404 when non-existant section is requested" do get 'show', id: mo.pid, part: 100 - expect(response.code).to eq('404') + expect(response.code).to eq('404') end end @@ -274,12 +612,12 @@ context 'Before sign in' do it 'persists the current url on the session' do get 'show', id: media_object.pid - session[:previous_url].should eql media_object_path(media_object) + expect(session[:previous_url]).to eql media_object_path(media_object) end end context 'After sign in' do - before do + before do @user = FactoryGirl.create(:user) @media_object = FactoryGirl.create(:media_object, visibility: 'private', read_users: [@user.username] ) end @@ -289,31 +627,61 @@ end end end - + context "Items should not be available to unauthorized users" do it "should redirect to sign in when not logged in and item is unpublished" do media_object.publish!(nil) - media_object.should_not be_published + expect(media_object).not_to be_published get 'show', id: media_object.pid - response.should redirect_to new_user_session_path + expect(response).to redirect_to new_user_session_path end it "should redirect to home page when logged in and item is unpublished" do media_object.publish!(nil) - media_object.should_not be_published + expect(media_object).not_to be_published login_as :user get 'show', id: media_object.pid - response.should redirect_to root_path + expect(response).to redirect_to root_path end end + + context "with json format" do + subject(:json) { JSON.parse(response.body) } + let!(:media_object) { FactoryGirl.create(:media_object) } + + before do + request.headers['Avalon-Api-Key'] = 'secret_token' + end + + it "should return json for specific media_object" do + get 'show', id: media_object.pid, format:'json' + expect(json['id']).to eq(media_object.pid) + expect(json['title']).to eq(media_object.title) + expect(json['collection']).to eq(media_object.collection.name) + expect(json['main_contributors']).to eq(media_object.creator) + expect(json['publication_date']).to eq(media_object.date_created) + expect(json['published_by']).to eq(media_object.avalon_publisher) + expect(json['published']).to eq(media_object.published?) + expect(json['summary']).to eq(media_object.abstract) + end + + it "should return 404 if requested media_object not present" do + login_as(:administrator) + get 'show', id: 'avalon:doesnt_exist', format: 'json' + expect(response.status).to eq(404) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end + end - + describe "#destroy" do let!(:collection) { FactoryGirl.create(:collection) } - before(:each) do + before(:each) do login_user collection.managers.first end - + it "should remove the MediaObject and MasterFiles from the system" do media_object = FactoryGirl.create(:media_object_with_master_file, collection: collection) delete :destroy, id: media_object.pid @@ -327,12 +695,6 @@ expect(response.code).to eq '404' end - it "should fail if user is not authorized" do - media_object = FactoryGirl.create(:media_object) - expect(delete :destroy, id: media_object.id).to redirect_to root_path - expect(flash[:notice]).to include("permission denied") - end - it "should remove multiple items" do media_objects = [] 3.times { media_objects << FactoryGirl.create(:media_object, collection: collection) } @@ -343,6 +705,9 @@ end describe "#update_status" do + before { Delayed::Worker.delay_jobs = false } + after { Delayed::Worker.delay_jobs = true } + let!(:collection) { FactoryGirl.create(:collection) } before(:each) do login_user collection.managers.first @@ -354,7 +719,7 @@ Permalink.on_generate { |obj| "http://example.edu/permalink" } end it 'publishes media object' do - media_object = FactoryGirl.create(:media_object, collection: collection) + media_object = FactoryGirl.create(:media_object, collection: collection) get 'update_status', id: media_object.pid, status: 'publish' media_object.reload expect(media_object).to be_published @@ -362,25 +727,19 @@ end it "should fail when id doesn't exist" do - get 'update_status', id: 'avalon:this-pid-is-fake', status: 'publish' - expect(response.code).to eq '404' - end - - it "should fail if user is not authorized" do - media_object = FactoryGirl.create(:media_object) - expect(get 'update_status', id: media_object.pid, status: 'publish').to be_redirect - expect(flash[:notice]).to include("permission denied") + get 'update_status', id: 'avalon:this-pid-is-fake', status: 'publish' + expect(response.code).to eq '404' end it "should publish multiple items" do - media_objects = [] - 3.times { media_objects << FactoryGirl.create(:media_object, collection: collection) } + media_objects = [] + 3.times { media_objects << FactoryGirl.create(:media_object, collection: collection) } get 'update_status', id: media_objects.map(&:id), status: 'publish' - expect(flash[:notice]).to include('3 media objects') + expect(flash[:notice]).to include('3 media objects') media_objects.each do |mo| mo.reload - expect(mo).to be_published - expect(mo.permalink).to be_present + expect(mo).to be_published + expect(mo.permalink).to be_present end end end @@ -394,44 +753,54 @@ end it "should fail when id doesn't exist" do - get 'update_status', id: 'avalon:this-pid-is-fake', status: 'unpublish' - expect(response.code).to eq '404' - end - - it "should fail if user is not authorized" do - media_object = FactoryGirl.create(:media_object) - expect(get 'update_status', id: media_object.pid, status: 'unpublish').to be_redirect - expect(flash[:notice]).to include("permission denied") + get 'update_status', id: 'avalon:this-pid-is-fake', status: 'unpublish' + expect(response.code).to eq '404' end it "should unpublish multiple items" do - media_objects = [] - 3.times { media_objects << FactoryGirl.create(:published_media_object, collection: collection) } + media_objects = [] + 3.times { media_objects << FactoryGirl.create(:published_media_object, collection: collection) } get 'update_status', id: media_objects.map(&:id), status: 'unpublish' - expect(flash[:notice]).to include('3 media objects') + expect(flash[:notice]).to include('3 media objects') media_objects.each do |mo| mo.reload - expect(mo).not_to be_published + expect(mo).not_to be_published end end end end + describe "#save" do + it 'removes bookmarks that are no longer viewable' do + media_object = FactoryGirl.create(:published_media_object) + user = FactoryGirl.create(:public) + bookmark = Bookmark.create(document_id: media_object.pid, user_id: user.id) + login_user media_object.collection.managers.first + request.env["HTTP_REFERER"] = '/' + expect { + get 'update_status', id: media_object.pid, status: 'unpublish' + }.to change { Bookmark.exists? bookmark }.from( true ).to( false ) + end + end + describe "#update" do it 'updates the order' do media_object = FactoryGirl.create(:media_object) - media_object.parts << FactoryGirl.create(:master_file) - media_object.parts << FactoryGirl.create(:master_file) + 2.times do + mf = FactoryGirl.create(:master_file) + mf.mediaobject = media_object + mf.save + end master_file_pids = media_object.parts.map(&:id) media_object.section_pid = master_file_pids media_object.save( validate: false ) login_user media_object.collection.managers.first - + put 'update', :id => media_object.pid, :masterfile_ids => master_file_pids.reverse, :step => 'structure' media_object.reload - media_object.section_pid.should eq master_file_pids.reverse + expect(media_object.section_pid).to eq master_file_pids.reverse end it 'sets the MIME type' do media_object = FactoryGirl.create(:media_object) @@ -442,5 +811,123 @@ media_object.reload expect(media_object.descMetadata.media_type).to eq(["video/mp4"]) end + + context 'large objects' do + before(:each) do + Permalink.on_generate { |obj| sleep(0.5); "http://example.edu/permalink" } + end + + let!(:media_object) do + mo = FactoryGirl.create(:published_media_object) + 10.times { FactoryGirl.create(:master_file_with_derivative, mediaobject: mo) } + mo + end + + it "should update all the labels" do + login_user media_object.collection.managers.first + part_params = {} + media_object.parts_with_order.each_with_index { |mf,i| part_params[mf.pid] = { pid: mf.pid, label: "Part #{i}", permalink: '', poster_offset: '00:00:00.000' } } + params = { id: media_object.pid, parts: part_params, save: 'Save', step: 'file-upload', donot_advance: 'true' } + patch 'update', params + media_object.reload + media_object.parts_with_order.each_with_index do |mf,i| + expect(mf.label).to eq "Part #{i}" + end + end + end + + context "access controls" do + let!(:media_object) { FactoryGirl.create(:media_object) } + let!(:user) { Faker::Internet.email } + let!(:group) { Faker::Lorem.word } + let!(:classname) { Faker::Lorem.word } + let!(:ipaddr) { Faker::Internet.ip_v4_address } + before(:each) { login_user media_object.collection.managers.first } + + context "grant and revoke special read access" do + it "grants and revokes special read access to users" do + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_user: user, submit_add_user: 'Add' }.to change { media_object.reload.read_users }.from([]).to([user]) + expect {put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', remove_user: user, submit_remove_user: 'Remove' }.to change { media_object.reload.read_users }.from([user]).to([]) + end + it "grants and revokes special read access to groups" do + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_group: group, submit_add_group: 'Add' }.to change { media_object.reload.read_groups }.from([]).to([group]) + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', remove_group: group, submit_remove_group: 'Remove' }.to change { media_object.reload.read_groups }.from([group]).to([]) + end + it "grants and revokes special read access to external groups" do + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_class: classname, submit_add_class: 'Add' }.to change { media_object.reload.read_groups }.from([]).to([classname]) + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', remove_class: classname, submit_remove_class: 'Remove' }.to change { media_object.reload.read_groups }.from([classname]).to([]) + end + it "grants and revokes special read access to ips" do + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_ipaddress: ipaddr, submit_add_ipaddress: 'Add' }.to change { media_object.reload.read_groups }.from([]).to([ipaddr]) + expect { put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', remove_ipaddress: ipaddr, submit_remove_ipaddress: 'Remove' }.to change { media_object.reload.read_groups }.from([ipaddr]).to([]) + end + end + + context "grant and revoke time-based special read access" do + it "should grant and revoke time-based access for users" do + expect { + put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_user: user, submit_add_user: 'Add', add_user_begin: Date.yesterday, add_user_end: Date.tomorrow + media_object.reload + }.to change{media_object.governing_policies.count}.by(1) + expect(media_object.governing_policies.last.class).to eq(Lease) + lease_pid = media_object.reload.governing_policies.last.pid + expect { + put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', remove_lease: lease_pid + media_object.reload + }.to change{media_object.governing_policies.count}.by(-1) + end + end + + context "must validate lease date ranges" do + it "should accept valid date range for lease" do + expect { + put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_user: user, submit_add_user: 'Add', add_user_begin: Date.today, add_user_end: Date.tomorrow + media_object.reload + }.to change{media_object.governing_policies.count}.by(1) + end + it "should reject reverse date range for lease" do + expect { + put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_user: user, submit_add_user: 'Add', add_user_begin: Date.tomorrow, add_user_end: Date.today + media_object.reload + }.not_to change{media_object.governing_policies.count} + end + it "should accept missing begin date and set it to today" do + expect { + put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_user: user, submit_add_user: 'Add', add_user_begin: '', add_user_end: Date.tomorrow + media_object.reload + }.to change{media_object.governing_policies.count}.by(1) + expect(media_object.governing_policies.last.begin_time).to eq(Date.today) + end + it "should reject missing end date" do + expect { + put :update, id: media_object.id, step: 'access-control', donot_advance: 'true', add_user: user, submit_add_user: 'Add', add_user_begin: Date.tomorrow, add_user_end: '' + media_object.reload + }.not_to change{media_object.governing_policies.count} + end + end + end + end + + describe "#show_progress" do + it "should return information about the processing state of the media object's parts" do + media_object = FactoryGirl.create(:media_object_with_master_file) + login_as 'administrator' + get :show_progress, id: media_object.id, format: 'json' + expect(JSON.parse(response.body)["overall"]).to_not be_empty + end + it "should return something even if the media object has no parts" do + media_object = FactoryGirl.create(:media_object) + login_as 'administrator' + get :show_progress, id: media_object.id, format: 'json' + expect(JSON.parse(response.body)["overall"]).to_not be_empty + end + end + + describe "#set_session_quality" do + it "should set the posted quality in the session" do + login_as 'administrator' + post :set_session_quality, quality: 'crazy_high' + expect(@request.session[:quality]).to eq('crazy_high') + end end end diff --git a/spec/controllers/object_controller_spec.rb b/spec/controllers/object_controller_spec.rb index ac7baa18ea..bbda565a54 100644 --- a/spec/controllers/object_controller_spec.rb +++ b/spec/controllers/object_controller_spec.rb @@ -18,25 +18,25 @@ describe "#show" do it "should redirect you to root if no object is found" do get :show, id: 'avalon:bad-pid' - response.should redirect_to root_path + expect(response).to redirect_to root_path end it "should redirect you to the object show page" do obj = FactoryGirl.create(:media_object) get :show, id: obj.pid - response.should redirect_to(media_object_path(obj)) + expect(response).to redirect_to(media_object_path(obj)) end it "should pass along request params" do obj = FactoryGirl.create(:media_object) get :show, id: obj.pid, foo: 'bar' - response.should redirect_to(media_object_path(obj, {foo: 'bar'})) + expect(response).to redirect_to(media_object_path(obj, {foo: 'bar'})) end it "should redirect to appended url" do obj = FactoryGirl.create(:media_object) get :show, id: obj.pid, urlappend: 'test' - response.should redirect_to(media_object_path(obj)+"/test") + expect(response).to redirect_to(media_object_path(obj)+"/test") end end diff --git a/spec/controllers/playlist_items_controller_spec.rb b/spec/controllers/playlist_items_controller_spec.rb new file mode 100644 index 0000000000..5bb6e4e269 --- /dev/null +++ b/spec/controllers/playlist_items_controller_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +RSpec.describe PlaylistItemsController, type: :controller do + # This should return the minimal set of attributes required to create a valid + # PlaylistItem. As you add validations to PlaylistItem, be sure to + # adjust the attributes here as well. + let(:valid_attributes) do + { title: Faker::Lorem.word, start_time: "00:00:00", end_time: "00:01:37", master_file_id: master_file.pid } + end + + let(:invalid_attributes) do + { title: "", start_time: 'not-a-time', end_time: 'not-a-time' } + end + + let(:invalid_times) do + { title: Faker::Lorem.word, start_time: 0.0, end_time: 'not-a-time', master_file_id: master_file.pid } + end + + # This should return the minimal set of values that should be in the session + # in order to pass any filters (e.g. authentication) defined in + # PlaylistsController. Be sure to keep this updated too. + let(:valid_session) { {} } + + let(:user) { login_as :user } + let(:playlist) { FactoryGirl.create(:playlist, user: user) } + let(:master_file) { FactoryGirl.create(:master_file, duration: "100000") } + + describe 'security' do + let(:playlist) { FactoryGirl.create(:playlist) } + let(:playlist_item) { FactoryGirl.create(:playlist_item, playlist: playlist) } + context 'with unauthenticated user' do + it "all routes should redirect to sign in" do + expect(post :create, playlist_id: playlist.to_param, playlist_item: valid_attributes).to redirect_to(new_user_session_path) + expect(put :update, playlist_id: playlist.to_param, id: playlist_item.id).to redirect_to(new_user_session_path) + end + end + context 'with end-user' do + before do + login_as :user + end + it "all routes should redirect to /" do + expect(post :create, playlist_id: playlist.to_param, playlist_item: valid_attributes).to have_http_status(:unauthorized) + expect(put :update, playlist_id: playlist.to_param, id: playlist_item.id).to have_http_status(:unauthorized) + end + end + end + + + describe 'POST #create' do + + context 'with valid params' do + it 'creates a new Playlist Item' do + expect do + post :create, { playlist_id: playlist.to_param, playlist_item: valid_attributes }, valid_session + end.to change(PlaylistItem, :count).by(1) + end + + it 'creates a new AvalonAnnotation' do + expect do + post :create, { playlist_id: playlist.to_param, playlist_item: valid_attributes }, valid_session + end.to change(AvalonAnnotation, :count).by(1) + expect(AvalonAnnotation.last.start_time).to eq (0.0) + expect(AvalonAnnotation.last.end_time).to eq (97000.0) + end + + it 'adds the Playlist Item to the playlist' do + expect do + post :create, { playlist_id: playlist.to_param, playlist_item: valid_attributes }, valid_session + end.to change { playlist.reload.items.size }.by(1) + end + + it 'responds with 201 CREATED status code' do + post :create, { playlist_id: playlist.to_param, playlist_item: valid_attributes }, valid_session + expect(response).to have_http_status(:created) + end + + it 'responds with a flash message with link to playlist' do + post :create, { playlist_id: playlist.to_param, playlist_item: valid_attributes }, valid_session + expect(JSON.parse(response.body)['message']).to include('Add to playlist was successful.') + expect(JSON.parse(response.body)['message']).to include(playlist_url(playlist)) + end + end + + context 'with invalid params' do + it 'invalid times respond with a 400 BAD REQUEST' do + post :create, { playlist_id: playlist.to_param, playlist_item: invalid_times }, valid_session + expect(response).to have_http_status(:bad_request) + end + end + end + describe 'PATCH #update' do + let!(:video_master_file) { FactoryGirl.create(:master_file, duration: "200000") } + let!(:annotation) { AvalonAnnotation.create(master_file: video_master_file, title: Faker::Lorem.word, comment: Faker::Lorem.sentence, start_time: 1000, end_time: 2000) } + let!(:playlist_item) { PlaylistItem.create!(playlist_id: playlist.id, annotation_id: annotation.id) } + + context 'with valid params' do + it 'updates Playlist Item' do + expect do + patch :update, { playlist_id: playlist.id, id: playlist_item.id, playlist_item: { title: Faker::Lorem.word, start_time:'00:20', end_time:'1:20' }}, valid_session + end.to change{ playlist_item.reload.title } + end + end + context 'with invalid params' do + it 'fails to update Playlist Item' do + expect do + patch :update, { playlist_id: playlist.id, id: playlist_item.id, playlist_item: { title: Faker::Lorem.word, start_time:'00:20', end_time:'not-a-time' }}, valid_session + end.not_to change{ playlist_item.reload.title } + end + end + end +end diff --git a/spec/controllers/playlists_controller_spec.rb b/spec/controllers/playlists_controller_spec.rb new file mode 100644 index 0000000000..1f82f85f1a --- /dev/null +++ b/spec/controllers/playlists_controller_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to specify the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. +# +# Compared to earlier versions of this generator, there is very limited use of +# stubs and message expectations in this spec. Stubs are only used when there +# is no simpler way to get a handle on the object needed for the example. +# Message expectations are only used when there is no simpler way to specify +# that an instance is receiving a specific message. + +RSpec.describe PlaylistsController, type: :controller do + # This should return the minimal set of attributes required to create a valid + # Playlist. As you add validations to Playlist, be sure to + # adjust the attributes here as well. + let(:valid_attributes) do + { title: Faker::Lorem.word, visibility: Playlist::PUBLIC, user: user } + end + + let(:invalid_attributes) do + { visibility: 'unknown' } + end + + # This should return the minimal set of values that should be in the session + # in order to pass any filters (e.g. authentication) defined in + # PlaylistsController. Be sure to keep this updated too. + let(:valid_session) { {} } + + let(:user) { login_as :user } + + describe 'security' do + let(:playlist) { FactoryGirl.create(:playlist) } + context 'with unauthenticated user' do + #New is isolated here due to issues caused by the controller instance not being regenerated + it "should redirect to sign in" do + expect(get :new).to redirect_to(new_user_session_path) + end + it "all routes should redirect to sign in" do + expect(get :index).to redirect_to(new_user_session_path) + expect(get :edit, id: playlist.id).to redirect_to(new_user_session_path) + expect(post :create).to redirect_to(new_user_session_path) + expect(put :update, id: playlist.id).to redirect_to(new_user_session_path) + expect(put :update_multiple, id: playlist.id).to redirect_to(new_user_session_path) + expect(delete :destroy, id: playlist.id).to redirect_to(new_user_session_path) + end + context 'with a public playlist' do + let(:playlist) { FactoryGirl.create(:playlist, visibility: Playlist::PUBLIC) } + it "should return the playlist view page" do + expect(get :show, id: playlist.id).not_to redirect_to(new_user_session_path) + end + end + context 'with a private playlist' do + it "should NOT return the playlist view page" do + expect(get :show, id: playlist.id).to redirect_to(new_user_session_path) + end + end + end + context 'with end-user' do + before do + login_as :user + end + it "all routes should redirect to /" do + expect(get :edit, id: playlist.id).to redirect_to(root_path) + expect(put :update, id: playlist.id).to redirect_to(root_path) + expect(put :update_multiple, id: playlist.id).to redirect_to(root_path) + expect(delete :destroy, id: playlist.id).to redirect_to(root_path) + end + context 'with a public playlist' do + let(:playlist) { FactoryGirl.create(:playlist, visibility: Playlist::PUBLIC) } + it "should return the playlist view page" do + expect(get :show, id: playlist.id).not_to redirect_to(root_path) + end + end + context 'with a private playlist' do + it "should NOT return the playlist view page" do + expect(get :show, id: playlist.id).to redirect_to(root_path) + end + end + end + end + + describe 'GET #index' do + it 'assigns accessible playlists as @playlists' do + # TODO: test non-accessible playlists not appearing + playlist = Playlist.create! valid_attributes + get :index, {}, valid_session + expect(assigns(:playlists)).to eq([playlist]) + end + end + + describe 'GET #show' do + it 'assigns the requested playlist as @playlist' do + playlist = Playlist.create! valid_attributes + get :show, { id: playlist.to_param }, valid_session + expect(assigns(:playlist)).to eq(playlist) + end + # TODO: write tests for public/private playists + end + + describe 'GET #new' do + before do + login_as :user + end + it 'assigns a new playlist as @playlist' do + get :new, {}, valid_session + expect(assigns(:playlist)).to be_a_new(Playlist) + end + end + + describe 'GET #edit' do + it 'assigns the requested playlist as @playlist' do + playlist = Playlist.create! valid_attributes + get :edit, { id: playlist.to_param }, valid_session + expect(assigns(:playlist)).to eq(playlist) + end + end + + describe 'POST #create' do + context 'with valid params' do + it 'creates a new Playlist' do + expect do + post :create, { playlist: valid_attributes }, valid_session + end.to change(Playlist, :count).by(1) + end + + it 'assigns a newly created playlist as @playlist' do + post :create, { playlist: valid_attributes }, valid_session + expect(assigns(:playlist)).to be_a(Playlist) + expect(assigns(:playlist)).to be_persisted + end + + it 'redirects to the created playlist' do + post :create, { playlist: valid_attributes }, valid_session + expect(response).to redirect_to(Playlist.last) + end + end + + context 'with invalid params' do + before do + login_as :user + end + it 'assigns a newly created but unsaved playlist as @playlist' do + post :create, { playlist: invalid_attributes }, valid_session + expect(assigns(:playlist)).to be_a_new(Playlist) + end + + it "re-renders the 'new' template" do + post :create, { playlist: invalid_attributes }, valid_session + expect(response).to render_template('new') + end + end + end + + describe 'PUT #update' do + context 'with valid params' do + let(:new_attributes) do + { title: Faker::Lorem.word, visibility: Playlist::PUBLIC, comment: Faker::Lorem.sentence } + end + + it 'updates the requested playlist' do + playlist = Playlist.create! valid_attributes + put :update, { id: playlist.to_param, playlist: new_attributes }, valid_session + playlist.reload + expect(playlist.title).to eq new_attributes[:title] + expect(playlist.visibility).to eq new_attributes[:visibility] + expect(playlist.comment).to eq new_attributes[:comment] + end + + it 'assigns the requested playlist as @playlist' do + playlist = Playlist.create! valid_attributes + put :update, { id: playlist.to_param, playlist: valid_attributes }, valid_session + expect(assigns(:playlist)).to eq(playlist) + end + + it 'redirects to edit playlist' do + playlist = Playlist.create! valid_attributes + put :update, { id: playlist.to_param, playlist: valid_attributes }, valid_session + expect(response).to redirect_to(edit_playlist_path(playlist)) + end + end + + context 'with invalid params' do + it 'assigns the playlist as @playlist' do + playlist = Playlist.create! valid_attributes + put :update, { id: playlist.to_param, playlist: invalid_attributes }, valid_session + expect(assigns(:playlist)).to eq(playlist) + end + + it "re-renders the 'edit' template" do + playlist = Playlist.create! valid_attributes + put :update, { id: playlist.to_param, playlist: invalid_attributes }, valid_session + expect(response).to render_template('edit') + end + end + end + + describe 'PUT #update_multiple' do + context 'delete' do + it 'redirects to edit playlist' do + playlist = Playlist.create! valid_attributes + put :update_multiple, { id: playlist.to_param, annotation_ids: [] }, valid_session + expect(response).to redirect_to(edit_playlist_path(playlist)) + end + end + end + + describe 'DELETE #destroy' do + it 'destroys the requested playlist' do + playlist = Playlist.create! valid_attributes + expect do + delete :destroy, { id: playlist.to_param }, valid_session + end.to change(Playlist, :count).by(-1) + end + + it 'redirects to the playlists list' do + playlist = Playlist.create! valid_attributes + delete :destroy, { id: playlist.to_param }, valid_session + expect(response).to redirect_to(playlists_url) + end + end + + describe 'GET #edit' do + it 'assigns the requested playlist as @playlist' do + playlist = Playlist.create! valid_attributes + get :edit, { id: playlist.to_param }, valid_session + expect(assigns(:playlist)).to eq(playlist) + end + end + +end diff --git a/spec/controllers/vocabulary_controller_spec.rb b/spec/controllers/vocabulary_controller_spec.rb new file mode 100644 index 0000000000..cdc20706cf --- /dev/null +++ b/spec/controllers/vocabulary_controller_spec.rb @@ -0,0 +1,111 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'spec_helper' +require 'fileutils' + +describe VocabularyController, type: :controller do + render_views + + before(:all) { + FileUtils.cp_r 'spec/fixtures/controlled_vocabulary.yml', 'spec/fixtures/controlled_vocabulary.yml.tmp' + Avalon::ControlledVocabulary.class_variable_set :@@path, Rails.root.join('spec/fixtures/controlled_vocabulary.yml.tmp') + } + after(:all) { + File.delete('spec/fixtures/controlled_vocabulary.yml.tmp') + } + + before do + request.headers['Avalon-Api-Key'] = 'secret_token' + end + + describe 'security' do + let(:vocab) { :units } + describe 'ingest api' do + it "all routes should return 401 when no token is present" do + request.headers['Avalon-Api-Key'] = nil + expect(get :index, format: 'json').to have_http_status(401) + expect(get :show, id: vocab, format: 'json').to have_http_status(401) + expect(put :update, id: vocab, format: 'json').to have_http_status(401) + expect(patch :update, id: vocab, format: 'json').to have_http_status(401) + end + it "all routes should return 403 when a bad token in present" do + request.headers['Avalon-Api-Key'] = 'badtoken' + expect(get :index, format: 'json').to have_http_status(403) + expect(get :show, id: vocab, format: 'json').to have_http_status(403) + expect(put :update, id: vocab, format: 'json').to have_http_status(403) + expect(patch :update, id: vocab, format: 'json').to have_http_status(403) + end + end + describe 'normal auth' do + context 'with end-user' do + before do + request.headers['Avalon-Api-Key'] = nil + login_as :user + end + it "all routes should redirect to /" do + expect(get :index, format: 'json').to redirect_to(root_path) + expect(get :show, id: vocab, format: 'json').to redirect_to(root_path) + expect(put :update, id: vocab, format: 'json').to redirect_to(root_path) + expect(patch :update, id: vocab, format: 'json').to redirect_to(root_path) + end + end + end + end + + describe "#index" do + it "should return vocabulary for entire app" do + get 'index', format:'json' + expect(JSON.parse(response.body)).to include('units','note_types','identifier_types') + end + end + describe "#show" do + it "should return a particular vocabulary" do + get 'show', format:'json', id: :units + expect(JSON.parse(response.body)).to include('Default Unit') + end + it "should return 404 if requested vocabulary not present" do + get 'show', format:'json', id: :doesnt_exist + expect(response.status).to eq(404) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end + describe "#update" do + it "should add unit to controlled vocabulary" do + put 'update', format:'json', id: :units, entry: 'New Unit' + expect(Avalon::ControlledVocabulary.vocabulary[:units]).to include("New Unit") + end + it "should return 404 if requested vocabulary not present" do + put 'update', format:'json', id: :doesnt_exist, entry: 'test' + expect(response.status).to eq(404) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + it "should return 422 if no new value sent" do + put 'update', format:'json', id: :units + expect(response.status).to eq(422) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + it "should return 422 if update fails" do + allow(Avalon::ControlledVocabulary).to receive(:vocabulary=).and_return(false) + put 'update', format:'json', id: :units + expect(response.status).to eq(422) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + end + +end diff --git a/spec/factories/avalon_annotation.rb b/spec/factories/avalon_annotation.rb new file mode 100644 index 0000000000..c1a1ed6d07 --- /dev/null +++ b/spec/factories/avalon_annotation.rb @@ -0,0 +1,23 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +FactoryGirl.define do + factory :avalon_annotation do + master_file { FactoryGirl.create(:master_file) } + title { Faker::Lorem.word } + comment { Faker::Lorem.sentence } + start_time { 0.0 } + end_time { 100.0 } + end +end diff --git a/spec/factories/collections.rb b/spec/factories/collections.rb index d97be03dd1..6156cbf128 100644 --- a/spec/factories/collections.rb +++ b/spec/factories/collections.rb @@ -25,6 +25,7 @@ transient { items 0 } after(:create) do |c, env| 1.upto(env.items) { FactoryGirl.create(:media_object, collection: c) } + c.reload end end end diff --git a/spec/factories/lease.rb b/spec/factories/lease.rb new file mode 100644 index 0000000000..b342df1040 --- /dev/null +++ b/spec/factories/lease.rb @@ -0,0 +1,20 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +FactoryGirl.define do + factory :lease, class: Lease do + begin_time { Date.yesterday } + end_time { Date.tomorrow } + end +end diff --git a/spec/factories/master_files.rb b/spec/factories/master_files.rb index 12c9ebbd90..ce0b44c2ac 100644 --- a/spec/factories/master_files.rb +++ b/spec/factories/master_files.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -18,7 +18,8 @@ file_format {'Moving image'} percent_complete {"#{rand(100)}"} workflow_name 'avalon' - mediaobject {FactoryGirl.create(:media_object)} + duration {'100'} + association :mediaobject, factory: :media_object factory :master_file_with_derivative do workflow_name 'avalon' @@ -35,5 +36,17 @@ mf.save end end + factory :master_file_with_structure do + after(:create) do |mf| + mf.structuralMetadata.content = File.read('spec/fixtures/structure.xml') + mf.save + end + end + factory :master_file_sound do + after(:create) do |mf| + mf.file_format = 'Sound' + mf.save + end + end end end diff --git a/spec/factories/media_objects.rb b/spec/factories/media_objects.rb index 673b863501..cf9ff5de62 100644 --- a/spec/factories/media_objects.rb +++ b/spec/factories/media_objects.rb @@ -32,7 +32,7 @@ topical_subject {[Faker::Lorem.word]} temporal_subject {[Faker::Lorem.word]} geographic_subject {[Faker::Address.country]} - physical_description {Faker::Lorem.word} + physical_description {[Faker::Lorem.word]} table_of_contents {[Faker::Lorem.paragraph]} after(:create) do |mo| mo.update_datastream(:descMetadata, { @@ -55,6 +55,12 @@ mo.save end end + factory :media_object_with_completed_workflow do + after(:create) do |mo| + mo.workflow.last_completed_step = [HYDRANT_STEPS.last.step] + mo.save + end + end end factory :minimal_record, class: MediaObject do @@ -78,8 +84,9 @@ factory :multiple_entries, class: MediaObject do title 'Multiple contributors' creator ['RSpec'] - date_issued '#{Date.today.edtf}' + date_issued {"#{Date.today.edtf}"} abstract 'A record with multiple contributors, publishers, and search terms' + collection {FactoryGirl.create(:collection)} contributor ['Chris Colvard', 'Nathan Rogers', 'Phuong Dinh'] publisher ['Mark Notess', 'Jon Dunn', 'Stu Baker'] diff --git a/spec/factories/playlist.rb b/spec/factories/playlist.rb new file mode 100644 index 0000000000..1dee32b4ce --- /dev/null +++ b/spec/factories/playlist.rb @@ -0,0 +1,22 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +FactoryGirl.define do + factory :playlist do + user + title { Faker::Lorem.word } + comment { Faker::Lorem.sentence } + visibility { Playlist::PRIVATE } + end +end diff --git a/spec/factories/playlist_item.rb b/spec/factories/playlist_item.rb new file mode 100644 index 0000000000..9b82b52124 --- /dev/null +++ b/spec/factories/playlist_item.rb @@ -0,0 +1,20 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +FactoryGirl.define do + factory :playlist_item do + playlist + association :annotation, factory: :avalon_annotation + end +end diff --git a/spec/fixtures/7763100-ns.xml b/spec/fixtures/7763100-ns.xml new file mode 100644 index 0000000000..a5dfea1ec0 --- /dev/null +++ b/spec/fixtures/7763100-ns.xml @@ -0,0 +1,271 @@ + +1.11info:srw/schema/1/marcxml-v1.1xml + 01523cgm a2200433 a 4500 + 7763100 + 20150127160330.0 + aj can|n + 150127r19991990ilu--- kneng d + + IEN + IEN + + + eng + fre + ger + + + 111 A + C + D + E + F + G + K + L + N + P + Q + T + + + 245 A : + B + F + G + K + N + P + S / + C. + + + 246 + + + 260 A : + 260 B, + 2010. + + + 260 A : + 260 B, + 2014. + + + 300 A : + 300 B ; + 300 C. + + + 600 (100) A + B + C + D + F + G + K + L + N + P + Q + T + + + 610 (110) A + B + B + C + D + F + G + K + L + N + P + T + + + 611 (111) A + C + D + E + F + G + K + L + N + P + Q + T + + + 500 + + + 520 + + + 530 + + + 540 + + + 600 A + B + C + D + F + G + K + L + M + N + O + P + Q + R + S + T + 600 X + 600 Y + 600 Z + 600 V + + + 610 A + B + B + C + D + F + G + K + L + N + P + T + 610 X + 610 Y + 610 Z + 610 V + + + 611 A + C + D + E + F + G + K + L + N + P + Q + S + T + 611 X + 611 Y + 611 Z + 611 V + + + 630 A + D + F + H + K + L + O + R + 630 X + 630 Y + 630 Z + 630 V + + + 648 + lcsh. + + + 648 + + + 650 A + 650 X + 650 Y + 650 Z + 650 V + + + 651 A + 651 X + 651 Y + 651 Z + 651 V + + + 653 A + 653 A + + + 655 A + + + 700 A + B + C + D + F + G + K + L + M + N + O + P + Q + R + S + T + + + 710 A + B + B + C + D + F + G + K + L + N + P + T + + + 711 A + C + D + E + F + G + K + L + N + P + Q + S + T + + + 752 A + 752 B + 752 C + 752 D + + + suppressed record for testing Avalon. + +1 diff --git a/spec/fixtures/controlled_vocabulary.yml b/spec/fixtures/controlled_vocabulary.yml new file mode 100644 index 0000000000..37ed03f8d8 --- /dev/null +++ b/spec/fixtures/controlled_vocabulary.yml @@ -0,0 +1,22 @@ +--- +:units: +- Default Unit +:identifier_types: + local: Catalog Key + oclc: OCLC + lccn: LCCN + issue number: Issue Number + matrix number: Matrix Number + music publisher: Music Publisher/Label + videorecording identifier: Videorecording Identifier + other: Other +:note_types: + general: General Note + awards: Awards + biographical/historical: Bibliographical/Historical Note + creation/production credits: Creation/Production Credits + language: Language Note + local: Local Note + performers: Performers + statement of responsibility: Statement of Responsibility + venue: Venue/Event Date diff --git a/spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt b/spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt new file mode 100644 index 0000000000..0e9a211f7d --- /dev/null +++ b/spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt @@ -0,0 +1,33 @@ +WEBVTT FILE + +1 +00:00:03.500 --> 00:00:05.000 D:vertical A:start +Everyone wants the most from life + +2 +00:00:06.000 --> 00:00:09.000 A:start +Like internet experiences that are rich and entertaining + +3 +00:00:11.000 --> 00:00:14.000 A:end +Phone conversations where people truly connect + +4 +00:00:14.500 --> 00:00:18.000 +Your favourite TV programmes ready to watch at the touch of a button + +5 +00:00:19.000 --> 00:00:24.000 +Which is why we are bringing TV, internet and phone together in one super package + +6 +00:00:24.500 --> 00:00:26.000 +One simple way to get everything + +7 +00:00:26.500 --> 00:00:27.500 L:12% +UPC + +8 +00:00:28.000 --> 00:00:30.000 L:75% +Simply for everyone \ No newline at end of file diff --git a/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx b/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx index f748131aed..57e74e858c 100644 Binary files a/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx and b/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx differ diff --git a/spec/fixtures/dropbox/example_batch_ingest/missing_required_field.xlsx b/spec/fixtures/dropbox/example_batch_ingest/missing_required_field.xlsx index 83af06efc1..89960b4b09 100644 Binary files a/spec/fixtures/dropbox/example_batch_ingest/missing_required_field.xlsx and b/spec/fixtures/dropbox/example_batch_ingest/missing_required_field.xlsx differ diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index acdaa50bbd..b14fa9a579 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -17,39 +17,39 @@ describe ApplicationHelper do describe "#active_for_controller" do before(:each) do - helper.stub(:params).and_return({controller: 'media_objects'}) + allow(helper).to receive(:params).and_return({controller: 'media_objects'}) end it "should return 'active' for matching controller names" do - helper.active_for_controller('media_objects').should == 'active' + expect(helper.active_for_controller('media_objects')).to eq('active') end it "should return '' for non-matching controller names" do - helper.active_for_controller('master_files').should == '' + expect(helper.active_for_controller('master_files')).to eq('') end it "should handle name-spaced controllers" do - helper.stub(:params).and_return({controller: 'admin/groups'}) - helper.active_for_controller('admin/groups').should == 'active' - helper.active_for_controller('groups').should == '' + allow(helper).to receive(:params).and_return({controller: 'admin/groups'}) + expect(helper.active_for_controller('admin/groups')).to eq('active') + expect(helper.active_for_controller('groups')).to eq('') end end describe "#stream_label_for" do it "should return the label first if it is available" do master_file = FactoryGirl.build(:master_file, label: 'Label') - master_file.file_location.should_not be_nil - master_file.label.should_not be_nil - helper.stream_label_for(master_file).should == 'Label' + expect(master_file.file_location).not_to be_nil + expect(master_file.label).not_to be_nil + expect(helper.stream_label_for(master_file)).to eq('Label') end it "should return the filename second if it is available" do master_file = FactoryGirl.build(:master_file, label: nil) - master_file.file_location.should_not be_nil - master_file.label.should be_nil - helper.stream_label_for(master_file).should == File.basename(master_file.file_location) + expect(master_file.file_location).not_to be_nil + expect(master_file.label).to be_nil + expect(helper.stream_label_for(master_file)).to eq(File.basename(master_file.file_location)) end it "should handle empty file_locations and labels" do master_file = FactoryGirl.build(:master_file, file_location: nil, label: nil) - master_file.file_location.should be_nil - master_file.label.should be_nil - helper.stream_label_for(master_file).should == master_file.pid + expect(master_file.file_location).to be_nil + expect(master_file.label).to be_nil + expect(helper.stream_label_for(master_file)).to eq(master_file.pid) end end @@ -104,27 +104,27 @@ describe "#image_for" do # image_for expects hash keys as labels, not strings it "should return nil" do - doc = {"mods_tesim" => [] } + doc = {"avalon_resource_type_tesim" => [] } expect(helper.image_for(doc)).to eq(nil) end it "should return audio icon" do - doc = {"mods_tesim" => ['sound recording 2', 'sound recording 1'] } + doc = {"avalon_resource_type_tesim" => ['sound recording 2', 'sound recording 1'] } expect(helper.image_for(doc)).to eq('/assets/audio_icon.png') end it "should return video icon" do - doc = {"mods_tesim" => ['moving image 1'] } + doc = {"avalon_resource_type_tesim" => ['moving image 1'] } expect(helper.image_for(doc)).to eq('/assets/video_icon.png') end it "should return hybrid icon" do - doc = {"mods_tesim" => ['moving image 1', 'sound recording 1'] } + doc = {"avalon_resource_type_tesim" => ['moving image 1', 'sound recording 1'] } expect(helper.image_for(doc)).to eq('/assets/hybrid_icon.png') end it "should return nil when only unprocessed video" do - doc = {"section_pid_tesim" => ['1'], "mods_tesim" => [] } + doc = {"section_pid_tesim" => ['1'], "avalon_resource_type_tesim" => [] } expect(helper.image_for(doc)).to eq(nil) end it "should return thumbnail" do - doc = {"section_pid_tesim" => ['1'], "mods_tesim" => ['moving image 1'] } + doc = {"section_pid_tesim" => ['1'], "avalon_resource_type_tesim" => ['moving image 1'] } expect(helper.image_for(doc)).to eq('/master_files/1/thumbnail') end end diff --git a/spec/integration/omniauth_callbacks_controller_spec.rb b/spec/integration/omniauth_callbacks_controller_spec.rb index e1bc796f75..b11a6d0b94 100644 --- a/spec/integration/omniauth_callbacks_controller_spec.rb +++ b/spec/integration/omniauth_callbacks_controller_spec.rb @@ -28,7 +28,7 @@ before :each do hide_const("Avalon::GROUP_LDAP") - IMS::LTI::ToolProvider.any_instance.stub(:valid_request!) { true } + allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request!) { true } @old_config = Devise.omniauth_configs[:lti].options[:consumers] Devise.omniauth_configs[:lti].options[:consumers] = Devise.omniauth_configs[:lti].strategy[:consumers] = lti_config end @@ -65,7 +65,7 @@ subject { Hash.new } before :each do - Users::OmniauthCallbacksController.any_instance.stub(:user_session) { subject } + allow_any_instance_of(Users::OmniauthCallbacksController).to receive(:user_session) { subject } post '/users/auth/lti/callback', foo_hash end @@ -84,7 +84,7 @@ subject { Hash.new } before :each do - Users::OmniauthCallbacksController.any_instance.stub(:user_session) { subject } + allow_any_instance_of(Users::OmniauthCallbacksController).to receive(:user_session) { subject } foo_hash.delete('lis_person_sourcedid') post '/users/auth/lti/callback', foo_hash end @@ -102,14 +102,14 @@ subject { post '/users/auth/lti/callback', foo_hash } let(:user_session) { Hash.new } before :each do - Users::OmniauthCallbacksController.any_instance.stub(:user_session) { user_session } + allow_any_instance_of(Users::OmniauthCallbacksController).to receive(:user_session) { user_session } end it "should redirect to the external group facet applied for the lti group" do expect(subject).to redirect_to catalog_index_path('f[read_access_virtual_group_ssim][]' => user_session[:lti_group]) end context 'when there are other external groups' do before do - User.any_instance.stub(:ldap_groups) { [Faker::Lorem.word] } + allow_any_instance_of(User).to receive(:ldap_groups) { [Faker::Lorem.word] } end it "should redirect to the external group facet applied for the lti group" do expect(subject).to redirect_to catalog_index_path('f[read_access_virtual_group_ssim][]' => user_session[:lti_group]) diff --git a/spec/lib/avalon/batch_entry_spec.rb b/spec/lib/avalon/batch_entry_spec.rb index f5a5f3d9dc..8f323ccfa3 100644 --- a/spec/lib/avalon/batch_entry_spec.rb +++ b/spec/lib/avalon/batch_entry_spec.rb @@ -38,9 +38,9 @@ let(:collection) {FactoryGirl.create(:collection)} let(:manifest) do manifest = double() - manifest.stub_chain(:package, :dir).and_return(testdir) - manifest.stub_chain(:package, :user, :user_key).and_return('archivist1@example.org') - manifest.stub_chain(:package, :collection).and_return(collection) + allow(manifest).to receive_message_chain(:package, :dir).and_return(testdir) + allow(manifest).to receive_message_chain(:package, :user, :user_key).and_return('archivist1@example.org') + allow(manifest).to receive_message_chain(:package, :collection).and_return(collection) manifest end let(:entry) do diff --git a/spec/lib/avalon/batch_ingest_spec.rb b/spec/lib/avalon/batch_ingest_spec.rb index 5e05b0c8e4..ae7095a882 100644 --- a/spec/lib/avalon/batch_ingest_spec.rb +++ b/spec/lib/avalon/batch_ingest_spec.rb @@ -43,7 +43,7 @@ # this is a test environment, we don't want to kick off # generation jobs if possible - MasterFile.any_instance.stub(:save).and_return(true) + allow_any_instance_of(MasterFile).to receive(:save).and_return(true) end describe 'valid manifest' do @@ -72,8 +72,8 @@ it 'should send email when batch finishes processing' do mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_success).with(duck_type(:each)).and_return(mailer) - mailer.should_receive(:deliver) + expect(IngestBatchMailer).to receive(:batch_ingest_validation_success).with(duck_type(:each)).and_return(mailer) + expect(mailer).to receive(:deliver) batch_ingest.ingest end @@ -84,21 +84,21 @@ expect { batch_ingest.ingest }.not_to raise_error expect { batch_ingest.ingest }.not_to change{IngestBatch.count} error_file = File.join(@dropbox_dir,'example_batch_ingest','bad_manifest.xlsx.error') - File.exists?(error_file).should be true - File.read(error_file).should =~ /^Invalid manifest/ + expect(File.exists?(error_file)).to be true + expect(File.read(error_file)).to match(/^Invalid manifest/) end it 'should ingest batch with spaces in name' do space_batch_path = File.join('spec/fixtures/dropbox/example batch ingest', 'batch manifest with spaces.xlsx') space_batch = Avalon::Batch::Package.new(space_batch_path, collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [space_batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [space_batch] expect{batch_ingest.ingest}.to change{IngestBatch.count}.by(1) end it 'should ingest batch with skip-transcoding derivatives' do derivatives_batch_path = File.join('spec/fixtures/dropbox/pretranscoded_batch_ingest', 'batch_manifest_derivatives.xlsx') derivatives_batch = Avalon::Batch::Package.new(derivatives_batch_path, collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [derivatives_batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [derivatives_batch] expect_any_instance_of(MasterFile).to receive(:process).with(hash_including('quality-high', 'quality-medium', 'quality-low')) expect{batch_ingest.ingest}.to change{IngestBatch.count}.by(1) end @@ -111,8 +111,8 @@ batch_ingest.ingest ingest_batch = IngestBatch.first media_object = MediaObject.find(ingest_batch.media_object_ids.last) - media_object.bibliographic_id.should == ['local', bib_id] - media_object.title.should == '245 A : B F G K N P S' + expect(media_object.bibliographic_id).to eq(['local', bib_id]) + expect(media_object.title).to eq('245 A : B F G K N P S') end it 'should ingest structural metadata' do @@ -120,7 +120,15 @@ ingest_batch = IngestBatch.first media_object = MediaObject.find(ingest_batch.media_object_ids.first) master_file = media_object.parts.first - expect(master_file.structuralMetadata.has_content?).to be_true + expect(master_file.structuralMetadata.has_content?).to be_truthy + end + + it 'should ingest captions' do + batch_ingest.ingest + ingest_batch = IngestBatch.first + media_object = MediaObject.find(ingest_batch.media_object_ids.first) + master_file = media_object.parts.first + expect(master_file.captions.has_content?).to be_truthy end it 'should set MasterFile details' do @@ -128,56 +136,57 @@ ingest_batch = IngestBatch.last media_object = MediaObject.find(ingest_batch.media_object_ids.first) master_file = media_object.parts.first - master_file.label.should == 'Quis quo' - master_file.poster_offset.to_i.should == 500 - master_file.workflow_name.should == 'avalon' - master_file.absolute_location.should == Avalon::FileResolver.new.path_to(master_file.file_location) - + expect(master_file.label).to eq('Quis quo') + expect(master_file.poster_offset.to_i).to eq(500) + expect(master_file.workflow_name).to eq('avalon') + expect(master_file.absolute_location).to eq(Avalon::FileResolver.new.path_to(master_file.file_location)) + expect(master_file.date_digitized).to eq('2015-10-30T00:00:00Z') # if a master file is saved on a media object # it should have workflow name set # master_file.workflow_name.should be_nil master_file = media_object.parts[1] - master_file.label.should == 'Unde aliquid' - master_file.poster_offset.to_i.should == 500 - master_file.workflow_name.should == 'avalon-skip-transcoding' - master_file.absolute_location.should == 'file:///tmp/sheephead_mountain_master.mov' + expect(master_file.label).to eq('Unde aliquid') + expect(master_file.poster_offset.to_i).to eq(500) + expect(master_file.workflow_name).to eq('avalon-skip-transcoding') + expect(master_file.absolute_location).to eq('file:///tmp/sheephead_mountain_master.mov') + expect(master_file.date_digitized).to eq('2015-10-31T00:00:00Z') master_file = media_object.parts[2] - master_file.label.should == 'Audio' - master_file.workflow_name.should == 'fullaudio' - master_file.absolute_location.should == Avalon::FileResolver.new.path_to(master_file.file_location) + expect(master_file.label).to eq('Audio') + expect(master_file.workflow_name).to eq('fullaudio') + expect(master_file.absolute_location).to eq(Avalon::FileResolver.new.path_to(master_file.file_location)) end it 'should set avalon_uploader' do batch_ingest.ingest ingest_batch = IngestBatch.last media_object = MediaObject.find(ingest_batch.media_object_ids.first) - media_object.avalon_uploader.should == 'frances.dickens@reichel.com' + expect(media_object.avalon_uploader).to eq('frances.dickens@reichel.com') end it 'should set hidden' do batch_ingest.ingest ingest_batch = IngestBatch.last media_object = MediaObject.find(ingest_batch.media_object_ids.first) - media_object.should_not be_hidden + expect(media_object).not_to be_hidden media_object = MediaObject.find(ingest_batch.media_object_ids[1]) - media_object.should be_hidden + expect(media_object).to be_hidden end it 'should correctly set identifiers' do batch_ingest.ingest ingest_batch = IngestBatch.last media_object = MediaObject.find(ingest_batch.media_object_ids.last) - media_object.bibliographic_id.should eq(["local",bib_id]) + expect(media_object.bibliographic_id).to eq(["local",bib_id]) end it 'should correctly set notes' do batch_ingest.ingest ingest_batch = IngestBatch.last media_object = MediaObject.find(ingest_batch.media_object_ids.first) - media_object.note.first.should eq(["general","This is a test general note"]) + expect(media_object.note.first).to eq(["general","This is a test general note"]) end end @@ -198,65 +207,65 @@ end it 'does not create an ingest batch object when there are zero packages' do - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [] #expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(anything(), include("Expected error message")) expect{batch_ingest.ingest}.to_not change{IngestBatch.count} end it 'should result in an error if a file is not found' do batch = Avalon::Batch::Package.new( 'spec/fixtures/dropbox/example_batch_ingest/wrong_filename_manifest.xlsx', collection ) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) - mailer.should_receive(:deliver) + expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) + expect(mailer).to receive(:deliver) expect{batch_ingest.ingest}.to_not change{IngestBatch.count} - batch.errors[3].messages.should have_key(:content) - batch.errors[3].messages[:content].should eq(["File not found: spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain_wrong.mov"]) + expect(batch.errors[3].messages).to have_key(:content) + expect(batch.errors[3].messages[:content]).to eq(["File not found: spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain_wrong.mov"]) end it 'does not create an ingest batch object when there are no files' do batch = Avalon::Batch::Package.new('spec/fixtures/dropbox/example_batch_ingest/no_files.xlsx', collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] expect{batch_ingest.ingest}.to_not change{IngestBatch.count} end it 'should fail if the manifest specified a non-manager user' do batch = Avalon::Batch::Package.new('spec/fixtures/dropbox/example_batch_ingest/non_manager_manifest.xlsx', collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_error).with(anything(), include("User jay@krajcik.org does not have permission to add items to collection: Ut minus ut accusantium odio autem odit..")).and_return(mailer) - mailer.should_receive(:deliver) + expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(anything(), include("User jay@krajcik.org does not have permission to add items to collection: Ut minus ut accusantium odio autem odit..")).and_return(mailer) + expect(mailer).to receive(:deliver) expect{batch_ingest.ingest}.to_not change{IngestBatch.count} end it 'should fail if a bad offset is specified' do batch = Avalon::Batch::Package.new('spec/fixtures/dropbox/example_batch_ingest/bad_offset_manifest.xlsx', collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) - mailer.should_receive(:deliver) + expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) + expect(mailer).to receive(:deliver) expect{batch_ingest.ingest}.to_not change{IngestBatch.count} - batch.errors[4].messages.should have_key(:offset) - batch.errors[4].messages[:offset].should eq(['Invalid offset: 5:000']) + expect(batch.errors[4].messages).to have_key(:offset) + expect(batch.errors[4].messages[:offset]).to eq(['Invalid offset: 5:000']) end it 'should fail if missing required field' do batch = Avalon::Batch::Package.new('spec/fixtures/dropbox/example_batch_ingest/missing_required_field.xlsx', collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) - mailer.should_receive(:deliver) + expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) + expect(mailer).to receive(:deliver) expect{batch_ingest.ingest}.to_not change{IngestBatch.count} - batch.errors[4].messages.should have_key(:creator) - batch.errors[4].messages[:creator].should eq(['field is required.']) + expect(batch.errors[4].messages).to have_key(:title) + expect(batch.errors[4].messages[:title]).to eq(['field is required.']) end it 'should fail if field is not in accepted metadata field list' do batch = Avalon::Batch::Package.new('spec/fixtures/dropbox/example_batch_ingest/badColumnName_nonRequired.xlsx', collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) - mailer.should_receive(:deliver) + expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(duck_type(:each),duck_type(:each)).and_return(mailer) + expect(mailer).to receive(:deliver) expect{batch_ingest.ingest}.to_not change{IngestBatch.count} expect(batch.errors[4].messages).to have_key(:contributator) expect(batch.errors[4].messages[:contributator]).to eq(["Metadata attribute 'contributator' not found"]) @@ -264,11 +273,11 @@ it 'should fail if an unknown error occurs' do batch = Avalon::Batch::Package.new('spec/fixtures/dropbox/example_batch_ingest/badColumnName_nonRequired.xlsx', collection) - Avalon::Dropbox.any_instance.stub(:find_new_packages).and_return [batch] + allow_any_instance_of(Avalon::Dropbox).to receive(:find_new_packages).and_return [batch] mailer = double('mailer').as_null_object - IngestBatchMailer.should_receive(:batch_ingest_validation_error).with(batch ,['RuntimeError: Foo']).and_return(mailer) - mailer.should_receive(:deliver) - batch_ingest.should_receive(:ingest_package) { raise "Foo" } + expect(IngestBatchMailer).to receive(:batch_ingest_validation_error).with(batch ,['RuntimeError: Foo']).and_return(mailer) + expect(mailer).to receive(:deliver) + expect(batch_ingest).to receive(:ingest_package) { raise "Foo" } expect { batch_ingest.ingest }.to_not raise_error end end diff --git a/spec/lib/avalon/bib_retriever_spec.rb b/spec/lib/avalon/bib_retriever_spec.rb index 3cdbe7de5b..082e59cb4d 100644 --- a/spec/lib/avalon/bib_retriever_spec.rb +++ b/spec/lib/avalon/bib_retriever_spec.rb @@ -41,26 +41,47 @@ describe 'sru' do let(:sru_url) { "http://zgate.example.edu:9000/db?version=1.1&operation=searchRetrieve&maximumRecords=1&recordSchema=marcxml&query=rec.id=%5E%25#{bib_id}" } - let(:sru_response) { File.read(File.expand_path("../../../fixtures/#{bib_id}.xml",__FILE__)) } - - before :each do - Avalon::Configuration['bib_retriever'] = { 'protocol' => 'sru', 'url' => 'http://zgate.example.edu:9000/db' } - FakeWeb.register_uri :get, sru_url, body: sru_response - end - after :each do - FakeWeb.clean_registry + describe 'default namespace' do + let(:sru_response) { File.read(File.expand_path("../../../fixtures/#{bib_id}.xml",__FILE__)) } + + before :each do + Avalon::Configuration['bib_retriever'] = { 'protocol' => 'sru', 'url' => 'http://zgate.example.edu:9000/db' } + FakeWeb.register_uri :get, sru_url, body: sru_response + end + + after :each do + FakeWeb.clean_registry + end + + it 'retrieves proper MODS' do + response = Avalon::BibRetriever.instance.get_record("^%#{bib_id}") + expect(Nokogiri::XML(response)).to be_equivalent_to(mods) + end end - it 'retrieves proper MODS' do - response = Avalon::BibRetriever.instance.get_record("^%#{bib_id}") - expect(Nokogiri::XML(response)).to be_equivalent_to(mods) + describe 'alternate namespace' do + let(:sru_response) { File.read(File.expand_path("../../../fixtures/#{bib_id}-ns.xml",__FILE__)) } + + before :each do + Avalon::Configuration['bib_retriever'] = { 'protocol' => 'sru', 'url' => 'http://zgate.example.edu:9000/db', 'namespace' => 'http://example.edu/fake/sru/namespace/' } + FakeWeb.register_uri :get, sru_url, body: sru_response + end + + after :each do + FakeWeb.clean_registry + end + + it 'retrieves proper MODS' do + response = Avalon::BibRetriever.instance.get_record("^%#{bib_id}") + expect(Nokogiri::XML(response)).to be_equivalent_to(mods) + end end end describe 'zoom' do it 'retrieves proper MODS' do - pending "need a reasonable way to mock ruby-zoom" + skip "need a reasonable way to mock ruby-zoom" end end end diff --git a/spec/lib/avalon/controlled_vocabulary_spec.rb b/spec/lib/avalon/controlled_vocabulary_spec.rb index 7a933be8d6..61befbe19c 100644 --- a/spec/lib/avalon/controlled_vocabulary_spec.rb +++ b/spec/lib/avalon/controlled_vocabulary_spec.rb @@ -17,33 +17,33 @@ describe Avalon::ControlledVocabulary do before do - File.stub(:read).and_return '' - File.stub(:file?).and_return true + allow(File).to receive(:read).and_return '' + allow(File).to receive(:file?).and_return true end describe '#vocabulary' do it 'reads the file directly from disk' do - File.should_receive(:read).twice + expect(File).to receive(:read).twice Avalon::ControlledVocabulary.vocabulary Avalon::ControlledVocabulary.vocabulary end it 'returns an empty hash when yaml file is empty' do - Avalon::ControlledVocabulary.vocabulary.should eql({}) + expect(Avalon::ControlledVocabulary.vocabulary).to eql({}) end end describe '#find_by_name' do before do - Avalon::ControlledVocabulary.stub(:vocabulary).and_return({ units: ['Archives'] }) + allow(Avalon::ControlledVocabulary).to receive(:vocabulary).and_return({ units: ['Archives'] }) end it 'finds a vocabulary by name' do - Avalon::ControlledVocabulary.find_by_name('units').should eql ['Archives'] + expect(Avalon::ControlledVocabulary.find_by_name('units')).to eql ['Archives'] end it 'finds a vocabulary by symbol' do - Avalon::ControlledVocabulary.find_by_name(:units).should eql ['Archives'] + expect(Avalon::ControlledVocabulary.find_by_name(:units)).to eql ['Archives'] end end diff --git a/spec/lib/avalon/dropbox_spec.rb b/spec/lib/avalon/dropbox_spec.rb index 0e8ee8ed51..df37e1f33b 100644 --- a/spec/lib/avalon/dropbox_spec.rb +++ b/spec/lib/avalon/dropbox_spec.rb @@ -26,12 +26,12 @@ let(:collection) { FactoryGirl.create(:collection, name: 'Ut minus ut accusantium odio autem odit.', managers: ['frances.dickens@reichel.com']) } subject { Avalon::Dropbox.new(Avalon::Configuration.lookup('dropbox.path'),collection) } it 'returns true if the file is found' do - File.stub(:delete).and_return true + allow(File).to receive(:delete).and_return true subject.delete('some_file.mov') end it 'returns false if the file is not found' do - subject.delete('some_file.mov').should be false + expect(subject.delete('some_file.mov')).to be false end end diff --git a/spec/lib/avalon/file_resolver_spec.rb b/spec/lib/avalon/file_resolver_spec.rb index f3b46a14e8..d631b1c6a9 100644 --- a/spec/lib/avalon/file_resolver_spec.rb +++ b/spec/lib/avalon/file_resolver_spec.rb @@ -18,23 +18,23 @@ let(:resolver){ Avalon::FileResolver.new } describe "#path_to" do it 'returns umodified path when string already has a schema' do - resolver.path_to('http://example.com').should == 'http://example.com' + expect(resolver.path_to('http://example.com')).to eq('http://example.com') end it 'returns path with schema' do - resolver.stub(:mount_map).and_return({'/Volumes/dropbox/'=> 'smb://example.edu/dropbox'}) - resolver.path_to('/Volumes/dropbox/master_files/').should == 'smb://example.edu/dropbox/master_files' + allow(resolver).to receive(:mount_map).and_return({'/Volumes/dropbox/'=> 'smb://example.edu/dropbox'}) + expect(resolver.path_to('/Volumes/dropbox/master_files/')).to eq('smb://example.edu/dropbox/master_files') end it 'returns path with file schema when no mounts match' do - resolver.stub(:mount_map).and_return({}) - resolver.path_to('/storage/master_files/').should == 'file:///storage/master_files/' + allow(resolver).to receive(:mount_map).and_return({}) + expect(resolver.path_to('/storage/master_files/')).to eq('file:///storage/master_files/') end end describe "#mount_map" do it 'returns a formatted mount' do - resolver.stub(:overrides).and_return({}) + allow(resolver).to receive(:overrides).and_return({}) resolver.instance_variable_set(:@mounts, ['//adam@example.edu/dropbox on /Volumes/dropbox (smbfs, nodev, nosuid, mounted by adam)']) - resolver.mount_map.should == {'/Volumes/dropbox/'=>'smb://example.edu/dropbox'} + expect(resolver.mount_map).to eq({'/Volumes/dropbox/'=>'smb://example.edu/dropbox'}) end end end diff --git a/spec/lib/avalon/matterhorn_rtmp_url_spec.rb b/spec/lib/avalon/matterhorn_rtmp_url_spec.rb index a9c5aa17b1..ea8cdcbb3a 100644 --- a/spec/lib/avalon/matterhorn_rtmp_url_spec.rb +++ b/spec/lib/avalon/matterhorn_rtmp_url_spec.rb @@ -18,11 +18,13 @@ describe Avalon::MatterhornRtmpUrl do subject {Avalon::MatterhornRtmpUrl.parse('rtmp://localhost/avalon/mp4:98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4')} - its(:application) {should == 'avalon'} - its(:prefix) {should == 'mp4'} - its(:media_id) {should == '98285a5b-603a-4a14-acc0-20e37a3514bb'} - its(:stream_id) {should == 'b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3'} - its(:filename) {should == 'MVI_0057'} - its(:extension) {should == 'mp4'} - its(:to_path) {should == '98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4'} + it "should have attributes" do + expect(subject.application).to eq('avalon') + expect(subject.prefix).to eq('mp4') + expect(subject.media_id).to eq('98285a5b-603a-4a14-acc0-20e37a3514bb') + expect(subject.stream_id).to eq('b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3') + expect(subject.filename).to eq('MVI_0057') + expect(subject.extension).to eq('mp4') + expect(subject.to_path).to eq('98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4') + end end diff --git a/spec/lib/avalon/sanitizer_spec.rb b/spec/lib/avalon/sanitizer_spec.rb index 94b1c42b28..4f35ed1052 100644 --- a/spec/lib/avalon/sanitizer_spec.rb +++ b/spec/lib/avalon/sanitizer_spec.rb @@ -18,15 +18,15 @@ describe Avalon::Sanitizer do describe '#sanitize' do it 'replaces blacklisted characters' do - Avalon::Sanitizer.sanitize('abcdefg&',['&','_']).should == 'abcdefg_' + expect(Avalon::Sanitizer.sanitize('abcdefg&',['&','_'])).to eq('abcdefg_') end it 'replaces multiple blacklisted characters' do - Avalon::Sanitizer.sanitize('avalon*media&system',['*&','__']).should == 'avalon_media_system' + expect(Avalon::Sanitizer.sanitize('avalon*media&system',['*&','__'])).to eq('avalon_media_system') end it 'does not modify a string without any blacklisted characters' do - Avalon::Sanitizer.sanitize('avalon_media_system',['*&','__']).should == 'avalon_media_system' + expect(Avalon::Sanitizer.sanitize('avalon_media_system',['*&','__'])).to eq('avalon_media_system') end end end diff --git a/spec/mailers/ingest_batch_mailer_spec.rb b/spec/mailers/ingest_batch_mailer_spec.rb index 1e40854ebd..759a872ebd 100644 --- a/spec/mailers/ingest_batch_mailer_spec.rb +++ b/spec/mailers/ingest_batch_mailer_spec.rb @@ -23,20 +23,20 @@ it 'has an error if media object has not been set' do ingest_batch = IngestBatch.create @email = IngestBatchMailer.status_email( ingest_batch.id ) - @email.should have_body_text('There was an error. It appears no files have been submitted') + expect(@email).to have_body_text('There was an error. It appears no files have been submitted') end it 'has an error if there are no media objects present' do @ingest_batch = IngestBatch.create( media_object_ids: []) @email = IngestBatchMailer.status_email( @ingest_batch.id ) - @email.should have_body_text('There was an error. It appears no files have been submitted') + expect(@email).to have_body_text('There was an error. It appears no files have been submitted') end it 'shows the title of one media object' do media_object = FactoryGirl.create(:media_object) ingest_batch = IngestBatch.create(media_object_ids: [media_object.id]) @email = IngestBatchMailer.status_email(ingest_batch.id) - @email.should have_body_text(media_object.title) + expect(@email).to have_body_text(media_object.title) end it 'has the status of the master file in a first row' do @@ -50,8 +50,8 @@ # Ideally a within block would be nice here fragment = email_message.find("table > tbody > tr:nth-child(1)") - fragment.find('.master-file').should have_content(File.basename(master_file.file_location)) - fragment.find('.percent-complete').should have_content(master_file.percent_complete) - fragment.find('.status-code').should have_content(master_file.status_code.downcase.titleize) + expect(fragment.find('.master-file')).to have_content(File.basename(master_file.file_location)) + expect(fragment.find('.percent-complete')).to have_content(master_file.percent_complete) + expect(fragment.find('.status-code')).to have_content(master_file.status_code.downcase.titleize) end end diff --git a/spec/mailers/notifications_mailer_spec.rb b/spec/mailers/notifications_mailer_spec.rb index adbb16c6fb..31bf2ede8b 100644 --- a/spec/mailers/notifications_mailer_spec.rb +++ b/spec/mailers/notifications_mailer_spec.rb @@ -39,38 +39,38 @@ end it 'has correct e-mail address' do - @email.should deliver_to(@admin_user.email) + expect(@email).to deliver_to(@admin_user.email) end context 'subject' do it 'has collection name' do - @email.should have_subject(/#{@collection.name}/) + expect(@email).to have_subject(/#{@collection.name}/) end end context 'body' do it 'has link to collection' do - @email.should have_body_text(admin_collection_url(@collection)) + expect(@email).to have_body_text(admin_collection_url(@collection)) end it 'has collection name' do - @email.should have_body_text(@collection.name) + expect(@email).to have_body_text(@collection.name) end it 'has old collection name' do - @email.should have_body_text(@old_name) + expect(@email).to have_body_text(@old_name) end it 'has updater e-mail' do - @email.should have_body_text(@updater.email) + expect(@email).to have_body_text(@updater.email) end it 'has collection description' do - @email.should have_body_text(@collection.description) + expect(@email).to have_body_text(@collection.description) end it 'has unit name' do - @email.should have_body_text(@collection.unit) + expect(@email).to have_body_text(@collection.unit) end it 'has dropbox absolute path' do @@ -94,30 +94,30 @@ end it 'has correct e-mail address' do - @email.should deliver_to(@admin_user.email) + expect(@email).to deliver_to(@admin_user.email) end context 'subject' do it 'has collection name' do - @email.should have_subject(/#{@collection.name}/) + expect(@email).to have_subject(/#{@collection.name}/) end end context 'body' do it 'has collection name' do - @email.should have_body_text(@collection.name) + expect(@email).to have_body_text(@collection.name) end it 'has creator e-mail' do - @email.should have_body_text(@creator.email) + expect(@email).to have_body_text(@creator.email) end it 'has collection description' do - @email.should have_body_text(@collection.description) + expect(@email).to have_body_text(@collection.description) end it 'has unit name' do - @email.should have_body_text(@collection.unit) + expect(@email).to have_body_text(@collection.unit) end it 'has dropbox absolute path' do diff --git a/spec/migrations/r2_group_migration_spec.rb b/spec/migrations/r2_group_migration_spec.rb index 922c885d02..4dfb4fc347 100644 --- a/spec/migrations/r2_group_migration_spec.rb +++ b/spec/migrations/r2_group_migration_spec.rb @@ -38,11 +38,11 @@ end it "should create the administrator group" do - Admin::Group.find('administrator').should be_nil + expect(Admin::Group.find('administrator')).to be_nil R2GroupMigration.new.up admin_group = Admin::Group.find('administrator') - admin_group.should_not be_nil - admin_group.users.should include(Admin::Group.find('group_manager').users.first) + expect(admin_group).not_to be_nil + expect(admin_group.users).to include(Admin::Group.find('group_manager').users.first) end end diff --git a/spec/models/avalon_annonation_spec.rb b/spec/models/avalon_annonation_spec.rb new file mode 100644 index 0000000000..372793d50c --- /dev/null +++ b/spec/models/avalon_annonation_spec.rb @@ -0,0 +1,182 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'spec_helper' + +describe AvalonAnnotation do + subject(:video_master_file) { FactoryGirl.create(:master_file, duration: "1") } + #subject(:sound_master_file) { FactoryGirl.create(:master_file_sound) } + let(:annotation) { AvalonAnnotation.new(master_file: video_master_file) } + + describe 'creating an annotation' do + it 'sets the master file when creating the object' do + expect(annotation.master_file.pid).to eq(video_master_file.pid) + end + it 'sets the source uri' do + expect(annotation.source).not_to be_nil + expect { URI.parse(annotation.source) }.not_to raise_error + end + it 'sets default end and start times' do + expect(annotation.start_time).to eq(0) + expect(annotation.end_time).to eq(1) + end + + it 'sets the end time to the duration of the master_file by default' do + allow(video_master_file).to receive(:duration).and_return(100) + annotation_with_duration = AvalonAnnotation.new(master_file: video_master_file) + expect(annotation_with_duration.end_time).to eq(100) + end + it 'sets the title to the master_file label by default' do + expect(annotation.title).to match(video_master_file.embed_title) + end + + it 'loads the master_file' do + annotation.save! + new_instance = AvalonAnnotation.find_by(id: annotation.id) + expect(new_instance.master_file.pid).to eq(video_master_file.pid) + end + + it 'can store start and end times' do + annotation.start_time = 0.5 + annotation.end_time = 0.75 + annotation.save! + annotation.reload + expect(annotation.start_time).to eq(0.5) + expect(annotation.end_time).to eq(0.75) + end + end + describe 'aliases for Avalon Annotation' do + describe 'aliasing content with comment' do + it 'sets content using comment=' do + content = 'Big Two, Little Eight' + annotation.comment = content + annotation.save! + expect(annotation.reload.content).to match(content) + end + it 'accesses content using comment' do + content = '1817' + annotation.content = content + annotation.save! + expect(annotation.reload.comment).to match(content) + end + end + describe 'aliasing label with title' do + it 'sets label using title=' do + title = 'Astrolounge' + annotation.title = title + annotation.save! + expect(annotation.reload.label).to match(title) + end + it 'accesses label using title' do + title = 'Defeat You' + annotation.label = title + annotation.save! + expect(annotation.reload.title).to match(title) + end + end + end + it 'can load the master_file based on the uri' do + mf = annotation.master_file + expect(mf.pid).to eq(video_master_file.pid) + end + describe 'selector' do + it 'can create the selector using the start and end time' do + video_master_file.stub(:rdf_uri).and_return(RuntimeError, 'htt://www.avalon.edu/obj') + expect{ annotation.mediafragment_uri }.not_to raise_error + end + end + describe 'solr' do + it 'can create a solr hash' do + expect(annotation.to_solr.class).to eq(Hash) + end + it 'can save the annotation and solrize it' do + expect(annotation).to receive(:post_to_solr).once + expect { annotation.save }.not_to raise_error + end + it 'can destroy annotation and remove it from solr' do + annotation.save! + expect(annotation).to receive(:delete_from_solr).once + expect { annotation.destroy }.not_to raise_error + end + end + describe 'time validation' do + describe 'negative times' do + it 'raises an ArgumentError when start_time is negative' do + annotation.start_time = -1 + expect(annotation).not_to be_valid + end + it 'raises an ArgumentError when end_time is negative' do + annotation.end_time = -1 + expect(annotation).not_to be_valid + end + end + describe 'start and end time spacing' do + it 'raises an error when the end time preceeds the start time' do + annotation.end_time = 0 + annotation.start_time = 1 + expect(annotation).not_to be_valid + end + it 'raises an error when the end time equals the start time' do + annotation.end_time = 0 + annotation.start_time = 0 + expect(annotation).not_to be_valid + end + end + describe 'duration' do + subject(:video_master_file) { FactoryGirl.create(:master_file, duration: "8", derivatives: [derivative, derivative2]) } + let(:derivative) { FactoryGirl.create(:derivative, duration: "12") } + let(:derivative2) { FactoryGirl.create(:derivative, duration: "10") } + it 'raises an error when end time exceeds the duration' do + annotation.end_time = 60 + expect(annotation).not_to be_valid + end + it 'allows end times longer than master_file duration but not longer than the longest derivative' do + annotation.end_time = 12 + expect(annotation).to be_valid + end + end + end + describe 'related annotations' do + let(:second_annotation) { AvalonAnnotation.new(master_file: video_master_file) } + let!(:user) {FactoryGirl.build(:user)} + it 'returns nil when there are no related annotations' do + allow(PlaylistItem).to receive(:where).and_return([]) + expect(annotation.playlist_position(1)).to be_nil + end + it 'returns position when there is a related annotation' do + #byebug + stub_playlist + expect(annotation.playlist_position(@playlist.id)).to eq(1) + expect(second_annotation.playlist_position(@playlist.id)).to eq(2) + end + + def stub_playlist + user.save! + @playlist = Playlist.new + @playlist.title = 'whatever' + @playlist.user_id = User.last.id + @playlist.save! + pos = 1 + [annotation, second_annotation].each do |a| + a.save! + @pi = PlaylistItem.new + @pi.playlist_id = @playlist.id + @pi.annotation_id = a.id + @pi.position = pos + @pi.save! + pos += 1 + end + end + end +end diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index daf2baebf2..69d9f8e48a 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -26,7 +26,7 @@ let(:ability){ Ability.new(user) } let(:user){ FactoryGirl.create(:administrator) } - it{ should be_able_to(:manage, collection) } + it{ is_expected.to be_able_to(:manage, collection) } end context 'when manager' do @@ -34,16 +34,16 @@ let(:ability){ Ability.new(user) } let(:user){ User.where(username: collection.managers.first).first } - it{ should be_able_to(:read, Admin::Collection) } - it{ should be_able_to(:update, collection) } - it{ should be_able_to(:read, collection) } - it{ should be_able_to(:update_unit, collection) } - it{ should be_able_to(:update_managers, collection) } - it{ should be_able_to(:update_editors, collection) } - it{ should be_able_to(:update_depositors, collection) } - it{ should be_able_to(:create, Admin::Collection) } - it{ should be_able_to(:destroy, collection) } - it{ should be_able_to(:update_access_control, collection) } + it{ is_expected.to be_able_to(:read, Admin::Collection) } + it{ is_expected.to be_able_to(:update, collection) } + it{ is_expected.to be_able_to(:read, collection) } + it{ is_expected.to be_able_to(:update_unit, collection) } + it{ is_expected.to be_able_to(:update_managers, collection) } + it{ is_expected.to be_able_to(:update_editors, collection) } + it{ is_expected.to be_able_to(:update_depositors, collection) } + it{ is_expected.to be_able_to(:create, Admin::Collection) } + it{ is_expected.to be_able_to(:destroy, collection) } + it{ is_expected.to be_able_to(:update_access_control, collection) } end context 'when editor' do @@ -52,16 +52,16 @@ let(:user){ User.where(username: collection.editors.first).first } #Will need to define new action that covers just the things that an editor is allowed to edit - it{ should be_able_to(:read, Admin::Collection) } - it{ should be_able_to(:read, collection) } - it{ should be_able_to(:update, collection) } - it{ should_not be_able_to(:update_unit, collection) } - it{ should_not be_able_to(:update_managers, collection) } - it{ should_not be_able_to(:update_editors, collection) } - it{ should be_able_to(:update_depositors, collection) } - it{ should_not be_able_to(:create, Admin::Collection) } - it{ should_not be_able_to(:destroy, collection) } - it{ should_not be_able_to(:update_access_control, collection) } + it{ is_expected.to be_able_to(:read, Admin::Collection) } + it{ is_expected.to be_able_to(:read, collection) } + it{ is_expected.to be_able_to(:update, collection) } + it{ is_expected.not_to be_able_to(:update_unit, collection) } + it{ is_expected.not_to be_able_to(:update_managers, collection) } + it{ is_expected.not_to be_able_to(:update_editors, collection) } + it{ is_expected.to be_able_to(:update_depositors, collection) } + it{ is_expected.not_to be_able_to(:create, Admin::Collection) } + it{ is_expected.not_to be_able_to(:destroy, collection) } + it{ is_expected.not_to be_able_to(:update_access_control, collection) } end context 'when depositor' do @@ -69,16 +69,16 @@ let(:ability){ Ability.new(user) } let(:user){ User.where(username: collection.depositors.first).first } - it{ should be_able_to(:read, Admin::Collection) } - it{ should be_able_to(:read, collection) } - it{ should_not be_able_to(:update_unit, collection) } - it{ should_not be_able_to(:update_managers, collection) } - it{ should_not be_able_to(:update_editors, collection) } - it{ should_not be_able_to(:update_depositors, collection) } - it{ should_not be_able_to(:create, collection) } - it{ should_not be_able_to(:update, collection) } - it{ should_not be_able_to(:destroy, collection) } - it{ should_not be_able_to(:update_access_control, collection) } + it{ is_expected.to be_able_to(:read, Admin::Collection) } + it{ is_expected.to be_able_to(:read, collection) } + it{ is_expected.not_to be_able_to(:update_unit, collection) } + it{ is_expected.not_to be_able_to(:update_managers, collection) } + it{ is_expected.not_to be_able_to(:update_editors, collection) } + it{ is_expected.not_to be_able_to(:update_depositors, collection) } + it{ is_expected.not_to be_able_to(:create, collection) } + it{ is_expected.not_to be_able_to(:update, collection) } + it{ is_expected.not_to be_able_to(:destroy, collection) } + it{ is_expected.not_to be_able_to(:update_access_control, collection) } end context 'when end user' do @@ -86,16 +86,16 @@ let(:ability){ Ability.new(user) } let(:user){ FactoryGirl.create(:user) } - it{ should_not be_able_to(:read, Admin::Collection) } - it{ should_not be_able_to(:read, collection) } - it{ should_not be_able_to(:update_unit, collection) } - it{ should_not be_able_to(:update_managers, collection) } - it{ should_not be_able_to(:update_editors, collection) } - it{ should_not be_able_to(:update_depositors, collection) } - it{ should_not be_able_to(:create, collection) } - it{ should_not be_able_to(:update, collection) } - it{ should_not be_able_to(:destroy, collection) } - it{ should_not be_able_to(:update_access_control, collection) } + it{ is_expected.not_to be_able_to(:read, Admin::Collection) } + it{ is_expected.not_to be_able_to(:read, collection) } + it{ is_expected.not_to be_able_to(:update_unit, collection) } + it{ is_expected.not_to be_able_to(:update_managers, collection) } + it{ is_expected.not_to be_able_to(:update_editors, collection) } + it{ is_expected.not_to be_able_to(:update_depositors, collection) } + it{ is_expected.not_to be_able_to(:create, collection) } + it{ is_expected.not_to be_able_to(:update, collection) } + it{ is_expected.not_to be_able_to(:destroy, collection) } + it{ is_expected.not_to be_able_to(:update_access_control, collection) } end context 'when lti user' do @@ -103,16 +103,16 @@ let(:ability){ Ability.new(user) } let(:user){ FactoryGirl.create(:user_lti) } - it{ should_not be_able_to(:read, Admin::Collection) } - it{ should_not be_able_to(:read, collection) } - it{ should_not be_able_to(:update_unit, collection) } - it{ should_not be_able_to(:update_managers, collection) } - it{ should_not be_able_to(:update_editors, collection) } - it{ should_not be_able_to(:update_depositors, collection) } - it{ should_not be_able_to(:create, collection) } - it{ should_not be_able_to(:update, collection) } - it{ should_not be_able_to(:destroy, collection) } - it{ should_not be_able_to(:update_access_control, collection) } + it{ is_expected.not_to be_able_to(:read, Admin::Collection) } + it{ is_expected.not_to be_able_to(:read, collection) } + it{ is_expected.not_to be_able_to(:update_unit, collection) } + it{ is_expected.not_to be_able_to(:update_managers, collection) } + it{ is_expected.not_to be_able_to(:update_editors, collection) } + it{ is_expected.not_to be_able_to(:update_depositors, collection) } + it{ is_expected.not_to be_able_to(:create, collection) } + it{ is_expected.not_to be_able_to(:update, collection) } + it{ is_expected.not_to be_able_to(:destroy, collection) } + it{ is_expected.not_to be_able_to(:update_access_control, collection) } end end @@ -123,34 +123,35 @@ let(:editor) {FactoryGirl.create(:user)} let(:depositor) {FactoryGirl.create(:user)} - it {should validate_presence_of(:name)} - it {should validate_uniqueness_of(:name)} + it {is_expected.to validate_presence_of(:name)} + it {is_expected.to validate_uniqueness_of(:name)} it "shouldn't complain about partial name matches" do FactoryGirl.create(:collection, name: "This little piggy went to market") expect { FactoryGirl.create(:collection, name: "This little piggy") }.not_to raise_error end - it {should validate_presence_of(:unit)} - it {should ensure_inclusion_of(:unit).in_array(Admin::Collection.units)} + it {is_expected.to validate_presence_of(:unit)} + it {is_expected.to ensure_inclusion_of(:unit).in_array(Admin::Collection.units)} it "should ensure length of :managers is_at_least(1)" - its(:name) {should == "Herman B. Wells Collection"} - its(:unit) {should == "University Archives"} - its(:description) {should == "Collection about our 11th university president, 1938-1962"} - its(:created_at) {should == DateTime.parse(wells_collection.create_date)} - its(:managers) {should == [manager.username]} - its(:editors) {should == [editor.username]} - its(:depositors) {should == [depositor.username]} - - its(:rightsMetadata) {should be_kind_of Hydra::Datastream::RightsMetadata} - its(:inheritedRights) {should be_kind_of Hydra::Datastream::InheritableRightsMetadata} - its(:defaultRights) {should be_kind_of Hydra::Datastream::NonIndexedRightsMetadata} + it "should have attributes" do + expect(subject.name).to eq("Herman B. Wells Collection") + expect(subject.unit).to eq("University Archives") + expect(subject.description).to eq("Collection about our 11th university president, 1938-1962") + expect(subject.created_at).to eq(DateTime.parse(wells_collection.create_date)) + expect(subject.managers).to eq([manager.username]) + expect(subject.editors).to eq([editor.username]) + expect(subject.depositors).to eq([depositor.username]) + expect(subject.rightsMetadata).to be_kind_of Hydra::Datastream::RightsMetadata + expect(subject.inheritedRights).to be_kind_of Hydra::Datastream::InheritableRightsMetadata + expect(subject.defaultRights).to be_kind_of Hydra::Datastream::NonIndexedRightsMetadata + end end describe "Admin::Collection.units" do it "should return an array of units" do - Admin::Collection.stub(:units).and_return ["University Archives", "Black Film Center/Archive"] - Admin::Collection.units.should be_an_instance_of Array - Admin::Collection.units.should == ["University Archives", "Black Film Center/Archive"] + allow(Admin::Collection).to receive(:units).and_return ["University Archives", "Black Film Center/Archive"] + expect(Admin::Collection.units).to be_an_instance_of Array + expect(Admin::Collection.units).to eq(["University Archives", "Black Film Center/Archive"]) end end @@ -158,7 +159,7 @@ it "should solrize important information" do map = Solrizer.default_field_mapper collection.name = "Herman B. Wells Collection" - collection.to_solr[ map.solr_name(:name, :stored_searchable, type: :string) ].should == "Herman B. Wells Collection" + expect(collection.to_solr[ map.solr_name(:name, :stored_searchable, type: :string) ]).to eq("Herman B. Wells Collection") end end @@ -169,75 +170,75 @@ describe "#managers" do it "should return the intersection of edit_users and managers role" do collection.edit_users = [user.username, "pdinh"] - RoleControls.should_receive("users").with("manager").and_return([user.username, "atomical"]) - RoleControls.should_receive("users").with("administrator").and_return([]) - collection.managers.should == [user.username] #collection.edit_users & RoleControls.users("manager") + expect(RoleControls).to receive("users").with("manager").and_return([user.username, "atomical"]) + expect(RoleControls).to receive("users").with("administrator").and_return([]) + expect(collection.managers).to eq([user.username]) #collection.edit_users & RoleControls.users("manager") end end describe "#managers=" do it "should add managers to the collection" do manager_list = [FactoryGirl.create(:manager).username, FactoryGirl.create(:manager).username] collection.managers = manager_list - collection.managers.should == manager_list + expect(collection.managers).to eq(manager_list) end it "should call add_manager" do manager_list = [FactoryGirl.create(:manager).username, FactoryGirl.create(:manager).username] - collection.should_receive("add_manager").with(manager_list[0]) - collection.should_receive("add_manager").with(manager_list[1]) + expect(collection).to receive("add_manager").with(manager_list[0]) + expect(collection).to receive("add_manager").with(manager_list[1]) collection.managers = manager_list end it "should remove managers from the collection" do manager_list = [FactoryGirl.create(:manager).username, FactoryGirl.create(:manager).username] collection.managers = manager_list - collection.managers.should == manager_list + expect(collection.managers).to eq(manager_list) collection.managers -= manager_list - collection.managers.should == [] + expect(collection.managers).to eq([]) end it "should call remove_manager" do collection.managers = [user.username] - collection.should_receive("remove_manager").with(user.username) + expect(collection).to receive("remove_manager").with(user.username) collection.managers = [FactoryGirl.create(:manager).username] end end describe "#add_manager" do it "should give edit access to the collection" do collection.add_manager(user.username) - collection.edit_users.should include(user.username) - collection.inherited_edit_users.should include(user.username) - collection.managers.should include(user.username) + expect(collection.edit_users).to include(user.username) + expect(collection.inherited_edit_users).to include(user.username) + expect(collection.managers).to include(user.username) end it "should add users who have the administrator role" do administrator = FactoryGirl.create(:administrator) collection.add_manager(administrator.username) - collection.edit_users.should include(administrator.username) - collection.inherited_edit_users.should include(administrator.username) - collection.managers.should include(administrator.username) + expect(collection.edit_users).to include(administrator.username) + expect(collection.inherited_edit_users).to include(administrator.username) + expect(collection.managers).to include(administrator.username) end it "should not add administrators to editors role" do administrator = FactoryGirl.create(:administrator) collection.add_manager(administrator.username) - collection.editors.should_not include(administrator.username) + expect(collection.editors).not_to include(administrator.username) end it "should not add users who do not have the manager role" do not_manager = FactoryGirl.create(:user) expect {collection.add_manager(not_manager.username)}.to raise_error(ArgumentError) - collection.managers.should_not include(not_manager.username) + expect(collection.managers).not_to include(not_manager.username) end end describe "#remove_manager" do it "should revoke edit access to the collection" do collection.remove_manager(user.username) - collection.edit_users.should_not include(user.username) - collection.inherited_edit_users.should_not include(user.username) - collection.managers.should_not include(user.username) + expect(collection.edit_users).not_to include(user.username) + expect(collection.inherited_edit_users).not_to include(user.username) + expect(collection.managers).not_to include(user.username) end it "should not remove users who do not have the manager role" do not_manager = FactoryGirl.create(:user) collection.edit_users = [not_manager.username] collection.inherited_edit_users = [not_manager.username] collection.remove_manager(not_manager.username) - collection.edit_users.should include(not_manager.username) - collection.inherited_edit_users.should include(not_manager.username) + expect(collection.edit_users).to include(not_manager.username) + expect(collection.inherited_edit_users).to include(not_manager.username) end end end @@ -249,31 +250,31 @@ describe "#editors" do it "should not return managers" do collection.edit_users = [user.username, FactoryGirl.create(:manager).username] - collection.editors.should == [user.username] + expect(collection.editors).to eq([user.username]) end end describe "#editors=" do it "should add editors to the collection" do editor_list = [FactoryGirl.create(:user).username, FactoryGirl.create(:user).username] collection.editors = editor_list - collection.editors.should == editor_list + expect(collection.editors).to eq(editor_list) end it "should call add_editor" do editor_list = [FactoryGirl.create(:user).username, FactoryGirl.create(:user).username] - collection.should_receive("add_editor").with(editor_list[0]) - collection.should_receive("add_editor").with(editor_list[1]) + expect(collection).to receive("add_editor").with(editor_list[0]) + expect(collection).to receive("add_editor").with(editor_list[1]) collection.editors = editor_list end it "should remove editors from the collection" do name = user.username collection.editors = [name] - collection.editors.should == [name] + expect(collection.editors).to eq([name]) collection.editors -= [name] - collection.editors.should == [] + expect(collection.editors).to eq([]) end it "should call remove_editor" do collection.editors = [user.username] - collection.should_receive("remove_editor").with(user.username) + expect(collection).to receive("remove_editor").with(user.username) collection.editors = [FactoryGirl.create(:user).username] end end @@ -281,26 +282,26 @@ it "should give edit access to the collection" do not_editor = FactoryGirl.create(:user) collection.add_editor(not_editor.username) - collection.edit_users.should include(not_editor.username) - collection.inherited_edit_users.should include(not_editor.username) - collection.editors.should include(not_editor.username) + expect(collection.edit_users).to include(not_editor.username) + expect(collection.inherited_edit_users).to include(not_editor.username) + expect(collection.editors).to include(not_editor.username) end end describe "#remove_editor" do it "should revoke edit access to the collection" do collection.add_editor(user.username) collection.remove_editor(user.username) - collection.edit_users.should_not include(user.username) - collection.inherited_edit_users.should_not include(user.username) - collection.editors.should_not include(user.username) + expect(collection.edit_users).not_to include(user.username) + expect(collection.inherited_edit_users).not_to include(user.username) + expect(collection.editors).not_to include(user.username) end it "should not remove users who do not have the editor role" do not_editor = FactoryGirl.create(:manager) collection.edit_users = [not_editor.username] collection.inherited_edit_users = [not_editor.username] collection.remove_editor(not_editor.username) - collection.edit_users.should include(not_editor.username) - collection.inherited_edit_users.should_not include(user.username) + expect(collection.edit_users).to include(not_editor.username) + expect(collection.inherited_edit_users).not_to include(user.username) end end end @@ -312,31 +313,31 @@ describe "#depositors" do it "should return the read_users" do collection.read_users = [user.username] - collection.depositors.should == [user.username] + expect(collection.depositors).to eq([user.username]) end end describe "#depositors=" do it "should add depositors to the collection" do depositor_list = [FactoryGirl.create(:user).username, FactoryGirl.create(:user).username] collection.depositors = depositor_list - collection.depositors.should == depositor_list + expect(collection.depositors).to eq(depositor_list) end it "should call add_depositor" do depositor_list = [FactoryGirl.create(:user).username, FactoryGirl.create(:user).username] - collection.should_receive("add_depositor").with(depositor_list[0]) - collection.should_receive("add_depositor").with(depositor_list[1]) + expect(collection).to receive("add_depositor").with(depositor_list[0]) + expect(collection).to receive("add_depositor").with(depositor_list[1]) collection.depositors = depositor_list end it "should remove depositors from the collection" do name = user.username collection.depositors = [name] - collection.depositors.should == [name] + expect(collection.depositors).to eq([name]) collection.depositors -= [name] - collection.depositors.should == [] + expect(collection.depositors).to eq([]) end it "should call remove_depositor" do collection.add_depositor(user.username) - collection.should_receive("remove_depositor").with(user.username) + expect(collection).to receive("remove_depositor").with(user.username) collection.depositors = [FactoryGirl.create(:user).username] end end @@ -344,22 +345,22 @@ it "should give edit access to the collection" do not_depositor = FactoryGirl.create(:user) collection.add_depositor(not_depositor.username) - collection.inherited_edit_users.should include(not_depositor.username) - collection.depositors.should include(not_depositor.username) + expect(collection.inherited_edit_users).to include(not_depositor.username) + expect(collection.depositors).to include(not_depositor.username) end end describe "#remove_depositor" do it "should revoke edit access to the collection" do collection.add_depositor(user.username) collection.remove_depositor(user.username) - collection.inherited_edit_users.should_not include(user.username) - collection.depositors.should_not include(user.username) + expect(collection.inherited_edit_users).not_to include(user.username) + expect(collection.depositors).not_to include(user.username) end it "should not remove users who do not have the depositor role" do not_depositor = FactoryGirl.create(:manager) collection.inherited_edit_users = [not_depositor.username] collection.remove_depositor(not_depositor.username) - collection.inherited_edit_users.should include(not_depositor.username) + expect(collection.inherited_edit_users).to include(not_depositor.username) end end end @@ -383,15 +384,15 @@ end it 'sets the new collection on media_object' do - @media_objects.each{|m| m.collection.should eql @target_collection } + @media_objects.each{|m| expect(m.collection).to eql @target_collection } end it 'removes the media object from the source collection' do - @source_collection.media_objects.should eq [] + expect(@source_collection.media_objects).to eq [] end it 'adds the media object to the target collection' do - @target_collection.media_objects.should eq @media_objects + expect(@target_collection.media_objects).to eq @media_objects end end @@ -407,13 +408,13 @@ end it "should persist assigned #default_read_users" do - Admin::Collection.find(collection.pid).default_read_users.should == users + expect(Admin::Collection.find(collection.pid).default_read_users).to eq(users) end it "should persist empty #default_read_users" do collection.default_read_users = [] collection.save - Admin::Collection.find(collection.pid).default_read_users.should == [] + expect(Admin::Collection.find(collection.pid).default_read_users).to eq([]) end end @@ -426,13 +427,13 @@ end it "should persist assigned #default_read_groups" do - Admin::Collection.find(collection.pid).default_read_groups.should == groups + expect(Admin::Collection.find(collection.pid).default_read_groups).to eq(groups) end it "should persist empty #default_read_groups" do collection.default_read_groups = [] collection.save - Admin::Collection.find(collection.pid).default_read_groups.should == [] + expect(Admin::Collection.find(collection.pid).default_read_groups).to eq([]) end end end @@ -442,23 +443,23 @@ let!(:collection) {FactoryGirl.create(:collection)} it 'should call reindex_members if name has changed' do collection.name = "New name" - collection.should be_name_changed - collection.should_receive("reindex_members").and_return(nil) + expect(collection).to be_name_changed + expect(collection).to receive("reindex_members").and_return(nil) collection.save end it 'should call reindex_members if unit has changed' do collection.unit = Admin::Collection.units.last - collection.should be_unit_changed - collection.should_receive("reindex_members").and_return(nil) + expect(collection).to be_unit_changed + expect(collection).to receive("reindex_members").and_return(nil) collection.save end it 'should not call reindex_members if name or unit has not been changed' do collection.description = "A different description" - collection.should_not be_name_changed - collection.should_not be_unit_changed - collection.should_not_receive("reindex_members") + expect(collection).not_to be_name_changed + expect(collection).not_to be_unit_changed + expect(collection).not_to receive("reindex_members") collection.save end end @@ -466,8 +467,7 @@ describe "reindex_members" do before do - @media_objects = (1..3).map{ FactoryGirl.create(:media_object)} - @collection = FactoryGirl.create(:collection, media_objects: @media_objects) + @collection = FactoryGirl.create(:collection, items: 3) allow(Admin::Collection).to receive(:find).with(@collection.pid).and_return(@collection) end it 'should reindex in the background' do @@ -475,7 +475,7 @@ end it 'should call update_index on all member objects' do Delayed::Worker.delay_jobs = false - @media_objects.each {|mo| mo.should_receive("update_index").and_return(true)} + @collection.media_objects.each {|mo| expect(mo).to receive("update_index").and_return(true)} @collection.reindex_members {} Delayed::Worker.delay_jobs = true end @@ -486,22 +486,22 @@ it 'removes bad characters from collection name' do collection.name = '../../secret.rb' - Dir.should_receive(:mkdir).with( File.join(Avalon::Configuration.lookup('dropbox.path'), '______secret_rb') ) - Dir.stub(:mkdir) # stubbing this out in a before(:each) block will effect where mkdir is used elsewhere (i.e. factories) + expect(Dir).to receive(:mkdir).with( File.join(Avalon::Configuration.lookup('dropbox.path'), '______secret_rb') ) + allow(Dir).to receive(:mkdir) # stubbing this out in a before(:each) block will effect where mkdir is used elsewhere (i.e. factories) collection.send(:create_dropbox_directory!) end it 'sets dropbox_directory_name on collection' do collection.name = 'african art' - Dir.stub(:mkdir) + allow(Dir).to receive(:mkdir) collection.send(:create_dropbox_directory!) - collection.dropbox_directory_name.should == 'african_art' + expect(collection.dropbox_directory_name).to eq('african_art') end it 'uses a different directory name if the directory exists' do collection.name = 'african art' FakeFS.activate! FileUtils.mkdir_p(File.join(Avalon::Configuration.lookup('dropbox.path'), 'african_art')) FileUtils.mkdir_p(File.join(Avalon::Configuration.lookup('dropbox.path'), 'african_art_2')) - Dir.should_receive(:mkdir).with(File.join(Avalon::Configuration.lookup('dropbox.path'), 'african_art_3')) + expect(Dir).to receive(:mkdir).with(File.join(Avalon::Configuration.lookup('dropbox.path'), 'african_art_3')) collection.send(:create_dropbox_directory!) FakeFS.deactivate! end @@ -514,8 +514,8 @@ it 'handles Unicode collection names correctly' do collection.name = collection_name - Dir.should_receive(:mkdir).with( File.join(Avalon::Configuration.lookup('dropbox.path'), collection_dir) ) - Dir.stub(:mkdir) + expect(Dir).to receive(:mkdir).with( File.join(Avalon::Configuration.lookup('dropbox.path'), collection_dir) ) + allow(Dir).to receive(:mkdir) collection.send(:create_dropbox_directory!) end end diff --git a/spec/models/comments_spec.rb b/spec/models/comments_spec.rb index bd90f75064..782b724d58 100644 --- a/spec/models/comments_spec.rb +++ b/spec/models/comments_spec.rb @@ -25,36 +25,36 @@ end it "should validate if all fields are entered correctly" do - @comment_test.should be_valid + expect(@comment_test).to be_valid end it "should fail if the name is missing" do @comment_test.name = nil - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end describe "Subject" do it "should fail if there is no subject" do @comment_test.subject = nil - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end it "should fail if the subject is not in the list" do @comment_test.subject = 'Not in the list' - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end end describe "Comments" do it "should fail if there is no comment" do @comment_test.comment = nil - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end it "should strip out any unsafe HTML" do @comment_test.comment = "

    But this is safe

    " - @comment_test.comment.should_not match /\.*\<\\script\>/ + expect(@comment_test.comment).not_to match /\.*\<\\script\>/ end end @@ -62,23 +62,23 @@ it "should warn if the addresses do not match" do @comment_test.email = "email_one@example.com" @comment_test.email_confirmation = "email_two@example.com" - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end it "should warn if an address is invalid" do @comment_test.email = "nosuchemail@" - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end it "should have matching email addresses" do - @comment_test.should be_valid + expect(@comment_test).to be_valid end end describe "Captcha" do it "should fail if a captcha value is entered" do @comment_test.nickname = 'Not empty' - @comment_test.should_not be_valid + expect(@comment_test).not_to be_valid end end end diff --git a/spec/models/concerns/hidden_spec.rb b/spec/models/concerns/hidden_spec.rb index bb528edff8..095ae2c0ab 100644 --- a/spec/models/concerns/hidden_spec.rb +++ b/spec/models/concerns/hidden_spec.rb @@ -27,14 +27,14 @@ class Foo < ActiveFedora::Base describe "hidden" do it "should default to discoverable" do - subject.hidden?.should be false - subject.to_solr["hidden_bsi"].should be false + expect(subject.hidden?).to be false + expect(subject.to_solr["hidden_bsi"]).to be false end it "should set hidden?" do subject.hidden = true - subject.hidden?.should be true - subject.to_solr["hidden_bsi"].should be_truthy + expect(subject.hidden?).to be true + expect(subject.to_solr["hidden_bsi"]).to be_truthy end end end diff --git a/spec/models/concerns/permalink_spec.rb b/spec/models/concerns/permalink_spec.rb index f7817cfa1f..9080ed89f0 100644 --- a/spec/models/concerns/permalink_spec.rb +++ b/spec/models/concerns/permalink_spec.rb @@ -22,11 +22,11 @@ context 'permalink does not exist' do it 'returns nil' do - master_file.permalink.should be_nil + expect(master_file.permalink).to be_nil end it 'returns nil with query variables' do - master_file.permalink({a:'b'}).should be_nil + expect(master_file.permalink({a:'b'})).to be_nil end end @@ -39,12 +39,12 @@ end it 'returns a string' do - master_file.permalink.should be_kind_of(String) + expect(master_file.permalink).to be_kind_of(String) end it 'appends query variables to the url' do query_var_hash = { urlappend: '/embed' } - master_file.permalink(query_var_hash).should == "#{permalink_url}?#{query_var_hash.to_query}" + expect(master_file.permalink(query_var_hash)).to eq("#{permalink_url}?#{query_var_hash.to_query}") end end diff --git a/spec/models/concerns/virtual_groups_spec.rb b/spec/models/concerns/virtual_groups_spec.rb index 4f5ee9d764..6671a017d8 100644 --- a/spec/models/concerns/virtual_groups_spec.rb +++ b/spec/models/concerns/virtual_groups_spec.rb @@ -28,8 +28,9 @@ class Foo < ActiveFedora::Base describe 'virtual groups' do let!(:local_groups) {[FactoryGirl.create(:group).name, FactoryGirl.create(:group).name]} let(:virtual_groups) {["vgroup1", "vgroup2"]} + let(:ip_groups) {[Faker::Internet.ip_v4_address, Faker::Internet.ip_v6_address, Faker::Internet.ip_v4_address + "/24"]} before(:each) do - subject.read_groups = local_groups + virtual_groups + subject.read_groups = local_groups + virtual_groups + ip_groups end describe '#local_group_exceptions' do @@ -43,5 +44,11 @@ class Foo < ActiveFedora::Base expect(subject.virtual_read_groups).to eq(virtual_groups) end end + + describe '#ip_group_exceptions' do + it 'should have only ip groups' do + expect(subject.ip_read_groups).to eq(ip_groups) + end + end end end diff --git a/spec/models/derivative_spec.rb b/spec/models/derivative_spec.rb index 71b03790d9..ed02bcf94f 100644 --- a/spec/models/derivative_spec.rb +++ b/spec/models/derivative_spec.rb @@ -16,6 +16,26 @@ describe Derivative do + describe "#from_output" do + let!(:api_output){[{ label: 'quality-high', + id: 'track-1', + url: 'rtmp://www.test.com/test.mp4', + hls_url: 'http://www.test.com/test.mp4.m3u8', + duration: "6315", + mime_type: "video/mp4", + audio_bitrate: "127716.0", + audio_codec: "AAC", + video_bitrate: "1000000.0", + video_codec: "AVC", + width: "640", + height: "480" }]} + it "Call from ingest API should populate :url and :hls_url" do + d = Derivative.from_output(api_output,false) + expect(d.location_url).to eq('rtmp://www.test.com/test.mp4') + expect(d.hls_url).to eq('http://www.test.com/test.mp4.m3u8') + end + end + describe "#destroy" do let!(:derivative) {FactoryGirl.create(:derivative)} @@ -54,25 +74,25 @@ it "RTMP video" do derivative.encoding.video = 'true' derivative.absolute_location = location - derivative.streaming_url(false).should == "#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content" + expect(derivative.streaming_url(false)).to eq("#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content") end it "RTMP audio" do derivative.encoding.audio = 'true' derivative.absolute_location = location - derivative.streaming_url(false).should == "#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content" + expect(derivative.streaming_url(false)).to eq("#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content") end it "HTTP video" do derivative.encoding.video = 'true' derivative.absolute_location = location - derivative.streaming_url(true).should == "#{http_base}/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8" + expect(derivative.streaming_url(true)).to eq("#{http_base}/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8") end it "HTTP audio" do derivative.encoding.audio = 'true' derivative.absolute_location = location - derivative.streaming_url(true).should == "#{http_base}/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8" + expect(derivative.streaming_url(true)).to eq("#{http_base}/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8") end end @@ -84,25 +104,25 @@ it "RTMP video" do derivative.encoding.video = 'true' derivative.absolute_location = location - derivative.streaming_url(false).should == "#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content" + expect(derivative.streaming_url(false)).to eq("#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content") end it "RTMP audio" do derivative.encoding.audio = 'true' derivative.absolute_location = location - derivative.streaming_url(false).should == "#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content" + expect(derivative.streaming_url(false)).to eq("#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content") end it "HTTP video" do derivative.encoding.video = 'true' derivative.absolute_location = location - derivative.streaming_url(true).should == "#{http_base}/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8" + expect(derivative.streaming_url(true)).to eq("#{http_base}/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8") end it "HTTP audio" do derivative.encoding.audio = 'true' derivative.absolute_location = location - derivative.streaming_url(true).should == "#{http_base}/audio-only/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8" + expect(derivative.streaming_url(true)).to eq("#{http_base}/audio-only/c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4.m3u8") end end @@ -114,25 +134,25 @@ it "RTMP video" do derivative.encoding.video = 'true' derivative.absolute_location = location - derivative.streaming_url(false).should == "#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content" + expect(derivative.streaming_url(false)).to eq("#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content") end it "RTMP audio" do derivative.encoding.audio = 'true' derivative.absolute_location = location - derivative.streaming_url(false).should == "#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content" + expect(derivative.streaming_url(false)).to eq("#{rtmp_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content") end it "HTTP video" do derivative.encoding.video = 'true' derivative.absolute_location = location - derivative.streaming_url(true).should == "#{http_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4/playlist.m3u8" + expect(derivative.streaming_url(true)).to eq("#{http_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4/playlist.m3u8") end it "HTTP audio" do derivative.encoding.audio = 'true' derivative.absolute_location = location - derivative.streaming_url(true).should == "#{http_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4/playlist.m3u8" + expect(derivative.streaming_url(true)).to eq("#{http_base}/mp4:c5e0f8b8-3f69-40de-9524-604f03b5f867/8c871d4b-a9a6-4841-8e2a-dd98cf2ee625/content.mp4/playlist.m3u8") end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 79da1b3ebb..ea965f89b9 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -17,18 +17,18 @@ describe Admin::Group do describe "name" do - it { should validate_presence_of( :name )} - it { should_not allow_value( 'group.01' ).for( :name )} - it { should_not allow_value( 'group;01' ).for( :name )} - it { should_not allow_value( 'group%01' ).for( :name )} - it { should_not allow_value( 'group/01' ).for( :name )} + it { is_expected.to validate_presence_of( :name )} + it { is_expected.not_to allow_value( 'group.01' ).for( :name )} + it { is_expected.not_to allow_value( 'group;01' ).for( :name )} + it { is_expected.not_to allow_value( 'group%01' ).for( :name )} + it { is_expected.not_to allow_value( 'group/01' ).for( :name )} end describe "non system groups" do it "should not have system groups" do groups = Admin::Group.non_system_groups system_groups = Avalon::Configuration.lookup('groups.system_groups') - groups.each { |g| system_groups.should_not include g.name } + groups.each { |g| expect(system_groups).not_to include g.name } end end diff --git a/spec/models/ingest_batch_spec.rb b/spec/models/ingest_batch_spec.rb index f8a17310de..b110811462 100644 --- a/spec/models/ingest_batch_spec.rb +++ b/spec/models/ingest_batch_spec.rb @@ -19,13 +19,13 @@ media_object_ids = ['first-item', 'second-item', 'third-item'] ingest_batch = IngestBatch.create(media_object_ids: media_object_ids) ingest_batch.reload - ingest_batch.media_object_ids.should == media_object_ids + expect(ingest_batch.media_object_ids).to eq(media_object_ids) end describe '#finished?' do it 'returns true when all the master files are finished' do media_object = FactoryGirl.create(:media_object, parts: [FactoryGirl.create(:master_file, status_code: 'STOPPED'), FactoryGirl.create(:master_file, status_code: 'SUCCEEDED')]) ingest_batch = IngestBatch.new(media_object_ids: [media_object.id], email: 'email@something.com') - ingest_batch.finished?.should be true + expect(ingest_batch.finished?).to be true end # fix: adding master_files to media object parts is broken @@ -37,14 +37,14 @@ media_object.parts << MasterFile.create(status_code: 'RUNNING') media_object.save(validate: false) ingest_batch = IngestBatch.new(media_object_ids: ['avalon:ingest-batch-test'], email: 'email@something.com') - ingest_batch.finished?.should be true + expect(ingest_batch.finished?).to be true end end describe '#media_objects' do it 'returns an empty array if media_object_ids is nil' do n = IngestBatch.new - n.media_objects.should == [] + expect(n.media_objects).to eq([]) end it 'returns an array of media objects based on ids passed in' do @@ -54,7 +54,7 @@ media_object } ingest_batch = IngestBatch.new(media_object_ids:media_objects.map(&:id)) - ingest_batch.media_objects.should == media_objects + expect(ingest_batch.media_objects).to eq(media_objects) end end @@ -62,11 +62,11 @@ let(:ingest_batch){ IngestBatch.new } it 'returns true if email_sent is true' do ingest_batch.email_sent = true - ingest_batch.email_sent?.should be true + expect(ingest_batch.email_sent?).to be true end it 'returns false if email_sent is false' do ingest_batch.email_sent = false - ingest_batch.email_sent?.should be false + expect(ingest_batch.email_sent?).to be false end end end diff --git a/spec/models/lease_spec.rb b/spec/models/lease_spec.rb new file mode 100644 index 0000000000..3b6ef60e5a --- /dev/null +++ b/spec/models/lease_spec.rb @@ -0,0 +1,187 @@ +# Copyright 2011-2015, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'spec_helper' + +describe Lease do + let!(:lease) {FactoryGirl.create(:lease)} + + describe 'Valid Lease Term' do + it 'should return true when the date is between the lease periods' do + expect(lease.lease_is_active?).to be_truthy + end + it 'should return false when today is prior to the lease beginning' do + allow(Date).to receive(:today).and_return(Date.parse('11 November 1984')) + expect(lease.lease_is_active?).to be_falsey + end + it 'should return false when today is after the expiration of the lease' do + allow(Date).to receive(:today).and_return(Date.parse('11 November 9999')) + expect(lease.lease_is_active?).to be_falsey + end + end + describe 'time input formats' do + before :each do + @lease = Lease.new + @lease.begin_time = Date.yesterday + end + it 'accepts a Time object' do + @lease.end_time = Time.now + expect { @lease.save }.not_to raise_error + end + it 'accepts a Date object' do + @lease.end_time = Date.today + expect { @lease.save }.not_to raise_error + end + it 'accepts a DateTime object' do + @lease.end_time = DateTime.now + expect { @lease.save }.not_to raise_error + end + it 'accepts a String represenation of a date' do + @lease.begin_time = '1 January 2010' + @lease.end_time = '1 January 2011' + expect { @lease.save }.not_to raise_error + end + end + describe 'formating times' do + it 'stores the begin and end times as DateTimes' do + expect(lease.begin_time.class).to eq(DateTime) + expect(lease.end_time.class).to eq(DateTime) + end + + it 'sets begin_time to the start of the day' do + expect(lease.begin_time).to eq(Time.now.utc.beginning_of_day - 1.day) + end + + it 'sets end_time to the end of the day' do + expect(lease.end_time).to eq(Time.now.utc.end_of_day + 1.day) + end + + describe 'start of day' do + it 'returns the start of the day when passed in a day' do + expect(lease.start_of_day(DateTime.now)).to eq(DateTime.now.utc.beginning_of_day.iso8601) + end + it 'returns the start of the day as a String' do + expect(lease.start_of_day(DateTime.now).class).to eq(String) + end + end + + describe 'end of day' do + it 'returns the start of the day when passed in a day' do + expect(lease.end_of_day(DateTime.now)).to eq(DateTime.now.utc.end_of_day.iso8601) + end + it 'returns the start of the day as a String' do + expect(lease.end_of_day(DateTime.now).class).to eq(String) + end + end + end + describe 'setting the begin_time' do + before :each do + @lease = Lease.new + @lease.end_time = Date.tomorrow + end + it 'sets the begin_time to today if it is nil' do + @lease.apply_default_begin_time + expect(@lease.begin_time).to eq(Time.now.utc.beginning_of_day) + end + it 'does not set the begin_time today is one is provided' do + @lease.begin_time = DateTime.parse(Date.yesterday.to_s) + @lease.apply_default_begin_time + expect(@lease.begin_time.iso8601).to match(DateTime.parse(Date.yesterday.to_s).beginning_of_day.iso8601) + end + end + describe 'end_time validation' do + before :each do + @lease = Lease.new + @lease.begin_time = Date.yesterday + end + it 'raises an ArgumentError if end_time is not set' do + expect { @lease.ensure_end_time_present }.to raise_error(ArgumentError) + expect { @lease.save }.to raise_error(ArgumentError) + end + + it 'does not raise an ArgumentError if end_time is set' do + @lease.end_time = Date.tomorrow + expect { @lease.ensure_end_time_present }.to_not raise_error + expect { @lease.save }.to_not raise_error + end + end + describe 'time range validation' do + before :each do + @lease = Lease.new + end + it 'raises an ArgumentError if end_time preceeds begin_time' do + @lease.end_time = Date.yesterday + @lease.begin_time = Date.tomorrow + expect { @lease.validate_dates }.to raise_error(ArgumentError) + expect { @lease.save }.to raise_error(ArgumentError) + end + it 'raises an ArgumentError if the end_time equals the begin_time' do + now = DateTime.now + @lease.end_time = now + @lease.begin_time = now + expect { @lease.validate_dates }.to raise_error(ArgumentError) + end + it 'does not raise an ArgumentError if the end_time is after the begin_time' do + @lease.end_time = Date.tomorrow + @lease.begin_time = Date.yesterday + expect { @lease.validate_dates }.not_to raise_error + expect { @lease.save }.not_to raise_error + end + end + describe 'solrizing and saving' do + before :all do + @begin_time_field = 'begin_time_dti' + @end_time_field = 'end_time_dti' + @deleted_fields = %w('begin_time_dtsim', 'end_time_dtsim') + @lenght_of_a_iso8601_time = 20 + end + it 'can save a lease' do + expect { lease.save }.not_to raise_error + end + it 'can generate a hash of the lease' do + expect(lease.to_solr.class).to eq(Hash) + end + it 'removes multi valued date fields' do + solr_hash = lease.to_solr + @deleted_fields.each do |field| + expect(solr_hash.keys.include? field).to be_falsey + end + end + it 'adds and populates the appriorate dti fields' do + solr_hash = lease.to_solr + expect(solr_hash.keys.include? @begin_time_field).to be_truthy + expect(solr_hash[@begin_time_field].size).to eq(@lenght_of_a_iso8601_time) + expect(solr_hash.keys.include? @end_time_field).to be_truthy + expect(solr_hash[@begin_time_field].size).to eq(@lenght_of_a_iso8601_time) + end + end + describe '#lease_type' do + it 'identifies user lease_type' do + lease.read_users = [Faker::Internet.email] + expect(lease.lease_type).to eq "user" + end + it 'identifies group lease_type' do + lease.read_groups = [FactoryGirl.create(:group).name] + expect(lease.lease_type).to eq "local" + end + it 'identifies external_group lease_type' do + lease.read_groups = ["ExternalGroup"] + expect(lease.lease_type).to eq "external" + end + it 'identifies ip lease_type' do + lease.read_groups = [Faker::Internet.ip_v4_address] + expect(lease.lease_type).to eq "ip" + end + end +end diff --git a/spec/models/master_file_spec.rb b/spec/models/master_file_spec.rb index 2731076ba8..f4dc7261b5 100644 --- a/spec/models/master_file_spec.rb +++ b/spec/models/master_file_spec.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -18,15 +18,17 @@ describe "validations" do subject {MasterFile.new} - it {should validate_presence_of(:workflow_name)} - it {should validate_inclusion_of(:workflow_name).in_array(MasterFile::WORKFLOWS)} - xit {should validate_presence_of(:file_format)} - xit {should validate_exclusion_of(:file_format).in_array(['Unknown']).with_message("The file was not recognized as audio or video.")} + it {is_expected.to validate_presence_of(:workflow_name)} + it {is_expected.to validate_inclusion_of(:workflow_name).in_array(MasterFile::WORKFLOWS)} + xit {is_expected.to validate_presence_of(:file_format)} + xit {is_expected.to validate_exclusion_of(:file_format).in_array(['Unknown']).with_message("The file was not recognized as audio or video.")} + it {is_expected.to validate_inclusion_of(:date_digitized).in_array([nil, '2016-04-07T15:05:01-05:00', '2016-04-07'])} + it {is_expected.to validate_exclusion_of(:date_digitized).in_array(["","2016-14-99","Blergh"]).with_message(//)} end describe "locations" do - subject { - mf = MasterFile.new + subject { + mf = MasterFile.new mf.file_location = '/foo/bar/baz/quux.mp4' mf.save mf @@ -38,21 +40,21 @@ end it "should know where its (Samba remote) masterfile is" do - Avalon::FileResolver.any_instance.stub(:mounts) { + allow_any_instance_of(Avalon::FileResolver).to receive(:mounts) { ["//user@some.server.at.an.example.edu/stuff on /foo/bar (smbfs, nodev, nosuid, mounted by user)"] } expect(subject.absolute_location).to eq 'smb://some.server.at.an.example.edu/stuff/baz/quux.mp4' end it "should know where its (CIFS remote) masterfile is" do - Avalon::FileResolver.any_instance.stub(:mounts) { + allow_any_instance_of(Avalon::FileResolver).to receive(:mounts) { ["//user@some.server.at.an.example.edu/stuff on /foo/bar (cifs, nodev, nosuid, mounted by user)"] } expect(subject.absolute_location).to eq 'cifs://some.server.at.an.example.edu/stuff/baz/quux.mp4' end it "should know where its (NFS remote) masterfile is" do - Avalon::FileResolver.any_instance.stub(:mounts) { + allow_any_instance_of(Avalon::FileResolver).to receive(:mounts) { ["some.server.at.an.example.edu:/stuff on /foo/bar (nfs, nodev, nosuid, mounted by user)"] } expect(subject.absolute_location).to eq 'nfs://some.server.at.an.example.edu/stuff/baz/quux.mp4' @@ -64,7 +66,7 @@ end it "should accept configurable overrides" do - Avalon::FileResolver.any_instance.stub(:overrides) { + allow_any_instance_of(Avalon::FileResolver).to receive(:overrides) { { '/foo/bar/' => 'http://repository.example.edu/foothings/' } } expect(subject.absolute_location).to eq 'http://repository.example.edu/foothings/baz/quux.mp4' @@ -85,12 +87,12 @@ let(:derivative) {Derivative.create} let(:master_file) {FactoryGirl.create(:master_file)} it "should set hasDerivation relationships on self" do - master_file.relationships(:is_derivation_of).size.should == 0 + expect(master_file.relationships(:is_derivation_of).size).to eq(0) master_file.derivatives += [derivative] - derivative.relationships(:is_derivation_of).size.should == 1 - derivative.relationships(:is_derivation_of).first.should == master_file.internal_uri + expect(derivative.relationships(:is_derivation_of).size).to eq(1) + expect(derivative.relationships(:is_derivation_of).first).to eq(master_file.internal_uri) end end @@ -99,15 +101,15 @@ let(:master_file){ MasterFile.new } it 'returns true for stopped' do master_file.status_code = 'CANCELLED' - master_file.finished_processing?.should be true + expect(master_file.finished_processing?).to be true end it 'returns true for succeeded' do master_file.status_code = 'COMPLETED' - master_file.finished_processing?.should be true + expect(master_file.finished_processing?).to be true end it 'returns true for failed' do master_file.status_code = 'FAILED' - master_file.finished_processing?.should be true + expect(master_file.finished_processing?).to be true end end end @@ -177,21 +179,21 @@ it "should accept a value" do offset = master_file.duration.to_i / 2 master_file.poster_offset = offset - master_file.poster_offset.should == offset.to_s - master_file.should be_valid + expect(master_file.poster_offset).to eq(offset.to_s) + expect(master_file).to be_valid end it "should complain if value < 0" do master_file.poster_offset = -1 - master_file.should_not be_valid - master_file.errors[:poster_offset].first.should == "must be between 0 and #{master_file.duration}" + expect(master_file).not_to be_valid + expect(master_file.errors[:poster_offset].first).to eq("must be between 0 and #{master_file.duration}") end it "should complain if value > duration" do offset = master_file.duration.to_i + rand(32514) + 500 master_file.poster_offset = offset - master_file.should_not be_valid - master_file.errors[:poster_offset].first.should == "must be between 0 and #{master_file.duration}" + expect(master_file).not_to be_valid + expect(master_file.errors[:poster_offset].first).to eq("must be between 0 and #{master_file.duration}") end end @@ -199,21 +201,21 @@ it "should accept a value" do offset = master_file.duration.to_i / 2 master_file.poster_offset = offset.to_hms - master_file.poster_offset.should == offset.to_s - master_file.should be_valid + expect(master_file.poster_offset).to eq(offset.to_s) + expect(master_file).to be_valid end it "should complain if value > duration" do offset = master_file.duration.to_i + rand(32514) + 500 master_file.poster_offset = offset.to_hms - master_file.should_not be_valid - master_file.errors[:poster_offset].first.should == "must be between 0 and #{master_file.duration}" + expect(master_file).not_to be_valid + expect(master_file.errors[:poster_offset].first).to eq("must be between 0 and #{master_file.duration}") end end describe "update images" do it "should update on save" do - MasterFile.should_receive(:extract_still).with(master_file.pid,{type:'both',offset:'12345'}) + expect(MasterFile).to receive(:extract_still).with(master_file.pid,{type:'both',offset:'12345'}) master_file.poster_offset = 12345 master_file.save end @@ -228,12 +230,12 @@ it "should not use the skipped transcoding workflow" do master_file.file_format = 'Moving image' master_file.set_workflow - master_file.workflow_name.should == 'avalon' - end + expect(master_file.workflow_name).to eq('avalon') + end it "should use the skipped transcoding workflow for video" do master_file.file_format = 'Moving image' master_file.set_workflow('skip_transcoding') - master_file.workflow_name.should == 'avalon-skip-transcoding' + expect(master_file.workflow_name).to eq('avalon-skip-transcoding') end end @@ -241,12 +243,12 @@ it "should not use the skipped transcoding workflow" do master_file.file_format = 'Sound' master_file.set_workflow - master_file.workflow_name.should == 'fullaudio' - end + expect(master_file.workflow_name).to eq('fullaudio') + end it "should use the skipped transcoding workflow for video" do master_file.file_format = 'Sound' master_file.set_workflow('skip_transcoding') - master_file.workflow_name.should == 'avalon-skip-transcoding-audio' + expect(master_file.workflow_name).to eq('avalon-skip-transcoding-audio') end end end @@ -254,21 +256,21 @@ it "should use the avalon workflow" do master_file.file_format = 'Moving image' master_file.set_workflow - master_file.workflow_name.should == 'avalon' - end + expect(master_file.workflow_name).to eq('avalon') + end end describe "audio" do it "should use the fullaudio workflow" do master_file.file_format = 'Sound' master_file.set_workflow - master_file.workflow_name.should == 'fullaudio' + expect(master_file.workflow_name).to eq('fullaudio') end end describe "unknown format" do it "should set workflow_name to nil" do master_file.file_format = 'Unknown' master_file.set_workflow - master_file.workflow_name.should == nil + expect(master_file.workflow_name).to eq(nil) end end end @@ -284,16 +286,16 @@ it "it should set the correct file location and size" do masterfile = FactoryGirl.create(:master_file) masterfile.setContent(derivative_hash) - masterfile.file_location.should == filename_high - masterfile.file_size.should == "199160" + expect(masterfile.file_location).to eq(filename_high) + expect(masterfile.file_size).to eq("199160") end end describe "quality-high does not exist" do it "should set the correct file location and size" do masterfile = FactoryGirl.create(:master_file) masterfile.setContent(derivative_hash.except("quality-high")) - masterfile.file_location.should == filename_medium - masterfile.file_size.should == "199160" + expect(masterfile.file_location).to eq(filename_medium) + expect(masterfile.file_size).to eq("199160") end end @@ -307,32 +309,32 @@ let(:tempfile) { File.join(tempdir, 'RackMultipart20130816-2519-y2wzc7') } let(:media_path) { File.expand_path("../../masterfiles-#{SecureRandom.uuid}",__FILE__)} let(:upload) { ActionDispatch::Http::UploadedFile.new :tempfile => File.open(tempfile), :filename => original, :type => 'video/mp4' } - subject { + subject { mf = MasterFile.new mf.setContent(upload) mf } - + before(:each) do @old_media_path = Avalon::Configuration.lookup('matterhorn.media_path') FileUtils.mkdir_p media_path FileUtils.cp fixture, tempfile end - + after(:each) do Avalon::Configuration['matterhorn']['media_path'] = @old_media_path File.unlink subject.file_location FileUtils.rm_rf media_path end - + it "should rename an uploaded file in place" do Avalon::Configuration['matterhorn'].delete('media_path') - subject.file_location.should == File.join(tempdir,original) + expect(subject.file_location).to eq(File.join(tempdir,original)) end - + it "should copy an uploaded file to the media path" do Avalon::Configuration['matterhorn']['media_path'] = media_path - subject.file_location.should == File.join(media_path,original) + expect(subject.file_location).to eq(File.join(media_path,original)) end end end @@ -340,37 +342,37 @@ describe "#encoder_class" do subject { FactoryGirl.create(:master_file) } - + before :all do class WorkflowEncoder < ActiveEncode::Base end - + module EncoderModule class MyEncoder < ActiveEncode::Base end end end - + after :all do EncoderModule.send(:remove_const, :MyEncoder) Object.send(:remove_const, :EncoderModule) Object.send(:remove_const, :WorkflowEncoder) end - + it "should default to ActiveEncode::Base" do expect(subject.encoder_class).to eq(ActiveEncode::Base) end - + it "should infer the class from a workflow name" do subject.workflow_name = 'workflow_encoder' expect(subject.encoder_class).to eq(WorkflowEncoder) end - + it "should fall back to ActiveEncode::Base when a workflow class can't be resolved" do subject.workflow_name = 'nonexistent_workflow_encoder' expect(subject.encoder_class).to eq(ActiveEncode::Base) end - + it "should resolve an explicitly named encoder class" do subject.encoder_classname = 'EncoderModule::MyEncoder' expect(subject.encoder_class).to eq(EncoderModule::MyEncoder) @@ -380,12 +382,12 @@ class MyEncoder < ActiveEncode::Base subject.encoder_classname = 'EncoderModule::NonexistentEncoder' expect(subject.encoder_class).to eq(ActiveEncode::Base) end - + it "should correctly set the encoder classname from the encoder" do subject.encoder_class = EncoderModule::MyEncoder expect(subject.encoder_classname).to eq('EncoderModule::MyEncoder') end - + it "should reject an invalid encoder class" do expect { subject.encoder_class = Object }.to raise_error(ArgumentError) end @@ -414,4 +416,46 @@ class MyEncoder < ActiveEncode::Base expect(ingest_batch.reload.email_sent?).to be true end end + + describe '#update_progress_on_success!' do + subject(:master_file) { FactoryGirl.create(:master_file) } + let(:encode) { double("encode", :output => []) } + + it 'should set the digitized date' do + master_file.update_progress_on_success!(encode) + master_file.reload + expect(master_file.date_digitized).to_not be_empty + end + + end + + describe "#structural_metadata_labels" do + subject(:master_file) { FactoryGirl.create(:master_file_with_structure) } + it 'should return correct list of labels' do + expect(master_file.structural_metadata_labels.first).to eq 'CD 1' + end + end + + describe 'rdf formatted information' do + subject(:video_master_file) { FactoryGirl.create(:master_file) } + subject(:sound_master_file) { FactoryGirl.create(:master_file_sound) } + describe 'type' do + it 'returns dctypes:MovingImage when the file is a video' do + expect(video_master_file.rdf_type).to match('dctypes:MovingImage') + end + it 'return dctypes:Sound when the file is audio' do + expect(sound_master_file.rdf_type).to match('dctypes:Sound') + end + end + describe 'uri' do + it 'returns a uri for a sound master file' do + expect(sound_master_file.rdf_uri.class).to eq(String) + expect { URI.parse(sound_master_file.rdf_uri) }.not_to raise_error + end + it 'returns a uri for a video master file' do + expect(video_master_file.rdf_uri.class).to eq(String) + expect { URI.parse(video_master_file.rdf_uri) }.not_to raise_error + end + end + end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 1210e21c79..3fff17b899 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -21,17 +21,15 @@ describe 'validations' do describe 'collection' do it 'has errors when not present' do - media_object.collection = nil - media_object.valid? - media_object.errors.should include(:collection) + expect{media_object.collection = nil}.to raise_error(ActiveFedora::RecordInvalid) end it 'does not have errors when present' do media_object.valid? - media_object.errors[:collection].should be_empty + expect(media_object.errors[:collection]).to be_empty end end describe 'governing_policy' do - it {should validate_presence_of(:governing_policy)} + it {is_expected.to validate_presence_of(:governing_policies)} end describe 'language' do it 'should validate valid language' do @@ -66,7 +64,8 @@ '1984~' => ['1984'], '1984?~' => ['1984'], '2004-06-11?' => ['2004'], - 'unknown/2006' => [], + 'unknown/2006' => ['Unknown'], + '2006/unknown' => ['Unknown'], '2001-21' => ['2001'], '[1667,1668,1670..1672]' => ['1667','1668','1670','1671','1672'], '{1667,1668,1670..1672}' => ['1667','1668','1670','1671','1672'], @@ -74,7 +73,8 @@ '159u-12' => [], '159u-12-25' => ['1590','1591','1592','1593','1594','1595','1596','1597','1598','1599'], '159x' => ['1590','1591','1592','1593','1594','1595','1596','1597','1598','1599'], - '2011-(06-04)~' => ['2011'] + '2011-(06-04)~' => ['2011'], + 'unknown/unknown' => ['Unknown'] }} it "should not accept invalid EDTF formatted dates" do [Faker::Lorem.sentence(4),'-999','17000'].each do |d| @@ -83,19 +83,19 @@ expect(media_object.errors[:date_issued].present?).to be_truthy end end - + it "should accept valid EDTF formatted dates" do valid_dates.keys do |d| media_object.date_issued = d expect(media_object.valid?).to be_truthy end end - + it "should gather the year from a date string" do valid_dates.each_pair do |k,v| - expect(media_object.descMetadata.send(:gather_years, k)).to eq v + expect(media_object.descMetadata.send(:gather_years, k)).to eq v end - end + end end describe 'notes' do @@ -115,35 +115,35 @@ describe 'delegators' do it 'correctly sets the creator' do media_object.creator = ['Creator, Joan'] - media_object.creator.should include('Creator, Joan') - media_object.descMetadata.creator.should include('Creator, Joan') + expect(media_object.creator).to include('Creator, Joan') + expect(media_object.descMetadata.creator).to include('Creator, Joan') end end describe 'abilities' do - let (:collection) { media_object.collection.reload } + let (:collection) { media_object.collection.reload } context 'when manager' do subject{ ability} let(:ability){ Ability.new(User.where(username: collection.managers.first).first) } - it{ should be_able_to(:create, MediaObject) } - it{ should be_able_to(:read, media_object) } - it{ should be_able_to(:update, media_object) } - it{ should be_able_to(:destroy, media_object) } - it{ should be_able_to(:inspect, media_object) } + it{ is_expected.to be_able_to(:create, MediaObject) } + it{ is_expected.to be_able_to(:read, media_object) } + it{ is_expected.to be_able_to(:update, media_object) } + it{ is_expected.to be_able_to(:destroy, media_object) } + it{ is_expected.to be_able_to(:inspect, media_object) } it "should be able to destroy and unpublish published item" do media_object.publish! "someone" - subject.should be_able_to(:destroy, media_object) - subject.should be_able_to(:unpublish, media_object) + expect(subject).to be_able_to(:destroy, media_object) + expect(subject).to be_able_to(:unpublish, media_object) end context 'and logged in through LTI' do let(:ability){ Ability.new(User.where(username: collection.managers.first).first, {full_login: false, virtual_groups: [Faker::Lorem.word]}) } - it{ should_not be_able_to(:share, MediaObject) } - it{ should_not be_able_to(:update, media_object) } - it{ should_not be_able_to(:destroy, media_object) } + it{ is_expected.not_to be_able_to(:share, MediaObject) } + it{ is_expected.not_to be_able_to(:update, media_object) } + it{ is_expected.not_to be_able_to(:destroy, media_object) } end end @@ -151,17 +151,17 @@ subject{ ability} let(:ability){ Ability.new(User.where(username: collection.editors.first).first) } - it{ should be_able_to(:create, MediaObject) } - it{ should be_able_to(:read, media_object) } - it{ should be_able_to(:update, media_object) } - it{ should be_able_to(:destroy, media_object) } - it{ should be_able_to(:update_access_control, media_object) } + it{ is_expected.to be_able_to(:create, MediaObject) } + it{ is_expected.to be_able_to(:read, media_object) } + it{ is_expected.to be_able_to(:update, media_object) } + it{ is_expected.to be_able_to(:destroy, media_object) } + it{ is_expected.to be_able_to(:update_access_control, media_object) } it "should not be able to destroy and unpublish published item" do media_object.publish! "someone" - subject.should_not be_able_to(:destroy, media_object) - subject.should_not be_able_to(:update, media_object) - subject.should_not be_able_to(:update_access_control, media_object) - subject.should_not be_able_to(:unpublish, media_object) + expect(subject).not_to be_able_to(:destroy, media_object) + expect(subject).not_to be_able_to(:update, media_object) + expect(subject).not_to be_able_to(:update_access_control, media_object) + expect(subject).not_to be_able_to(:unpublish, media_object) end end @@ -169,16 +169,16 @@ subject{ ability } let(:ability){ Ability.new(User.where(username: collection.depositors.first).first) } - it{ should be_able_to(:create, MediaObject) } - it{ should be_able_to(:read, media_object) } - it{ should be_able_to(:update, media_object) } - it{ should be_able_to(:destroy, media_object) } + it{ is_expected.to be_able_to(:create, MediaObject) } + it{ is_expected.to be_able_to(:read, media_object) } + it{ is_expected.to be_able_to(:update, media_object) } + it{ is_expected.to be_able_to(:destroy, media_object) } it "should not be able to destroy and unpublish published item" do media_object.publish! "someone" - subject.should_not be_able_to(:destroy, media_object) - subject.should_not be_able_to(:unpublish, media_object) + expect(subject).not_to be_able_to(:destroy, media_object) + expect(subject).not_to be_able_to(:unpublish, media_object) end - it{ should_not be_able_to(:update_access_control, media_object) } + it{ is_expected.not_to be_able_to(:update_access_control, media_object) } end context 'when end-user' do @@ -186,23 +186,23 @@ let(:ability){ Ability.new(user) } let(:user){FactoryGirl.create(:user)} - it{ should be_able_to(:share, MediaObject) } + it{ is_expected.to be_able_to(:share, MediaObject) } it "should not be able to read unauthorized, published MediaObject" do media_object.avalon_publisher = "random" media_object.save - subject.can?(:read, media_object).should be false + expect(subject.can?(:read, media_object)).to be false end it "should not be able to read authorized, unpublished MediaObject" do media_object.read_users += [user.user_key] - media_object.should_not be_published - subject.can?(:read, media_object).should be false + expect(media_object).not_to be_published + expect(subject.can?(:read, media_object)).to be false end it "should be able to read authorized, published MediaObject" do media_object.read_users += [user.user_key] media_object.publish! "random" - subject.can?(:read, media_object).should be true + expect(subject.can?(:read, media_object)).to be true end end @@ -211,21 +211,46 @@ let(:user){ FactoryGirl.create(:user_lti) } let(:ability){ Ability.new(user, {full_login: false, virtual_groups: [Faker::Lorem.word]}) } - it{ should_not be_able_to(:share, MediaObject) } + it{ is_expected.not_to be_able_to(:share, MediaObject) } + end + + context 'when ip address' do + subject{ ability } + let(:user) { FactoryGirl.create(:user) } + let(:ip_addr) { Faker::Internet.ip_v4_address } + let(:ability) { Ability.new(user, {remote_ip: ip_addr}) } + before do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(ip_addr) + end + + it 'should not be able to read unauthorized, published MediaObject' do + media_object.read_groups += [Faker::Internet.ip_v4_address] + media_object.publish! "random" + expect(subject.can?(:read, media_object)).to be_falsey + end + it 'should be able to read single-ip authorized, published MediaObject' do + media_object.read_groups += [ip_addr] + media_object.publish! "random" + expect(subject.can?(:read, media_object)).to be_truthy + end + it 'should be able to read ip-range authorized, published MediaObject' do + media_object.read_groups += ["#{ip_addr}/30"] + media_object.publish! "random" + expect(subject.can?(:read, media_object)).to be_truthy + end end end describe "Required metadata is present" do - it {should validate_presence_of(:creator)} - it {should validate_presence_of(:date_issued)} - it {should validate_presence_of(:title)} + it {is_expected.to validate_presence_of(:date_issued)} + it {is_expected.to validate_presence_of(:title)} end describe "Languages are handled correctly" do it "should handle pairs of language codes and language names" do media_object.update_datastream(:descMetadata, :language => ['eng','French','spa','uig']) - media_object.descMetadata.language_code.to_a.should =~ ['eng','fre','spa','uig'] - media_object.descMetadata.language_text.to_a.should =~ ['English','French','Spanish','Uighur'] + expect(media_object.descMetadata.language_code.to_a).to match_array(['eng','fre','spa','uig']) + expect(media_object.descMetadata.language_text.to_a).to match_array(['English','French','Spanish','Uighur']) end end @@ -245,30 +270,30 @@ media_object.contributor = contributor media_object.save - media_object.contributor.length.should == 1 - media_object.contributor.should == [contributor] + expect(media_object.contributor.length).to eq(1) + expect(media_object.contributor).to eq([contributor]) end xit "should support multiple contributors" do contributors = ['Chris Colvard', 'Phuong Dinh', 'Michael Klein', 'Nathan Rogers'] media_object.contributor = contributors media_object.save - media_object.contributor.length.should > 1 - media_object.contrinbutor.should == contributors + expect(media_object.contributor.length).to be > 1 + expect(media_object.contrinbutor).to eq(contributors) end xit "should support multiple publishers" do media_object.publisher = ['Indiana University'] - media_object.publisher.length.should == 1 - + expect(media_object.publisher.length).to eq(1) + publishers = ['Indiana University', 'Northwestern University', 'Ohio State University', 'Notre Dame'] media_object.publisher = publishers media_object.save - media_object.publisher.length.should > 1 - media_object.publisher.should == publishers + expect(media_object.publisher.length).to be > 1 + expect(media_object.publisher).to eq(publishers) end end - + describe "Update datastream" do it "should handle a complex update" do params = { @@ -279,11 +304,11 @@ 'date_created'=> '1956' } media_object.update_datastream(:descMetadata, params) - media_object.creator.should == params['creator'] - media_object.contributor.should == params['contributor'] - media_object.title.should == params['title'] - media_object.date_issued.should == params['date_issued'] - media_object.date_created.should == params['date_created'] + expect(media_object.creator).to eq(params['creator']) + expect(media_object.contributor).to eq(params['contributor']) + expect(media_object.title).to eq(params['title']) + expect(media_object.date_issued).to eq(params['date_issued']) + expect(media_object.date_created).to eq(params['date_created']) end end @@ -297,26 +322,37 @@ end end + describe "Update datastream with more than one originInfo element" do + it "shouldn't error out" do + media_object.date_created = '2016' + media_object.date_issued = nil + media_object.descMetadata.ng_xml.root.add_child('') + expect { media_object.descMetadata.add_date_issued('2017') }.not_to raise_error + expect(media_object.date_created).to eq '2016' + expect(media_object.date_issued).to eq '2017' + end + end + describe "Ingest status" do it "should default to unpublished" do - media_object.workflow.published.first.should eq "false" - media_object.workflow.published?.should eq false + expect(media_object.workflow.published.first).to eq "false" + expect(media_object.workflow.published?).to eq false end it "should be published when the item is visible" do media_object.workflow.publish - media_object.workflow.published.should == ['true'] - media_object.workflow.last_completed_step.first.should == HYDRANT_STEPS.last.step + expect(media_object.workflow.published).to eq(['true']) + expect(media_object.workflow.last_completed_step.first).to eq(HYDRANT_STEPS.last.step) end it "should recognize the current step" do media_object.workflow.last_completed_step = 'structure' - media_object.workflow.current?('access-control').should == true + expect(media_object.workflow.current?('access-control')).to eq(true) end it "should default to the first workflow step" do - media_object.workflow.last_completed_step.should == [''] + expect(media_object.workflow.last_completed_step).to eq(['']) end end @@ -324,32 +360,32 @@ it 'returns true if the statuses indicate processing is finished' do media_object.parts << MasterFile.new(status_code: 'CANCELLED') media_object.parts << MasterFile.new(status_code: 'COMPLETED') - media_object.finished_processing?.should be true + expect(media_object.finished_processing?).to be true end it 'returns true if the statuses indicate processing is not finished' do media_object.parts << MasterFile.new(status_code: 'CANCELLED') media_object.parts << MasterFile.new(status_code: 'RUNNING') - media_object.finished_processing?.should be false + expect(media_object.finished_processing?).to be false end end describe '#calculate_duration' do it 'returns zero if there are zero master files' do - media_object.send(:calculate_duration).should == 0 + expect(media_object.send(:calculate_duration)).to eq(0) end it 'returns the correct duration with two master files' do media_object.parts << MasterFile.new(duration: '40') media_object.parts << MasterFile.new(duration: '40') - media_object.send(:calculate_duration).should == 80 + expect(media_object.send(:calculate_duration)).to eq(80) end it 'returns the correct duration with two master files one nil' do media_object.parts << MasterFile.new(duration: '40') media_object.parts << MasterFile.new(duration: nil) - media_object.send(:calculate_duration).should == 40 + expect(media_object.send(:calculate_duration)).to eq(40) end it 'returns the correct duration with one master file that is nil' do media_object.parts << MasterFile.new(duration:nil) - media_object.send(:calculate_duration).should == 0 + expect(media_object.send(:calculate_duration)).to eq(0) end end @@ -360,51 +396,55 @@ media_object.section_pid = master_file_pids media_object.save( validate: false ) media_object.destroy - MasterFile.exists?(master_file_pids.first).should == false + expect(MasterFile.exists?(master_file_pids.first)).to eq(false) end end - describe '#set_duration!' do - it 'sets duration on the model' do - media_object.set_duration! - media_object.duration.should == '0' + context "dependent properties" do + describe '#set_duration!' do + it 'sets duration on the model' do + media_object.set_duration! + expect(media_object.duration).to eq('0') + end end - end - describe '#set_media_types!' do - let!(:master_file) { FactoryGirl.create(:master_file, mediaobject: media_object) } - it 'sets format on the model' do - expect(media_object.format).to be nil - media_object.set_media_types! - expect(media_object.format).to eq "video/mp4" + describe '#set_media_types!' do + let!(:master_file) { FactoryGirl.create(:master_file, mediaobject: media_object) } + it 'sets format on the model' do + media_object.update_attribute_in_metadata(:format, nil) + expect(media_object.format).to be_nil + media_object.set_media_types! + expect(media_object.format).to eq "video/mp4" + end end - end - describe '#set_resource_types!' do - let!(:master_file) { FactoryGirl.create(:master_file, mediaobject: media_object) } - it 'sets resource_type on the model' do - expect(media_object.descMetadata.resource_type).to eq [] - media_object.set_resource_types! - expect(media_object.descMetadata.resource_type).to eq ["moving image"] + describe '#set_resource_types!' do + let!(:master_file) { FactoryGirl.create(:master_file, mediaobject: media_object) } + it 'sets resource_type on the model' do + media_object.displayMetadata.avalon_resource_type = [] + expect(media_object.displayMetadata.avalon_resource_type).to be_empty + media_object.set_resource_types! + expect(media_object.displayMetadata.avalon_resource_type).to eq ["moving image"] + end end end - + describe '#publish!' do describe 'facet' do it 'publishes' do media_object.publish!('adam@adam.com') - media_object.to_solr["workflow_published_sim"].should == 'Published' + expect(media_object.to_solr["workflow_published_sim"]).to eq('Published') end it 'unpublishes' do media_object.publish!(nil) - media_object.to_solr["workflow_published_sim"].should == 'Unpublished' + expect(media_object.to_solr["workflow_published_sim"]).to eq('Unpublished') end end end describe 'indexing' do it 'uses stringified keys for everything except :id' do - media_object.to_solr.keys.reject { |k| k.is_a?(String) }.should == [:id] + expect(media_object.to_solr.keys.reject { |k| k.is_a?(String) }).to eq([:id]) end it 'should not index any unknown resource types' do media_object.descMetadata.resource_type = 'notated music' @@ -417,9 +457,31 @@ expect(solr_doc['other_identifier_sim']).to include('12345678','8675309 testing') expect(solr_doc['other_identifier_sim']).not_to include('123456788675309 testing') end + it 'should index identifier for parts' do + master_file = FactoryGirl.create(:master_file, mediaobject_id: media_object.pid) + master_file.DC.identifier += ["TestOtherID"] + master_file.save! + media_object.parts += [master_file] + media_object.save! + media_object.reload + solr_doc = media_object.to_solr + expect(solr_doc['other_identifier_sim']).to include('TestOtherID') + end + it 'should index labels for parts' do + master_file = FactoryGirl.create(:master_file_with_structure, mediaobject_id: media_object.pid, label: 'Test Label') + master_file.save! + media_object.parts += [master_file] + media_object.save! + media_object.reload + solr_doc = media_object.to_solr + expect(solr_doc['section_label_tesim']).to include('CD 1') + expect(solr_doc['section_label_tesim']).to include('Test Label') + end end describe 'permalink' do + before { Delayed::Worker.delay_jobs = false } + after { Delayed::Worker.delay_jobs = true } let(:media_object){ FactoryGirl.build(:media_object) } @@ -429,30 +491,30 @@ context 'unpublished' do it 'is empty when unpublished' do - media_object.permalink.should be_blank + expect(media_object.permalink).to be_blank end end context 'published' do - + before(:each){ media_object.publish!('C.S. Lewis') } # saves the object it 'responds to permalink' do - media_object.respond_to?(:permalink).should be true + expect(media_object.respond_to?(:permalink)).to be true end it 'sets the permalink on the object' do - media_object.permalink.should_not be_nil + expect(media_object.permalink).not_to be_nil end it 'sets the correct permalink' do - media_object.permalink.should == 'http://www.example.com/perma-url' + expect(media_object.permalink).to eq('http://www.example.com/perma-url') end it 'does not remove the permalink if the permalink service returns nil' do Permalink.on_generate{ nil } media_object.save( validate: false ) - media_object.permalink.should == 'http://www.example.com/perma-url' + expect(media_object.permalink).to eq('http://www.example.com/perma-url') end end @@ -467,8 +529,8 @@ 'http://www.example.com/perma-url' } media_object.ensure_permalink! - t.should == "http://test.host/media_objects/#{media_object.pid}" - media_object.permalink.should == 'http://www.example.com/perma-url' + expect(t).to eq("http://test.host/media_objects/#{media_object.pid}") + expect(media_object.permalink).to eq('http://www.example.com/perma-url') end end @@ -477,7 +539,7 @@ it 'logs an error when the permalink service returns an exception' do Permalink.on_generate{ 1 / 0 } - Rails.logger.should_receive(:error) + expect(Rails.logger).to receive(:error) media_object.ensure_permalink! end @@ -485,7 +547,7 @@ describe "#ensure_permalink!" do it 'is not called when the object is not persisted' do - media_object.should_not_receive(:ensure_permalink!) + expect(media_object).not_to receive(:ensure_permalink!) media_object.save end end @@ -493,13 +555,13 @@ describe '#ensure_permalink!' do it 'returns true when updated' do - media_object.should_receive(:ensure_permalink!).at_least(1).times.and_return(false) + expect(media_object).to receive(:ensure_permalink!).at_least(1).times.and_return(false) media_object.publish!('C.S. Lewis') - end + end it 'returns false when not updated' do media_object.publish!('C.S. Lewis') - media_object.should_receive(:ensure_permalink!).and_return(false) + expect(media_object).to receive(:ensure_permalink!).and_return(false) media_object.save( validate: false ) end end @@ -523,4 +585,95 @@ expect { media_object.descMetadata.populate_from_catalog!(bib_id, 'local') }.to_not change { media_object.resource_type } end end + + describe '#section_pid' do + before do + 2.times do + mf = FactoryGirl.create(:master_file) + mf.mediaobject = media_object + mf.save + end + media_object.save + end + let(:part_ids) { media_object.part_ids } + let(:trap_ids) { media_object.part_ids.reverse } + + it 'should append missing section_pids' do + media_object.section_pid = [part_ids.first] + expect( media_object.section_pid ).to eq(part_ids) + + media_object.section_pid = [trap_ids.first] + expect( media_object.section_pid ).to eq(trap_ids) + end + + it 'should remove superfluous section_pids' do + nope_ids = trap_ids + ['avalon:nope'] + media_object.section_pid = nope_ids + expect( nope_ids.length ).to eq(3) + expect( media_object.section_pid ).to eq(trap_ids) + end + + it 'should append missing section_pids and remove superfluous section_pids' do + media_object.section_pid = ['avalon:nope'] + expect( media_object.section_pid ).to eq(part_ids) + end + + it 'should report changes' do + expect( media_object.section_pid_changed? ).to be_falsey + expect( media_object.changes ).to eq({}) + media_object.section_pid = trap_ids + expect( media_object.section_pid_changed? ).to be_truthy + expect( media_object.changes ).to eq({"section_pid"=>[part_ids, trap_ids]}) + end + end + + describe '#section_labels' do + before do + mf = FactoryGirl.create(:master_file_with_structure, label: 'Test Label') + mf.mediaobject = media_object + mf.save + media_object.save + end + it 'should return correct list of labels' do + expect(media_object.section_labels.first).to eq 'CD 1' + expect(media_object.section_labels).to include 'Test Label' + end + end + + describe '#physical_description' do + it 'should return a list of physical descriptions' do + mf = FactoryGirl.create(:master_file_with_structure, label: 'Test Label') + mf.descMetadata.physical_description = 'stone tablet' + mf.mediaobject = media_object + mf.save + media_object.save + expect(media_object.section_physical_descriptions).to match(['stone tablet']) + end + + it 'should not return nil physical descriptions' do + mf = FactoryGirl.create(:master_file_with_structure, label: 'Test Label') + mf.mediaobject = media_object + mf.save + media_object.save + expect(media_object.section_physical_descriptions).to match([]) + end + + it 'should return a unique list of physical descriptions' do + mf = FactoryGirl.create(:master_file_with_structure, label: 'Test Label') + mf.descMetadata.physical_description = 'cave paintings' + mf.mediaobject = media_object + mf.save + + mf2 = FactoryGirl.create(:master_file_with_structure, label: 'Test Label2') + mf2.descMetadata.physical_description = 'cave paintings' + mf2.mediaobject = media_object + mf2.save + media_object.save + + expect(media_object.parts.size).to eq(2) + expect(media_object.section_physical_descriptions).to match(['cave paintings']) + + end + end + end diff --git a/spec/models/playlist_item_spec.rb b/spec/models/playlist_item_spec.rb new file mode 100644 index 0000000000..6a2eb591aa --- /dev/null +++ b/spec/models/playlist_item_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'cancan/matchers' + +RSpec.describe PlaylistItem, type: :model do + describe 'validations' do + it { is_expected.to validate_presence_of(:playlist) } + it { is_expected.to validate_presence_of(:annotation) } + end + + describe 'abilities' do + subject{ ability } + let(:ability){ Ability.new(user) } + let(:user){ FactoryGirl.create(:user) } + let(:master_file) { FactoryGirl.create(:master_file) } + let(:avalon_annotation) { FactoryGirl.create(:avalon_annotation, master_file: master_file) } + let(:playlist_item) { FactoryGirl.create(:playlist_item, playlist: playlist, annotation: avalon_annotation) } + + context 'when owner' do + let(:playlist) { FactoryGirl.create(:playlist, user: user) } + + it{ is_expected.to be_able_to(:create, playlist_item) } + it{ is_expected.to be_able_to(:update, playlist_item) } + it{ is_expected.to be_able_to(:delete, playlist_item) } + + context 'when master file is NOT readable by user' do + it{ is_expected.not_to be_able_to(:read, playlist_item) } + end + + context 'when master file is readable by user' do + let(:media_object) { FactoryGirl.create(:published_media_object, visibility: 'public') } + let(:master_file) { FactoryGirl.create(:master_file, mediaobject: media_object) } + + it{ is_expected.to be_able_to(:read, playlist_item) } + end + end + + context 'when other user' do + let(:playlist) { FactoryGirl.create(:playlist, visibility: Playlist::PUBLIC) } + + it{ is_expected.not_to be_able_to(:create, playlist_item) } + it{ is_expected.not_to be_able_to(:update, playlist_item) } + it{ is_expected.not_to be_able_to(:delete, playlist_item) } + + context 'when master file is NOT readable by user' do + it{ is_expected.not_to be_able_to(:read, playlist_item) } + end + + context 'when master file is readable by user' do + let(:media_object) { FactoryGirl.create(:published_media_object, visibility: 'public') } + let(:master_file) { FactoryGirl.create(:master_file, mediaobject: media_object) } + + it{ is_expected.to be_able_to(:read, playlist_item) } + end + end + end +end diff --git a/spec/models/playlist_spec.rb b/spec/models/playlist_spec.rb new file mode 100644 index 0000000000..a165412b5f --- /dev/null +++ b/spec/models/playlist_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' +require 'cancan/matchers' + +RSpec.describe Playlist, type: :model do + describe 'validations' do + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:visibility) } + it { is_expected.to validate_inclusion_of(:visibility).in_array([Playlist::PUBLIC, Playlist::PRIVATE]) } + end + + describe 'abilities' do + subject{ ability } + let(:ability){ Ability.new(user) } + let(:user){ FactoryGirl.create(:user) } + + context 'when owner' do + let(:playlist) { FactoryGirl.create(:playlist, user: user) } + + it{ is_expected.to be_able_to(:manage, playlist) } + it{ is_expected.to be_able_to(:create, playlist) } + it{ is_expected.to be_able_to(:read, playlist) } + it{ is_expected.to be_able_to(:update, playlist) } + it{ is_expected.to be_able_to(:delete, playlist) } + end + + context 'when other user' do + let(:playlist) { FactoryGirl.create(:playlist, visibility: Playlist::PUBLIC) } + + it{ is_expected.not_to be_able_to(:manage, playlist) } + it{ is_expected.not_to be_able_to(:create, playlist) } + it{ is_expected.to be_able_to(:read, playlist) } + it{ is_expected.not_to be_able_to(:update, playlist) } + it{ is_expected.not_to be_able_to(:delete, playlist) } + end + end + + describe 'related items' do + subject(:video_master_file) { FactoryGirl.create(:master_file) } + subject(:sound_master_file) { FactoryGirl.create(:master_file_sound) } + let(:v_one_annotation) { AvalonAnnotation.new(master_file: video_master_file) } + let(:v_two_annotation) { AvalonAnnotation.new(master_file: video_master_file) } + let(:s_one_annotation) { AvalonAnnotation.new(master_file: sound_master_file) } + + it 'returns a list of playlist items on the current playlist related to a playlist item' do + setup_playlist + expect(@playlist.related_items(PlaylistItem.first).size).to eq(1) + expect(@playlist.related_items(PlaylistItem.last).size).to eq(0) + end + it 'returns a list of playlist annotations on the current playlist related to a playlist item' do + setup_playlist + expect(@playlist.related_annotations(PlaylistItem.first).size).to eq(1) + expect(@playlist.related_annotations(PlaylistItem.last).size).to eq(0) + end + it 'returns a list of annotations who start time falls within the time range of the current playlist item' do + setup_playlist + expect(@playlist.related_annotations_time_contrained(PlaylistItem.first).size).to eq(1) + # Move the annotation outside of the time range + v_two_annotation.start_time = 2 + v_two_annotation.end_time = 3 + v_two_annotation.save! + v_one_annotation.end_time = 1 + v_one_annotation.save! + expect(@playlist.related_annotations_time_contrained(PlaylistItem.first).size).to eq(0) + end + def setup_playlist + annos = [v_one_annotation, v_two_annotation, s_one_annotation] + annos.each do |a| + a.save + end + @playlist = Playlist.new + @playlist.user_id = User.last.id + @playlist.title = 'spec test' + @playlist.save + pos = 1 + annos.each do |a| + @pi = PlaylistItem.new + @pi.playlist_id = @playlist.id + @pi.annotation_id = a.id + @pi.position = pos + @pi.save! + pos += 1 + end + end + end +end diff --git a/spec/models/role_map_spec.rb b/spec/models/role_map_spec.rb index 15af39556e..f0e1d25ab4 100644 --- a/spec/models/role_map_spec.rb +++ b/spec/models/role_map_spec.rb @@ -25,14 +25,14 @@ end it "should properly initialize the map" do - RoleMapper.map.should == YAML.load(File.read(File.join(Rails.root, "config/role_map_#{Rails.env}.yml"))) + expect(RoleMapper.map).to eq(YAML.load(File.read(File.join(Rails.root, "config/role_map_#{Rails.env}.yml")))) end it "should properly persist a hash" do new_hash = { 'archivist' => ['alice.archivist@example.edu'], 'registered' => ['bob.user@example.edu','charlie.user@example.edu'] } RoleMap.replace_with!(new_hash) - RoleMapper.map.should == new_hash - RoleMap.load.should == new_hash + expect(RoleMapper.map).to eq(new_hash) + expect(RoleMap.load).to eq(new_hash) end end end diff --git a/spec/models/stream_token_spec.rb b/spec/models/stream_token_spec.rb index 1b82abc5f8..df7ccabc94 100644 --- a/spec/models/stream_token_spec.rb +++ b/spec/models/stream_token_spec.rb @@ -21,7 +21,7 @@ let(:session) { { :session_id => '00112233445566778899aabbccddeeff' } } it "should create a token" do - StreamToken.find_or_create_session_token(session, target).should =~ /^[0-9a-f]{40}$/ + expect(StreamToken.find_or_create_session_token(session, target)).to match(/^[0-9a-f]{40}$/) end end @@ -29,7 +29,7 @@ let(:session) { {} } it "should create a token" do - StreamToken.find_or_create_session_token(session, target).should =~ /^[0-9a-f]{40}$/ + expect(StreamToken.find_or_create_session_token(session, target)).to match(/^[0-9a-f]{40}$/) end end diff --git a/spec/models/url_datastream_spec.rb b/spec/models/url_datastream_spec.rb index 6df34856ea..5fc11ae79f 100644 --- a/spec/models/url_datastream_spec.rb +++ b/spec/models/url_datastream_spec.rb @@ -41,8 +41,12 @@ end end + it "should accept an underscore in the hostname" do + expect { subject.location = "nfs://nfs_server.example.edu/share/foo/bar/baz.jpg" }.not_to raise_error + end + it "should require a valid URL" do - expect { subject.location = 'blah blah blah' }.to raise_error(URI::InvalidURIError) + expect { subject.location = 'http:///' }.to raise_error(URI::InvalidURIError) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3f424105ff..907088f200 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -20,27 +20,27 @@ let!(:list) {0.upto(rand(5)).collect { Faker::Internet.email }} describe "validations" do - it {should validate_presence_of(:username)} - it {should validate_uniqueness_of(:username)} - it {should validate_presence_of(:email)} - it {should validate_uniqueness_of(:email)} + it {is_expected.to validate_presence_of(:username)} + it {is_expected.to validate_uniqueness_of(:username)} + it {is_expected.to validate_presence_of(:email)} + it {is_expected.to validate_uniqueness_of(:email)} end describe "Membership" do it "should be a member if its key is in the list" do - user.should be_in(list,[user.user_key]) - user.should be_in(list+[user.user_key]) + expect(user).to be_in(list,[user.user_key]) + expect(user).to be_in(list+[user.user_key]) end it "should not be a member if its key is not in the list" do - user.should_not be_in(list) + expect(user).not_to be_in(list) end end describe "#groups" do let(:groups) { ["foorole"] } it "should return groups from the role map" do - RoleMapper.should_receive(:roles).and_return(groups) + expect(RoleMapper).to receive(:roles).and_return(groups) expect(user.groups).to eq(groups) end end @@ -65,4 +65,13 @@ end end + describe "#destroy" do + it 'removes bookmarks for user' do + media_object = FactoryGirl.create(:published_media_object) + user = FactoryGirl.create(:public) + bookmark = Bookmark.create(document_id: media_object.pid, user_id: user.id) + expect { user.destroy }.to change { Bookmark.exists? bookmark }.from( true ).to( false ) + end + end + end diff --git a/spec/routing/playlists_routing_spec.rb b/spec/routing/playlists_routing_spec.rb new file mode 100644 index 0000000000..8996ff6e93 --- /dev/null +++ b/spec/routing/playlists_routing_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +RSpec.describe PlaylistsController, type: :routing do + describe 'routing' do + it 'routes to #index' do + expect(get: '/playlists').to route_to('playlists#index') + end + + it 'routes to #new' do + expect(get: '/playlists/new').to route_to('playlists#new') + end + + it 'routes to #show' do + expect(get: '/playlists/1').to route_to('playlists#show', id: '1') + end + + it 'routes to #edit' do + expect(get: '/playlists/1/edit').to route_to('playlists#edit', id: '1') + end + + it 'routes to #create' do + expect(post: '/playlists').to route_to('playlists#create') + end + + it 'routes to #update via PUT' do + expect(put: '/playlists/1').to route_to('playlists#update', id: '1') + end + + it 'routes to #update via PATCH' do + expect(patch: '/playlists/1').to route_to('playlists#update', id: '1') + end + + it 'routes to #destroy' do + expect(delete: '/playlists/1').to route_to('playlists#destroy', id: '1') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0341b43689..833a96e46e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,14 +1,14 @@ # Copyright 2011-2015, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed +# +# Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- @@ -20,13 +20,16 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'rspec/autorun' -require 'rspec/its' require 'equivalent-xml/rspec_matchers' require 'capybara/rspec' require 'database_cleaner' require 'fakefs/safe' require 'fileutils' require 'tmpdir' +require 'coveralls' + +#Configure coveralls for CI builds +Coveralls.wear!('rails') # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -64,7 +67,7 @@ DatabaseCleaner[:active_record].strategy = :deletion DatabaseCleaner[:active_fedora].strategy = :deletion DatabaseCleaner.clean - Rails.cache.clear + Rails.cache.clear Deprecation.default_deprecation_behavior = :silence # Stub the entire dropbox @@ -75,7 +78,7 @@ Avalon::Configuration['dropbox']['path'] = Avalon::Configuration.lookup('spec.fake_dropbox') ActiveEncode::Base.engine_adapter = :test - + server_options = { host: 'test.host', port: nil } Rails.application.routes.default_url_options.merge!( server_options ) ActionMailer::Base.default_url_options.merge!( server_options ) @@ -94,13 +97,13 @@ Rails.cache.clear DatabaseCleaner.start RoleMap.reset! - Admin::Collection.stub(:units).and_return ['University Archives', 'University Library'] + allow(Admin::Collection).to receive(:units).and_return ['University Archives', 'University Library'] end config.after(:each) do Rails.cache.clear DatabaseCleaner.clean - end + end config.include Devise::TestHelpers, :type => :controller config.include ControllerMacros, :type => :controller diff --git a/spec/validators/uniqueness_validator_spec.rb b/spec/validators/uniqueness_validator_spec.rb index 817f78929c..43f8fab696 100644 --- a/spec/validators/uniqueness_validator_spec.rb +++ b/spec/validators/uniqueness_validator_spec.rb @@ -21,7 +21,7 @@ before(:each) do @record = double(pid:"avalon:1") - @record.stub("errors").and_return([]) + allow(@record).to receive("errors").and_return([]) end it "should raise an exception if solr_name option is missing" do @@ -29,22 +29,22 @@ end it "should not return errors when field is unique" do - validator.stub("find_doc").and_return(nil) - @record.should_not_receive('errors') + allow(validator).to receive("find_doc").and_return(nil) + expect(@record).not_to receive('errors') validator.validate_each(@record, "title", "new_title") end it "should not return errors when field is unique but record is the same" do doc = double(pid: "avalon:1") - validator.stub("find_doc").and_return(doc) - @record.should_not_receive('errors') + allow(validator).to receive("find_doc").and_return(doc) + expect(@record).not_to receive('errors') validator.validate_each(@record, "title", "new_title") end it "should return erros when field is not unique" do doc = double(pid: "avalon:2") - validator.stub("find_doc").and_return(doc) - @record.errors.should_receive('add') + allow(validator).to receive("find_doc").and_return(doc) + expect(@record.errors).to receive('add') validator.validate_each(@record, "title", "old_title") end @@ -54,22 +54,22 @@ it "should use the solr field name and supplied values" do relation = double() - relation.stub("first") - klass.should_receive("where").once.with(solr_field => value).and_return(relation) + allow(relation).to receive("first") + expect(klass).to receive("where").once.with(solr_field => value).and_return(relation) validator.find_doc(klass, value) end it "should return one record when present" do doc = double(pid: "avalon:1") relation = double() - relation.stub("first").and_return(doc) - klass.stub("where").and_return(relation) - validator.find_doc(klass, value).should be_an_instance_of klass + allow(relation).to receive("first").and_return(doc) + allow(klass).to receive("where").and_return(relation) + expect(validator.find_doc(klass, value)).to be_an_instance_of klass end it "should return nil when not present" do relation = double() - relation.stub("first").and_return(nil) - klass.stub("where").and_return(relation) - validator.find_doc(klass, value).should be_nil + allow(relation).to receive("first").and_return(nil) + allow(klass).to receive("where").and_return(relation) + expect(validator.find_doc(klass, value)).to be_nil end end end diff --git a/vendor/assets/javascripts/jquery.nestable.js b/vendor/assets/javascripts/jquery.nestable.js new file mode 100644 index 0000000000..2e98d7d228 --- /dev/null +++ b/vendor/assets/javascripts/jquery.nestable.js @@ -0,0 +1,646 @@ +/*! + * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/ + * Dual-licensed under the BSD or MIT licenses + */ +;(function($, window, document, undefined) +{ + var hasTouch = 'ontouchstart' in window; + var nestableCopy; + + /** + * Detect CSS pointer-events property + * events are normally disabled on the dragging element to avoid conflicts + * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js + */ + var hasPointerEvents = (function() + { + var el = document.createElement('div'), + docEl = document.documentElement; + if (!('pointerEvents' in el.style)) { + return false; + } + el.style.pointerEvents = 'auto'; + el.style.pointerEvents = 'x'; + docEl.appendChild(el); + var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto'; + docEl.removeChild(el); + return !!supports; + })(); + + var eStart = hasTouch ? 'touchstart' : 'mousedown', + eMove = hasTouch ? 'touchmove' : 'mousemove', + eEnd = hasTouch ? 'touchend' : 'mouseup', + eCancel = hasTouch ? 'touchcancel' : 'mouseup'; + + var defaults = { + listNodeName : 'ol', + itemNodeName : 'li', + rootClass : 'dd', + listClass : 'dd-list', + itemClass : 'dd-item', + dragClass : 'dd-dragel', + handleClass : 'dd-handle', + collapsedClass : 'dd-collapsed', + placeClass : 'dd-placeholder', + noDragClass : 'dd-nodrag', + noChildrenClass : 'dd-nochildren', + emptyClass : 'dd-empty', + expandBtnHTML : '', + collapseBtnHTML : '', + group : 0, + maxDepth : 5, + threshold : 20, + reject : [], + //method for call when an item has been successfully dropped + //method has 1 argument in which sends an object containing all + //necessary details + dropCallback : null, + // When a node is dragged it is moved to its new location. + // You can set the next option to true to create a copy of the node that is dragged. + cloneNodeOnDrag : false, + // When the node is dragged and released outside its list delete it. + dragOutsideToDelete : false + }; + + function Plugin(element, options) + { + this.w = $(document); + this.el = $(element); + this.options = $.extend({}, defaults, options); + this.init(); + } + + Plugin.prototype = { + + init: function() + { + var list = this; + + list.reset(); + + list.el.data('nestable-group', this.options.group); + + list.placeEl = $('
    '); + + $.each(this.el.find(list.options.itemNodeName), function(k, el) { + list.setParent($(el)); + }); + + list.el.on('click', 'button', function(e) + { + if (list.dragEl || (!hasTouch && e.button !== 0)) { + return; + } + var target = $(e.currentTarget), + action = target.data('action'), + item = target.parent(list.options.itemNodeName); + if (action === 'collapse') { + list.collapseItem(item); + } + if (action === 'expand') { + list.expandItem(item); + } + }); + + var onStartEvent = function(e) + { + var handle = $(e.target); + + list.nestableCopy = handle.closest('.'+list.options.rootClass).clone(true); + + if (!handle.hasClass(list.options.handleClass)) { + if (handle.closest('.' + list.options.noDragClass).length) { + return; + } + handle = handle.closest('.' + list.options.handleClass); + } + if (!handle.length || list.dragEl || (!hasTouch && e.which !== 1) || (hasTouch && e.touches.length !== 1)) { + return; + } + e.preventDefault(); + list.dragStart(hasTouch ? e.touches[0] : e); + }; + + var onMoveEvent = function(e) + { + if (list.dragEl) { + e.preventDefault(); + list.dragMove(hasTouch ? e.touches[0] : e); + } + }; + + var onEndEvent = function(e) + { + if (list.dragEl) { + e.preventDefault(); + list.dragStop(hasTouch ? e.touches[0] : e); + } + }; + + if (hasTouch) { + list.el[0].addEventListener(eStart, onStartEvent, false); + window.addEventListener(eMove, onMoveEvent, false); + window.addEventListener(eEnd, onEndEvent, false); + window.addEventListener(eCancel, onEndEvent, false); + } else { + list.el.on(eStart, onStartEvent); + list.w.on(eMove, onMoveEvent); + list.w.on(eEnd, onEndEvent); + } + + var destroyNestable = function() + { + if (hasTouch) { + list.el[0].removeEventListener(eStart, onStartEvent, false); + window.removeEventListener(eMove, onMoveEvent, false); + window.removeEventListener(eEnd, onEndEvent, false); + window.removeEventListener(eCancel, onEndEvent, false); + } else { + list.el.off(eStart, onStartEvent); + list.w.off(eMove, onMoveEvent); + list.w.off(eEnd, onEndEvent); + } + + list.el.off('click'); + list.el.unbind('destroy-nestable'); + + list.el.data("nestable", null); + + var buttons = list.el[0].getElementsByTagName('button'); + + $(buttons).remove(); + }; + + list.el.bind('destroy-nestable', destroyNestable); + }, + + destroy: function () + { + this.expandAll(); + this.el.trigger('destroy-nestable'); + }, + + serialize: function() + { + var data, + depth = 0, + list = this; + step = function(level, depth) + { + var array = [ ], + items = level.children(list.options.itemNodeName); + items.each(function() + { + var li = $(this), + item = $.extend({}, li.data()), + sub = li.children(list.options.listNodeName); + if (sub.length) { + item.children = step(sub, depth + 1); + } + array.push(item); + }); + return array; + }; + var el; + + if (list.el.is(list.options.listNodeName)) { + el = list.el; + } else { + el = list.el.find(list.options.listNodeName).first(); + } + data = step(el, depth); + + return data; + }, + + reset: function() + { + this.mouse = { + offsetX : 0, + offsetY : 0, + startX : 0, + startY : 0, + lastX : 0, + lastY : 0, + nowX : 0, + nowY : 0, + distX : 0, + distY : 0, + dirAx : 0, + dirX : 0, + dirY : 0, + lastDirX : 0, + lastDirY : 0, + distAxX : 0, + distAxY : 0 + }; + this.moving = false; + this.dragEl = null; + this.dragRootEl = null; + this.dragDepth = 0; + this.dragItem = null; + this.hasNewRoot = false; + this.pointEl = null; + this.sourceRoot = null; + this.isOutsideRoot = false; + }, + + expandItem: function(li) + { + li.removeClass(this.options.collapsedClass); + li.children('[data-action="expand"]').hide(); + li.children('[data-action="collapse"]').show(); + li.children(this.options.listNodeName).show(); + this.el.trigger('expand', [li]); + li.trigger('expand'); + }, + + collapseItem: function(li) + { + var lists = li.children(this.options.listNodeName); + if (lists.length) { + li.addClass(this.options.collapsedClass); + li.children('[data-action="collapse"]').hide(); + li.children('[data-action="expand"]').show(); + li.children(this.options.listNodeName).hide(); + } + this.el.trigger('collapse', [li]); + li.trigger('collapse'); + }, + + expandAll: function() + { + var list = this; + list.el.find(list.options.itemNodeName).each(function() { + list.expandItem($(this)); + }); + }, + + collapseAll: function() + { + var list = this; + list.el.find(list.options.itemNodeName).each(function() { + list.collapseItem($(this)); + }); + }, + + setParent: function(li) + { + if (li.children(this.options.listNodeName).length) { + li.prepend($(this.options.expandBtnHTML)); + li.prepend($(this.options.collapseBtnHTML)); + } + if( (' ' + li[0].className + ' ').indexOf(' ' + defaults.collapsedClass + ' ') > -1 ) + { + li.children('[data-action="collapse"]').hide(); + } else { + li.children('[data-action="expand"]').hide(); + } + }, + + unsetParent: function(li) + { + li.removeClass(this.options.collapsedClass); + li.children('[data-action]').remove(); + li.children(this.options.listNodeName).remove(); + }, + + dragStart: function(e) + { + var mouse = this.mouse, + target = $(e.target), + dragItem = target.closest('.' + this.options.handleClass).closest(this.options.itemNodeName); + + this.sourceRoot = target.closest('.' + this.options.rootClass); + + this.dragItem = dragItem; + + this.placeEl.css('height', dragItem.height()); + + mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left; + mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top; + mouse.startX = mouse.lastX = e.pageX; + mouse.startY = mouse.lastY = e.pageY; + + this.dragRootEl = this.el; + + this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass); + this.dragEl.css('width', dragItem.width()); + + // fix for zepto.js + //dragItem.after(this.placeEl).detach().appendTo(this.dragEl); + if(this.options.cloneNodeOnDrag) { + dragItem.after(dragItem.clone()); + } else { + dragItem.after(this.placeEl); + } + dragItem[0].parentNode.removeChild(dragItem[0]); + dragItem.appendTo(this.dragEl); + + $(document.body).append(this.dragEl); + this.dragEl.css({ + 'left' : e.pageX - mouse.offsetX, + 'top' : e.pageY - mouse.offsetY + }); + // total depth of dragging item + var i, depth, + items = this.dragEl.find(this.options.itemNodeName); + for (i = 0; i < items.length; i++) { + depth = $(items[i]).parents(this.options.listNodeName).length; + if (depth > this.dragDepth) { + this.dragDepth = depth; + } + } + }, + + dragStop: function(e) + { + // fix for zepto.js + //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach()); + var el = this.dragEl.children(this.options.itemNodeName).first(); + el[0].parentNode.removeChild(el[0]); + + if(this.isOutsideRoot && this.options.dragOutsideToDelete) + { + var parent = this.placeEl.parent(); + this.placeEl.remove(); + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + // If all nodes where deleted, create a placeholder element. + if (!this.dragRootEl.find(this.options.itemNodeName).length) + { + this.dragRootEl.append('
    '); + } + } + else + { + this.placeEl.replaceWith(el); + } + + if (!this.moving) + { + $(this.dragItem).trigger('click'); + } + + var i; + var isRejected = false; + for (i in this.options.reject) + { + var reject = this.options.reject[i]; + if (reject.rule.apply(this.dragRootEl)) + { + var nestableDragEl = el.clone(true); + this.dragRootEl.html(this.nestableCopy.children().clone(true)); + if (reject.action) { + reject.action.apply(this.dragRootEl, [nestableDragEl]); + } + + isRejected = true; + break; + } + } + + if (!isRejected) + { + this.dragEl.remove(); + this.el.trigger('change'); + + //Let's find out new parent id + var parentItem = el.parent().parent(); + var parentId = null; + if(parentItem !== null && !parentItem.is('.' + this.options.rootClass)) + parentId = parentItem.data('id'); + + if($.isFunction(this.options.dropCallback)) + { + var details = { + sourceId : el.data('id'), + destId : parentId, + sourceEl : el, + destParent : parentItem, + destRoot : el.closest('.' + this.options.rootClass), + sourceRoot : this.sourceRoot + }; + this.options.dropCallback.call(this, details); + } + + if (this.hasNewRoot) { + this.dragRootEl.trigger('change'); + } + + this.reset(); + } + }, + + dragMove: function(e) + { + var list, parent, prev, next, depth, + opt = this.options, + mouse = this.mouse; + + this.dragEl.css({ + 'left' : e.pageX - mouse.offsetX, + 'top' : e.pageY - mouse.offsetY + }); + + // mouse position last events + mouse.lastX = mouse.nowX; + mouse.lastY = mouse.nowY; + // mouse position this events + mouse.nowX = e.pageX; + mouse.nowY = e.pageY; + // distance mouse moved between events + mouse.distX = mouse.nowX - mouse.lastX; + mouse.distY = mouse.nowY - mouse.lastY; + // direction mouse was moving + mouse.lastDirX = mouse.dirX; + mouse.lastDirY = mouse.dirY; + // direction mouse is now moving (on both axis) + mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1; + mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1; + // axis mouse is now moving on + var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0; + + // do nothing on first move + if (!this.moving) { + mouse.dirAx = newAx; + this.moving = true; + return; + } + + // calc distance moved on this axis (and direction) + if (mouse.dirAx !== newAx) { + mouse.distAxX = 0; + mouse.distAxY = 0; + } else { + mouse.distAxX += Math.abs(mouse.distX); + if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) { + mouse.distAxX = 0; + } + mouse.distAxY += Math.abs(mouse.distY); + if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) { + mouse.distAxY = 0; + } + } + mouse.dirAx = newAx; + + /** + * move horizontal + */ + if (mouse.dirAx && mouse.distAxX >= opt.threshold) { + // reset move distance on x-axis for new phase + mouse.distAxX = 0; + prev = this.placeEl.prev(opt.itemNodeName); + // increase horizontal level if previous sibling exists and is not collapsed + if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass) && !prev.hasClass(opt.noChildrenClass)) { + // cannot increase level when item above is collapsed + list = prev.find(opt.listNodeName).last(); + // check if depth limit has reached + depth = this.placeEl.parents(opt.listNodeName).length; + if (depth + this.dragDepth <= opt.maxDepth) { + // create new sub-level if one doesn't exist + if (!list.length) { + list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass); + list.append(this.placeEl); + prev.append(list); + this.setParent(prev); + } else { + // else append to next level up + list = prev.children(opt.listNodeName).last(); + list.append(this.placeEl); + } + } + } + // decrease horizontal level + if (mouse.distX < 0) { + // we can't decrease a level if an item preceeds the current one + next = this.placeEl.next(opt.itemNodeName); + if (!next.length) { + parent = this.placeEl.parent(); + this.placeEl.closest(opt.itemNodeName).after(this.placeEl); + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + } + } + } + + var isEmpty = false; + + // find list item under cursor + if (!hasPointerEvents) { + this.dragEl[0].style.visibility = 'hidden'; + } + + this.pointEl = $(document.elementFromPoint(e.pageX - document.documentElement.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop))); + + // Check if the node is dragged outside of its list. + if(this.dragRootEl.has(this.pointEl).length) { + this.isOutsideRoot = false; + this.dragEl[0].style.opacity = 1; + } else { + this.isOutsideRoot = true; + this.dragEl[0].style.opacity = 0.5; + } + + // find parent list of item under cursor + var pointElRoot = this.pointEl.closest('.' + opt.rootClass), + isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id'); + + this.isOutsideRoot = !pointElRoot.length; + + if (!hasPointerEvents) { + this.dragEl[0].style.visibility = 'visible'; + } + if (this.pointEl.hasClass(opt.handleClass)) { + this.pointEl = this.pointEl.closest( opt.itemNodeName ); + } + + if (opt.maxDepth == 1 && !this.pointEl.hasClass(opt.itemClass)) { + this.pointEl = this.pointEl.closest("." + opt.itemClass); + } + + if (this.pointEl.hasClass(opt.emptyClass)) { + isEmpty = true; + } + else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) { + return; + } + + /** + * move vertical + */ + if (!mouse.dirAx || isNewRoot || isEmpty) { + // check if groups match if dragging over new root + if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) { + return; + } + // check depth limit + depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length; + if (depth > opt.maxDepth) { + return; + } + var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2); + parent = this.placeEl.parent(); + // if empty create new list to replace empty placeholder + if (isEmpty) { + list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass); + list.append(this.placeEl); + this.pointEl.replaceWith(list); + } + else if (before) { + this.pointEl.before(this.placeEl); + } + else { + this.pointEl.after(this.placeEl); + } + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + if (!this.dragRootEl.find(opt.itemNodeName).length) { + this.dragRootEl.append('
    '); + } + // parent root list has changed + this.dragRootEl = pointElRoot; + if (isNewRoot) { + this.hasNewRoot = this.el[0] !== this.dragRootEl[0]; + } + } + } + + }; + + $.fn.nestable = function(params) + { + var lists = this, + retval = this; + + var generateUid = function (separator) { + var delim = separator || "-"; + + function S4() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + } + + return (S4() + S4() + delim + S4() + delim + S4() + delim + S4() + delim + S4() + S4() + S4()); + }; + + lists.each(function() + { + var plugin = $(this).data("nestable"); + + if (!plugin) { + $(this).data("nestable", new Plugin(this, params)); + $(this).data("nestable-id", generateUid()); + } else { + if (typeof params === 'string' && typeof plugin[params] === 'function') { + retval = plugin[params](); + } + } + }); + + return retval || lists; + }; + +})(window.jQuery || window.Zepto, window, document);