diff --git a/README.md b/README.md index b1de11e2c..a5c5dadeb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Elasticsearch -This repository contains ActiveModel, ActiveRecord and Ruby on Rails integrations for -[Elasticsearch](http://elasticsearch.org): +This repository contains various Ruby and Rails integrations for [Elasticsearch](http://elasticsearch.org): * ActiveModel integration with adapters for ActiveRecord and Mongoid +* _Repository Pattern_ based persistence layer for Ruby objects * Enumerable-based wrapper for search results * ActiveRecord::Relation-based wrapper for returning search results as records * Convenience model methods such as `search`, `mapping`, `import`, etc @@ -41,13 +41,16 @@ or install it from a source code checkout: ## Usage -This project is split into two separate gems: +This project is split into three separate gems: * [**`elasticsearch-model`**](https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-model), - which contains model-related features such as setting up indices, `search` method, pagination, etc + which contains search integration for Ruby/Rails models such as ActiveRecord::Base and Mongoid, + +* [**`elasticsearch-persistence`**](https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-persistence), + which provides standalone persistence layer for Ruby/Rails objects and models * [**`elasticsearch-rails`**](https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-rails), - which contains features for Ruby on Rails applications + which contains various features for Ruby on Rails applications Example of a basic integration into an ActiveRecord-based model: @@ -64,13 +67,30 @@ Article.import @articles = Article.search('foobar').records ``` +Example of using Elasticsearch as a repository for a Ruby model: + +```ruby +require 'virtus' +class Article + include Virtus.model + attribute :title, String +end + +require 'elasticsearch/persistence' +repository = Elasticsearch::Persistence::Repository.new + +repository.save Article.new(title: 'Test') +# POST http://localhost:9200/repository/article [status:201, request:0.760s, query:n/a] +# => {"_index"=>"repository", "_type"=>"article", "_id"=>"Ak75E0U9Q96T5Y999_39NA", ...} +``` + You can generate a fully working Ruby on Rails application with a single command: ```bash rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/01-basic.rb ``` -Please refer to each library documentation for detailed information and examples. +**Please refer to each library documentation for detailed information and examples.** ### Model @@ -78,6 +98,12 @@ Please refer to each library documentation for detailed information and examples * [[Documentation]](http://rubydoc.info/gems/elasticsearch-model/) * [[Test Suite]](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-model/test) +### Persistence + +* [[README]](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-persistence/README.md) +* [[Documentation]](http://rubydoc.info/gems/elasticsearch-persistence/) +* [[Test Suite]](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-persistence/test) + ### Rails * [[README]](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-rails/README.md) diff --git a/Rakefile b/Rakefile index 7c5ef852c..61d48d28e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ require 'pathname' -subprojects = %w| elasticsearch-model elasticsearch-rails | +subprojects = %w| elasticsearch-model elasticsearch-rails elasticsearch-persistence | __current__ = Pathname( File.expand_path('..', __FILE__) ) diff --git a/elasticsearch-model/CHANGELOG.md b/elasticsearch-model/CHANGELOG.md index 818fb3f07..734599d4f 100644 --- a/elasticsearch-model/CHANGELOG.md +++ b/elasticsearch-model/CHANGELOG.md @@ -1,3 +1,5 @@ +## 0.1.3 + ## 0.1.2 * Properly delegate existence methods like `result.foo?` to `result._source.foo` diff --git a/elasticsearch-model/lib/elasticsearch/model/response.rb b/elasticsearch-model/lib/elasticsearch/model/response.rb index 04e802eda..b62ba822c 100644 --- a/elasticsearch-model/lib/elasticsearch/model/response.rb +++ b/elasticsearch-model/lib/elasticsearch/model/response.rb @@ -27,7 +27,9 @@ def initialize(klass, search, options={}) # @return [Hash] # def response - @response ||= search.execute! + @response ||= begin + Hashie::Mash.new(search.execute!) + end end # Returns the collection of "hits" from Elasticsearch diff --git a/elasticsearch-model/lib/elasticsearch/model/version.rb b/elasticsearch-model/lib/elasticsearch/model/version.rb index b1692ab2e..16d1b7f24 100644 --- a/elasticsearch-model/lib/elasticsearch/model/version.rb +++ b/elasticsearch-model/lib/elasticsearch/model/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Model - VERSION = "0.1.2" + VERSION = "0.1.3" end end diff --git a/elasticsearch-model/test/integration/active_record_basic_test.rb b/elasticsearch-model/test/integration/active_record_basic_test.rb index 7f9a1c9ed..45d41a6eb 100644 --- a/elasticsearch-model/test/integration/active_record_basic_test.rb +++ b/elasticsearch-model/test/integration/active_record_basic_test.rb @@ -164,6 +164,14 @@ class ::Article < ActiveRecord::Base assert_equal 'Testing Coding', response.records.order('title DESC').first.title end end + + should "allow dot access to response" do + response = Article.search query: { match: { title: { query: 'test' } } }, + aggregations: { dates: { date_histogram: { field: 'created_at', interval: 'hour' } } } + + response.response.respond_to?(:aggregations) + assert_equal 2, response.response.aggregations.dates.buckets.first.doc_count + end end end diff --git a/elasticsearch-model/test/unit/response_test.rb b/elasticsearch-model/test/unit/response_test.rb index 3f0a36c1a..ff14e5b22 100644 --- a/elasticsearch-model/test/unit/response_test.rb +++ b/elasticsearch-model/test/unit/response_test.rb @@ -25,6 +25,16 @@ def self.document_type; 'bar'; end assert_equal 'OK', response.shards.one end + should "wrap the raw Hash response in Hashie::Mash" do + @search = Elasticsearch::Model::Searching::SearchRequest.new OriginClass, '*' + @search.stubs(:execute!).returns({'hits' => { 'hits' => [] }, 'aggregations' => { 'dates' => 'FOO' }}) + + response = Elasticsearch::Model::Response::Response.new OriginClass, @search + + assert_respond_to response.response, :aggregations + assert_equal 'FOO', response.response.aggregations.dates + end + should "load and access the results" do @search.expects(:execute!).returns(RESPONSE) diff --git a/elasticsearch-persistence/CHANGELOG.md b/elasticsearch-persistence/CHANGELOG.md new file mode 100644 index 000000000..8abc96421 --- /dev/null +++ b/elasticsearch-persistence/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.1.3 + +* Released the "elasticsearch-persistence" Rubygem + +## 0.0.1 + +* Initial infrastructure for the gem diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md index 5af250444..df0a866f2 100644 --- a/elasticsearch-persistence/README.md +++ b/elasticsearch-persistence/README.md @@ -416,17 +416,248 @@ results.response._shards.failed #### Example Application -An example Sinatra application is available in -[`examples/sinatra/application.rb`](examples/sinatra/application.rb), -and demonstrates a rich set of features of the repository. +An example Sinatra application is available in [`examples/notes/application.rb`](examples/notes/application.rb), +and demonstrates a rich set of features: +* How to create and configure a custom repository class +* How to work with a plain Ruby class as the domain object +* How to integrate the repository with a Sinatra application +* How to write complex search definitions, including pagination, highlighting and aggregations +* How to use search results in the application view ### The ActiveRecord Pattern -[_Work in progress_](https://github.com/elasticsearch/elasticsearch-rails/pull/91). -The ActiveRecord [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html) will work -in a very similar way as `Tire::Model::Persistence`, allowing a drop-in replacement of -an Elasticsearch-backed model in Ruby on Rails applications. +The `Elasticsearch::Persistence::Model` module provides an implementation of the +active record [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html), +with a familiar interface for using Elasticsearch as a persistence layer in +Ruby on Rails applications. + +All the methods are documented with comprehensive examples in the source code, +available also online at . + +#### Installation/Usage + +To use the library in a Rails application, add it to your `Gemfile` with a `require` statement: + +```ruby +gem "elasticsearch-persistence", require: 'elasticsearch/persistence/model' +``` + +To use the library without Bundler, install it, and require the file: + +```bash +gem install elasticsearch-persistence +``` + +```ruby +# In your code +require 'elasticsearch/persistence/model' +``` + +#### Model Definition + +The integration is implemented by including the module in a Ruby class. +The model attribute definition support is implemented with the +[_Virtus_](https://github.com/solnic/virtus) Rubygem, and the +naming, validation, etc. features with the +[_ActiveModel_](https://github.com/rails/rails/tree/master/activemodel) Rubygem. + +```ruby +class Article + include Elasticsearch::Persistence::Model + + # Define a plain `title` attribute + # + attribute :title, String + + # Define an `author` attribute, with multiple analyzers for this field + # + attribute :author, String, mapping: { fields: { + author: { type: 'string'}, + raw: { type: 'string', analyzer: 'keyword' } + } } + + + # Define a `views` attribute, with default value + # + attribute :views, Integer, default: 0, mapping: { type: 'integer' } + + # Validate the presence of the `title` attribute + # + validates :title, presence: true + + # Execute code after saving the model. + # + after_save { puts "Successfuly saved: #{self}" } +end +``` + +Attribute validations works like for any other _ActiveModel_-compatible implementation: + +```ruby +article = Article.new # => #
+ +article.valid? +# => false + +article.errors.to_a +# => ["Title can't be blank"] +``` + +#### Persistence + +We can create a new article in the database... + +```ruby +Article.create id: 1, title: 'Test', author: 'John' +# PUT http://localhost:9200/articles/article/1 [status:201, request:0.015s, query:n/a] +``` + +... and find it: + +```ruby +article = Article.find(1) +# => #
+ +article._index +# => "articles" + +article.id +# => "1" + +article.title +# => "Test" +``` + +To update the model, either update the attribute and save the model: + +```ruby +article.title = 'Updated' + +article.save +=> {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>2, "created"=>false} +``` + +... or use the `update_attributes` method: + +```ruby +article.update_attributes title: 'Test', author: 'Mary' +# => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>3} +``` + +The implementation supports the familiar interface for updating model timestamps: + +```ruby +article.touch +# => => { ... "_version"=>4} +``` + +... and numeric attributes: + +```ruby +article.views +# => 0 + +article.increment :views +article.views +# => 1 +``` + +Any callbacks defined in the model will be triggered during the persistence operations: + +```ruby +article.save +# Successfuly saved: #
+``` + +The model also supports familiar `find_in_batches` and `find_each` methods to efficiently +retrieve big collections of model instance, using the Elasticsearch's _Scan API_: + +```ruby +Article.find_each(_source_include: 'title') { |a| puts "===> #{a.title.upcase}" } +# GET http://localhost:9200/articles/article/_search?scroll=5m&search_type=scan&size=20 +# GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb... +# ===> TEST +# GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb... +# => "c2Nhb..." +``` + +#### Search + +The model class provides a `search` method to retrieve model instances with a regular +search definition, including highlighting, aggregations, etc: + +```ruby +results = Article.search query: { match: { title: 'test' } }, + aggregations: { authors: { terms: { field: 'author.raw' } } }, + highlight: { fields: { title: {} } } + +puts results.first.title +# Test + +puts results.first.hit.highlight['title'] +# Test + +puts results.response.aggregations.authors.buckets.each { |b| puts "#{b['key']} : #{b['doc_count']}" } +# John : 1 +``` + +#### Accessing the Repository Gateway + +The Elasticsearch integration is implemented by embedding the repository object in the model. +You can access it through the `gateway` method: + +```ruby +Artist.gateway.client.info +# GET http://localhost:9200/ [status:200, request:0.011s, query:n/a] +# => {"status"=>200, "name"=>"Lightspeed", ...} +``` + +#### Rails Compatibility + +The model instances are fully compatible with Rails' conventions and helpers: + +```ruby +url_for article +# => "http://localhost:3000/articles/1" + +div_for article +# => '
' +``` + +... as well as form values for dates and times: + +```ruby +article = Article.new "title" => "Date", "published(1i)"=>"2014", "published(2i)"=>"1", "published(3i)"=>"1" + +article.published.iso8601 +# => "2014-01-01" +``` + +The library provides a Rails ORM generator: + +```bash +rails generate scaffold Person name:String email:String birthday:Date --orm=elasticsearch +``` + +#### Example application + +A fully working Ruby on Rails application can be generated with the following command: + +```bash +rails new music --force --skip --skip-bundle --skip-active-record --template https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/template.rb +``` + +The application demonstrates: + +* How to set up model attributes with custom mappings +* How to configure model relationships with Elasticsearch's parent/child +* How to configure models to use a common index, and create the index with proper mappings +* How to use Elasticsearch's completion suggester to drive auto-complete functionality +* How to use Elasticsearch-persisted model in Rails' views and forms +* How to write controller tests + +The source files for the application are available in the [`examples/music`](examples/music) folder. ## License diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index b0b618a78..5c1c8f752 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -26,13 +26,16 @@ Gem::Specification.new do |s| s.add_dependency "elasticsearch", '> 0.4' s.add_dependency "elasticsearch-model", '>= 0.1' s.add_dependency "activesupport", '> 3' + s.add_dependency "activemodel", '> 3' s.add_dependency "hashie" + s.add_dependency "virtus" s.add_development_dependency "bundler", "~> 1.5" s.add_development_dependency "rake" s.add_development_dependency "oj" - s.add_development_dependency "virtus" + + s.add_development_dependency "rails" s.add_development_dependency "elasticsearch-extensions" diff --git a/elasticsearch-persistence/examples/music/album.rb b/elasticsearch-persistence/examples/music/album.rb new file mode 100644 index 000000000..124e643ed --- /dev/null +++ b/elasticsearch-persistence/examples/music/album.rb @@ -0,0 +1,34 @@ +class Meta + include Virtus.model + + attribute :rating + attribute :have + attribute :want + attribute :formats +end + +class Album + include Elasticsearch::Persistence::Model + + index_name [Rails.application.engine_name, Rails.env].join('-') + + mapping _parent: { type: 'artist' } do + indexes :suggest_title, type: 'completion', payloads: true + indexes :suggest_track, type: 'completion', payloads: true + end + + attribute :artist + attribute :artist_id, String, mapping: { index: 'not_analyzed' } + attribute :label, Hash, mapping: { type: 'object' } + + attribute :title + attribute :suggest_title, String, default: {}, mapping: { type: 'completion', payloads: true } + attribute :released, Date + attribute :notes + attribute :uri + + attribute :tracklist, Array, mapping: { type: 'object' } + + attribute :styles + attribute :meta, Meta, mapping: { type: 'object' } +end diff --git a/elasticsearch-persistence/examples/music/artist.rb b/elasticsearch-persistence/examples/music/artist.rb new file mode 100644 index 000000000..bcf123dc3 --- /dev/null +++ b/elasticsearch-persistence/examples/music/artist.rb @@ -0,0 +1,50 @@ +class Artist + include Elasticsearch::Persistence::Model + + index_name [Rails.application.engine_name, Rails.env].join('-') + + analyzed_and_raw = { fields: { + name: { type: 'string', analyzer: 'snowball' }, + raw: { type: 'string', analyzer: 'keyword' } + } } + + attribute :name, String, mapping: analyzed_and_raw + attribute :suggest_name, String, default: {}, mapping: { type: 'completion', payloads: true } + + attribute :profile + attribute :date, Date + + attribute :members, String, default: [], mapping: analyzed_and_raw + attribute :members_combined, String, default: [], mapping: { analyzer: 'snowball' } + attribute :suggest_member, String, default: {}, mapping: { type: 'completion', payloads: true } + + attribute :urls, String, default: [] + attribute :album_count, Integer, default: 0 + + validates :name, presence: true + + def albums + Album.search( + { query: { + has_parent: { + type: 'artist', + query: { + filtered: { + filter: { + ids: { values: [ self.id ] } + } + } + } + } + }, + sort: 'released', + size: 100 + }, + { type: 'album' } + ) + end + + def to_param + [id, name.parameterize].join('-') + end +end diff --git a/elasticsearch-persistence/examples/music/artists/_form.html.erb b/elasticsearch-persistence/examples/music/artists/_form.html.erb new file mode 100644 index 000000000..55273679c --- /dev/null +++ b/elasticsearch-persistence/examples/music/artists/_form.html.erb @@ -0,0 +1,8 @@ +<%= simple_form_for @artist do |f| %> + <%= f.input :name %> + <%= f.input :profile, as: :text %> + <%= f.input :date, as: :date %> + <%= f.input :members, hint: 'Separate names by comma', input_html: { value: f.object.members.join(', ') } %> + + <%= f.button :submit %> +<% end %> diff --git a/elasticsearch-persistence/examples/music/artists/artists_controller.rb b/elasticsearch-persistence/examples/music/artists/artists_controller.rb new file mode 100644 index 000000000..458c243f7 --- /dev/null +++ b/elasticsearch-persistence/examples/music/artists/artists_controller.rb @@ -0,0 +1,67 @@ +class ArtistsController < ApplicationController + before_action :set_artist, only: [:show, :edit, :update, :destroy] + + rescue_from Elasticsearch::Persistence::Repository::DocumentNotFound do + render file: "public/404.html", status: 404, layout: false + end + + def index + @artists = Artist.all sort: 'name.raw', _source: ['name', 'album_count'] + end + + def show + @albums = @artist.albums + end + + def new + @artist = Artist.new + end + + def edit + end + + def create + @artist = Artist.new(artist_params) + + respond_to do |format| + if @artist.save refresh: true + format.html { redirect_to @artist, notice: 'Artist was successfully created.' } + format.json { render :show, status: :created, location: @artist } + else + format.html { render :new } + format.json { render json: @artist.errors, status: :unprocessable_entity } + end + end + end + + def update + respond_to do |format| + if @artist.update(artist_params, refresh: true) + format.html { redirect_to @artist, notice: 'Artist was successfully updated.' } + format.json { render :show, status: :ok, location: @artist } + else + format.html { render :edit } + format.json { render json: @artist.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @artist.destroy refresh: true + respond_to do |format| + format.html { redirect_to artists_url, notice: 'Artist was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + def set_artist + @artist = Artist.find(params[:id].split('-').first) + end + + def artist_params + a = params.require(:artist) + a[:members] = a[:members].split(/,\s?/) unless a[:members].is_a?(Array) || a[:members].blank? + return a + end +end diff --git a/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb b/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb new file mode 100644 index 000000000..3307f5e47 --- /dev/null +++ b/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb @@ -0,0 +1,53 @@ +require 'test_helper' + +class ArtistsControllerTest < ActionController::TestCase + setup do + IndexManager.create_index force: true + @artist = Artist.create(id: 1, name: 'TEST') + Artist.gateway.refresh_index! + end + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:artists) + end + + test "should get new" do + get :new + assert_response :success + end + + test "should create artist" do + assert_difference('Artist.count') do + post :create, artist: { name: @artist.name } + Artist.gateway.refresh_index! + end + + assert_redirected_to artist_path(assigns(:artist)) + end + + test "should show artist" do + get :show, id: @artist + assert_response :success + end + + test "should get edit" do + get :edit, id: @artist + assert_response :success + end + + test "should update artist" do + patch :update, id: @artist, artist: { name: @artist.name } + assert_redirected_to artist_path(assigns(:artist)) + end + + test "should destroy artist" do + assert_difference('Artist.count', -1) do + delete :destroy, id: @artist + Artist.gateway.refresh_index! + end + + assert_redirected_to artists_path + end +end diff --git a/elasticsearch-persistence/examples/music/artists/index.html.erb b/elasticsearch-persistence/examples/music/artists/index.html.erb new file mode 100644 index 000000000..c0c0c7304 --- /dev/null +++ b/elasticsearch-persistence/examples/music/artists/index.html.erb @@ -0,0 +1,57 @@ +
+

+ Artists + <%= button_to 'New Artist', new_artist_path, method: 'get', tabindex: 5 %> +

+
+ + + +
+ <% @artists.each do |artist| %> + <%= div_for artist, class: 'result clearfix' do %> +

+ <%= link_to artist do %> + <%= artist.name %> + <%= pluralize artist.album_count, 'album' %> + <% end %> +

+
+ <%= button_to 'Edit', edit_artist_path(artist), method: 'get' %> + <%= button_to 'Destroy', artist, method: :delete, data: { confirm: 'Are you sure?' } %> +
+ <% end %> + <% end %> +
+ +<% if @artists.empty? %> +
+

The search hasn't returned any results...

+
+<% end %> + + diff --git a/elasticsearch-persistence/examples/music/artists/show.html.erb b/elasticsearch-persistence/examples/music/artists/show.html.erb new file mode 100644 index 000000000..984f1f726 --- /dev/null +++ b/elasticsearch-persistence/examples/music/artists/show.html.erb @@ -0,0 +1,51 @@ +
+

+ <%= link_to "〈".html_safe, artists_path, title: "Back" %> + <%= @artist.name %> + <%= button_to 'Edit', edit_artist_path(@artist), method: 'get' %> +

+
+ +

<%= notice %>

+ +
+ <%= @artist.members.to_sentence last_word_connector: ' and ' %> | + <%= pluralize @albums.size, 'album' %> +

<%= @artist.profile %>

+
+ +
+ <% @albums.each do |album| %> + <%= div_for album, class: 'clearfix' do %> +

+ <%= album.title %> +
+ <%= album.meta.formats.join(', ') %> + <%= album.released %> +
+

+ +
+ <%= image_tag "http://ruby-demo-assets.s3.amazonaws.com/discogs/covers/#{album.id}.jpeg", width: '100px', class: 'cover' %> +
+ +
+ <% album.tracklist.in_groups_of(album.tracklist.size/2+1).each_with_index do |half, g| %> +
    start="<%= g < 1 ? 1 : album.tracklist.size/2+2 %>"> + <% half.compact.each_with_index do |track, i| %> +
  • + <%= g < 1 ? i+1 : i+(g*album.tracklist.size/2+2) %> + <%= track['title'] %> + <%= track['duration'] %> +
  • + <% end %> +
+ <% end %> +
+ <% end %> + + <% end %> + + + +
diff --git a/elasticsearch-persistence/examples/music/assets/application.css b/elasticsearch-persistence/examples/music/assets/application.css new file mode 100644 index 000000000..7be6447f4 --- /dev/null +++ b/elasticsearch-persistence/examples/music/assets/application.css @@ -0,0 +1,226 @@ +/* + *= require_tree . + *= require_self + *= require ui-lightness/jquery-ui-1.10.4.custom.min.css + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +body { + font-family: 'Helvetica Neue', Helvetica, sans-serif !important; + margin: 2em 4em; +} + +header { + margin: 0; + padding: 0 0 1em 0; + border-bottom: 1px solid #666; +} + +header h1 { + color: #999; + font-weight: 100; + text-transform: uppercase; + margin: 0; padding: 0; +} + +header a { + color: #0b6aff; + text-decoration: none; +} + +header .back { + font-size: 100%; + margin: 0 0.5em 0 -0.5em; +} + +h1 form { + float: right; +} + +#searchbox { + border-bottom: 1px solid #666; +} + +#searchbox input { + color: #444; + font-size: 100%; + font-weight: 100; + border: none; + padding: 1em 0 1em 0; + width: 100%; +} + +#searchbox input:focus { + outline-width: 0; +} + +.actions form { + float: right; + position: relative; + top: 0.2em; +} + +.no-results { + font-weight: 200; + font-size: 200%; +} + +.result, +.artist { + padding: 1em 0 1em 0; + margin: 0; + border-bottom: 1px solid #999; +} + +.result:hover, +.artist:hover { + background: #f9f9f9; +} + +.result h2, +.artist h2 { + color: #444; + margin: 0; + padding: 0; +} + +.artist h2 { + float: left; +} + +.result h2 a, +.artist h2 a { + color: #444; +} + +.result h2 small, +.artist h2 small { + font-size: 70%; + font-weight: 100; + margin-left: 0.5em; +} + +.result h2 a, +.artist h2 a { + text-decoration: none; +} + +.result h2 a:hover name, +.artist h2 a:hover .name { + text-decoration: underline; +} + +.result .small { + font-size: 90%; + font-weight: 200; + padding: 0; + margin: 0.25em 0 0.25em 0; +} + +.result .small .label { + color: #999; + font-size: 80%; + min-width: 5em; + display: inline-block; +} + +.artist-info { + color: #5f5f5f; + text-transform: uppercase; + font-weight: 200; + border-bottom: 1px solid #666; + padding: 0 0 1em 0; + margin: 0 0 1em 0; +} + +.artist-profile { + color: #999; + font-size: 95%; + font-weight: 100; + text-transform: none; + padding: 0; + margin: 0.25em 0 0 0; +} + +.album { + margin: 0 0 4em 0; +} + +.album .cover { + float: left; + width: 150px; +} + +.album .cover img { + border: 1px solid rgba(0,0,0,0.15); + box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.05); +} + +.album .content { + float: left; + margin-left: 25px; +} + +.album .content ul { + float: left; + margin: 0 2em 0 0; + padding: 0; + min-width: 18em; +} + +.album .content ul li { + line-height: 1.5em; + padding: 0.5em 0 0.5em 0; + border-bottom:1px solid #f8f8f8; + list-style: none; +} + +.album .content ul li .counter { + color: #999; + font-style: normal; + font-size: 80%; + font-weight: 100; + margin-right: 0.5em; +} + +.album h3 { + margin: 0; padding: 0; + border-bottom: 2px solid #e0e0e0; + padding: 0 0 0.5em 0; + margin: 0 0 1em 0; +} + +.album h3 .title { + text-transform: uppercase; + font-weight: 200; +} + +.album small { + color: #a3a3a3; + font-weight: 200; +} + +.album .info { + float: right; +} + +em[class^=hl] { + font-style: normal; + background: #e6efff; + padding: 0.15em 0.35em; + border-radius: 5px; +} \ No newline at end of file diff --git a/elasticsearch-persistence/examples/music/assets/autocomplete.css b/elasticsearch-persistence/examples/music/assets/autocomplete.css new file mode 100644 index 000000000..7f2340969 --- /dev/null +++ b/elasticsearch-persistence/examples/music/assets/autocomplete.css @@ -0,0 +1,48 @@ +.ui-autocomplete { + font-family: 'Helvetica Neue', Helvetica, sans-serif !important; + border: none !important; + border-radius: 0 !important; + background-color: #fff !important; + margin: 0 !important; + padding: 0 !important; + box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.75); +} + +.ui-autocomplete-category { + color: #fff; + background: #222; + font-size: 90%; + font-weight: 300; + text-transform: uppercase; + margin: 0 !important; + padding: 0.25em 0.5em 0.25em 0.5em; +} + +.ui-autocomplete-item { + border-bottom: 1px solid #000; + margin: 0 !important; + padding: 0 !important; +} + +.ui-autocomplete-item:hover, +.ui-autocomplete-item:focus { + color: #fff !important; + background: #0b6aff !important; +} + +.ui-state-focus, +.ui-state-focus a, +.ui-state-active, +.ui-state-active a, +.ui-autocomplete-item:hover a { + color: #fff !important; + background: #0b6aff !important; + outline: none !important; + border: none !important; + border-radius: 0 !important; +} + +a.ui-state-focus, +a.ui-state-active { + margin: 0px !important; +} diff --git a/elasticsearch-persistence/examples/music/assets/blank_cover.png b/elasticsearch-persistence/examples/music/assets/blank_cover.png new file mode 100644 index 000000000..8c513407a Binary files /dev/null and b/elasticsearch-persistence/examples/music/assets/blank_cover.png differ diff --git a/elasticsearch-persistence/examples/music/assets/form.css b/elasticsearch-persistence/examples/music/assets/form.css new file mode 100644 index 000000000..3a937e310 --- /dev/null +++ b/elasticsearch-persistence/examples/music/assets/form.css @@ -0,0 +1,113 @@ +/* Based on https://github.com/plataformatec/simple_form/wiki/CSS-for-simple_form */ + +body.edit h1, +body.new h1 { + color: #999; + font-size: 100%; + text-transform: uppercase; + margin: 0 0 1em 5.5em; +} + +body.edit a[href^="/artists"], +body.new a[href^="/artists"], +body.edit a[href^="/music/artists"], +body.new a[href^="/music/artists"] { + color: #222; + background: #ccc; + text-decoration: none; + border-radius: 0.3em; + padding: 0.25em 0.5em; + margin: 2em 0 0 5.5em; + display: inline-block; +} + +body.edit a[href^="/artists"]:hover, +body.new a[href^="/artists"]:hover, +body.edit a[href^="/music/artists"]:hover, +body.new a[href^="/music/artists"]:hover { + color: #fff; + background: #333; +} + +body.edit a[href^="/artists"]:last-child, +body.new a[href^="/artists"]:last-child, +body.edit a[href^="/music/artists"]:last-child, +body.new a[href^="/music/artists"]:last-child { + margin-left: 0; +} + +.simple_form div.input { + margin-bottom: 1em; + clear: both; +} + +.simple_form label { + color: #878787; + font-size: 80%; + text-transform: uppercase; + font-weight: 200; + float: left; + width: 5em; + text-align: right; + margin: 0.25em 1em; +} + +div.boolean, .simple_form input[type='submit'] { + margin-left: 8.5em; +} + +.field_with_errors input { + border: 2px solid #c70008 !important; +} + +.simple_form .error { + color: #fff !important; + background: #c70008; + font-weight: bold; + clear: left; + display: block; + padding: 0.25em 0.5em; + margin-left: 5.6em; + width: 27.45em; +} + +.simple_form .hint { + color: #878787; + font-size: 80%; + font-style: italic; + display: block; + margin: 0.25em 0 0 7em; + clear: left; +} + +input { + margin: 0; +} + +input.radio { + margin-right: 5px; + vertical-align: -3px; +} + +input.check_boxes { + margin-left: 3px; + vertical-align: -3px; +} + +label.collection_check_boxes { + float: none; + margin: 0; + vertical-align: -2px; + margin-left: 2px; +} + +input.string, +textarea.text { + padding: 0.5em; + min-width: 40em; + border: 1px solid #ccc; +} + +textarea.text { + min-height: 5em; +} diff --git a/elasticsearch-persistence/examples/music/index_manager.rb b/elasticsearch-persistence/examples/music/index_manager.rb new file mode 100644 index 000000000..fae8a93d1 --- /dev/null +++ b/elasticsearch-persistence/examples/music/index_manager.rb @@ -0,0 +1,60 @@ +require 'open-uri' + +class IndexManager + def self.create_index(options={}) + client = Artist.gateway.client + index_name = Artist.index_name + + client.indices.delete index: index_name rescue nil if options[:force] + + settings = Artist.settings.to_hash.merge(Album.settings.to_hash) + mappings = Artist.mappings.to_hash.merge(Album.mappings.to_hash) + + client.indices.create index: index_name, + body: { + settings: settings.to_hash, + mappings: mappings.to_hash } + end + + def self.import_from_yaml(source, options={}) + create_index force: true if options[:force] + + input = open(source) + artists = YAML.load_documents input + + artists.each do |artist| + Artist.create artist.update( + 'album_count' => artist['releases'].size, + 'members_combined' => artist['members'].join(', '), + 'suggest_name' => { + 'input' => artist['namevariations'].unshift(artist['name']), + 'output' => artist['name'], + 'payload' => { 'url' => "/artists/#{artist['id']}" } + }, + 'suggest_member' => { + 'input' => artist['members'], + 'output' => artist['name'], + 'payload' => { 'url' => "/artists/#{artist['id']}" } + } + ) + + artist['releases'].each do |album| + album.update( + 'suggest_title' => { + 'input' => album['title'], + 'output' => album['title'], + 'payload' => { 'url' => "/artists/#{artist['id']}#album_#{album['id']}" } + }, + 'suggest_track' => { + 'input' => album['tracklist'].map { |d| d['title'] }, + 'output' => album['title'], + 'payload' => { 'url' => "/artists/#{artist['id']}#album_#{album['id']}" } + } + ) + album['notes'] = album['notes'].to_s.gsub(/<.+?>/, '').gsub(/ {2,}/, '') + album['released'] = nil if album['released'] < 1 + Album.create album, id: album['id'], parent: artist['id'] + end + end + end +end diff --git a/elasticsearch-persistence/examples/music/search/index.html.erb b/elasticsearch-persistence/examples/music/search/index.html.erb new file mode 100644 index 000000000..aed20f590 --- /dev/null +++ b/elasticsearch-persistence/examples/music/search/index.html.erb @@ -0,0 +1,93 @@ +
+

+ <%= link_to "〈".html_safe, :back, title: "Back" %> + Artists & Albums +

+
+ + + +
+ <% @artists.each do |artist| %> + <%= content_tag :div, class: 'result clearfix' do %> +

+ <%= link_to artist do %> + <%= highlighted(artist, :name) %> + <%= pluralize artist.album_count, 'album' %> + <% end %> +

+ <% if highlight = highlight(artist, :members_combined) %> +

+ Members + <%= highlight.first.html_safe %> +

+ <% end %> + <% if highlight = highlight(artist, :profile) %> +

+ Profile + <%= highlight.join('…').html_safe %> +

+ <% end %> + <% end %> + <% end %> +
+ +
+ <% @albums.each do |album| %> + <%= content_tag :div, class: 'result clearfix' do %> +

+ <%= link_to artist_path(album.artist_id, anchor: "album_#{album.id}") do %> + <%= highlighted(album, :title) %> + <%= album.artist %> + (<%= [album.meta.formats.first, album.released].compact.join(' ') %>) + <% end %> +

+ + <% if highlight = highlight(album, 'tracklist.title') %> +

+ Tracks + <%= highlight.join('…').html_safe %> +

+ <% end %> + + <% if highlight = highlight(album, :notes) %> +

+ Notes + <%= highlight.map { |d| d.gsub(/^\.\s?/, '') }.join('…').html_safe %> +

+ <% end %> + <% end %> + <% end %> +
+ +<% if @artists.empty? && @albums.empty? %> +
+

The search hasn't returned any results...

+
+<% end %> + + diff --git a/elasticsearch-persistence/examples/music/search/search_controller.rb b/elasticsearch-persistence/examples/music/search/search_controller.rb new file mode 100644 index 000000000..bb845c5b6 --- /dev/null +++ b/elasticsearch-persistence/examples/music/search/search_controller.rb @@ -0,0 +1,41 @@ +class SearchController < ApplicationController + + def index + tags = { pre_tags: '', post_tags: '' } + @artists = Artist.search \ + query: { + multi_match: { + query: params[:q], + fields: ['name^10','members^2','profile'] + } + }, + highlight: { + tags_schema: 'styled', + fields: { + name: { number_of_fragments: 0 }, + members_combined: { number_of_fragments: 0 }, + profile: { fragment_size: 50 } + } + } + + @albums = Album.search \ + query: { + multi_match: { + query: params[:q], + fields: ['title^100','tracklist.title^10','notes^1'] + } + }, + highlight: { + tags_schema: 'styled', + fields: { + title: { number_of_fragments: 0 }, + 'tracklist.title' => { number_of_fragments: 0 }, + notes: { fragment_size: 50 } + } + } + end + + def suggest + render json: Suggester.new(params) + end +end diff --git a/elasticsearch-persistence/examples/music/search/search_controller_test.rb b/elasticsearch-persistence/examples/music/search/search_controller_test.rb new file mode 100644 index 000000000..308bad200 --- /dev/null +++ b/elasticsearch-persistence/examples/music/search/search_controller_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class SearchControllerTest < ActionController::TestCase + test "should get suggest" do + get :suggest + assert_response :success + end + +end diff --git a/elasticsearch-persistence/examples/music/search/search_helper.rb b/elasticsearch-persistence/examples/music/search/search_helper.rb new file mode 100644 index 000000000..65a57c322 --- /dev/null +++ b/elasticsearch-persistence/examples/music/search/search_helper.rb @@ -0,0 +1,15 @@ +module SearchHelper + + def highlight(object, field) + object.try(:hit).try(:highlight).try(field) + end + + def highlighted(object, field) + if h = object.try(:hit).try(:highlight).try(field).try(:first) + h.html_safe + else + field.to_s.split('.').reduce(object) { |result,item| result.try(item) } + end + end + +end diff --git a/elasticsearch-persistence/examples/music/suggester.rb b/elasticsearch-persistence/examples/music/suggester.rb new file mode 100644 index 000000000..7f14a33ef --- /dev/null +++ b/elasticsearch-persistence/examples/music/suggester.rb @@ -0,0 +1,45 @@ +class Suggester + attr_reader :response + + def initialize(params={}) + @term = params[:term] + end + + def response + @response ||= begin + Elasticsearch::Persistence.client.suggest \ + index: Artist.index_name, + body: { + artists: { + text: @term, + completion: { field: 'suggest_name' } + }, + members: { + text: @term, + completion: { field: 'suggest_member' } + }, + albums: { + text: @term, + completion: { field: 'suggest_title' } + }, + tracks: { + text: @term, + completion: { field: 'suggest_track' } + } + } + end + end + + def as_json(options={}) + response + .except('_shards') + .reduce([]) do |sum,d| + # category = { d.first => d.second.first['options'] } + item = { :label => d.first.titleize, :value => d.second.first['options'] } + sum << item + end + .reject do |d| + d[:value].empty? + end + end +end diff --git a/elasticsearch-persistence/examples/music/template.rb b/elasticsearch-persistence/examples/music/template.rb new file mode 100644 index 000000000..a6d80641e --- /dev/null +++ b/elasticsearch-persistence/examples/music/template.rb @@ -0,0 +1,392 @@ +# ====================================================================================== +# Template for generating a Rails application with support for Elasticsearch persistence +# ====================================================================================== +# +# This file creates a fully working Rails application with support for storing and retrieving models +# in Elasticsearch, using the `elasticsearch-persistence` gem +# (https://github.com/elasticsearch/elasticsearch-rails/tree/persistence-model/elasticsearch-persistence). +# +# Requirements: +# ------------- +# +# * Git +# * Ruby >= 1.9.3 +# * Rails >= 4 +# * Java >= 7 (for Elasticsearch) +# +# Usage: +# ------ +# +# $ time rails new music --force --skip --skip-bundle --skip-active-record --template /Users/karmi/Contracts/Elasticsearch/Projects/Clients/Ruby/elasticsearch-rails/elasticsearch-persistence/examples/music/template.rb +# +# ===================================================================================================== + +STDOUT.sync = true +STDERR.sync = true + +require 'uri' +require 'net/http' + +at_exit do + pid = File.read("#{destination_root}/tmp/pids/elasticsearch.pid") rescue nil + if pid + say_status "Stop", "Elasticsearch", :yellow + run "kill #{pid}" + end +end + +run "touch tmp/.gitignore" + +append_to_file ".gitignore", "vendor/elasticsearch-1.2.1/\n" + +git :init +git add: "." +git commit: "-m 'Initial commit: Clean application'" + +# ----- Download Elasticsearch -------------------------------------------------------------------- + +unless (Net::HTTP.get(URI.parse('http://localhost:9200')) rescue false) + COMMAND = <<-COMMAND.gsub(/^ /, '') + curl -# -O "http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.2.1.tar.gz" + tar -zxf elasticsearch-1.2.1.tar.gz + rm -f elasticsearch-1.2.1.tar.gz + ./elasticsearch-1.2.1/bin/elasticsearch -d -p #{destination_root}/tmp/pids/elasticsearch.pid + COMMAND + + puts "\n" + say_status "ERROR", "Elasticsearch not running!\n", :red + puts '-'*80 + say_status '', "It appears that Elasticsearch is not running on this machine." + say_status '', "Is it installed? Do you want me to install it for you with this command?\n\n" + COMMAND.each_line { |l| say_status '', "$ #{l}" } + puts + say_status '', "(To uninstall, just remove the generated application directory.)" + puts '-'*80, '' + + if yes?("Install Elasticsearch?", :bold) + puts + say_status "Install", "Elasticsearch", :yellow + + java_info = `java -version 2>&1` + + unless java_info.match /1\.[7-9]/ + puts + say_status "ERROR", "Required Java version (1.7) not found, exiting...", :red + exit(1) + end + + commands = COMMAND.split("\n") + exec = commands.pop + inside("vendor") do + commands.each { |command| run command } + run "(#{exec})" # Launch Elasticsearch in subshell + end + end +end unless ENV['RAILS_NO_ES_INSTALL'] + +# ----- Add README -------------------------------------------------------------------------------- + +puts +say_status "README", "Adding Readme...\n", :yellow +puts '-'*80, ''; sleep 0.25 + +remove_file 'README.rdoc' + +create_file 'README.rdoc', <<-README += Ruby on Rails and Elasticsearch persistence: Example application + +README + + +git add: "." +git commit: "-m 'Added README for the application'" + +# ----- Use Thin ---------------------------------------------------------------------------------- + +begin + require 'thin' + puts + say_status "Rubygems", "Adding Thin into Gemfile...\n", :yellow + puts '-'*80, ''; + + gem 'thin' +rescue LoadError +end + +# ----- Auxiliary gems ---------------------------------------------------------------------------- + +# ----- Remove CoffeeScript, Sass and "all that jazz" --------------------------------------------- + +comment_lines 'Gemfile', /gem 'coffee/ +comment_lines 'Gemfile', /gem 'sass/ +comment_lines 'Gemfile', /gem 'uglifier/ +uncomment_lines 'Gemfile', /gem 'therubyracer/ + +# ----- Add gems into Gemfile --------------------------------------------------------------------- + +puts +say_status "Rubygems", "Adding Elasticsearch libraries into Gemfile...\n", :yellow +puts '-'*80, ''; sleep 0.75 + +gem "quiet_assets" +gem "simple_form" + +gem 'elasticsearch', git: 'git://github.com/elasticsearch/elasticsearch-ruby.git' +gem 'elasticsearch-persistence', git: 'git://github.com/elasticsearch/elasticsearch-rails.git', branch: 'persistence-model', require: 'elasticsearch/persistence/model' +gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git' + +git add: "Gemfile*" +git commit: "-m 'Added libraries into Gemfile'" + +# ----- Install gems ------------------------------------------------------------------------------ + +puts +say_status "Rubygems", "Installing Rubygems...", :yellow +puts '-'*80, '' + +run "bundle install" + +# ----- Autoload ./lib ---------------------------------------------------------------------------- + +puts +say_status "Application", "Adding autoloading of ./lib...", :yellow +puts '-'*80, '' + +insert_into_file 'config/application.rb', + ' + config.autoload_paths += %W(#{config.root}/lib) + +', + after: 'class Application < Rails::Application' + +git commit: "-a -m 'Added autoloading of the ./lib folder'" + +# ----- Add jQuery UI ---------------------------------------------------------------------------- + +puts +say_status "Assets", "Adding jQuery UI...", :yellow +puts '-'*80, ''; sleep 0.25 + +if ENV['LOCAL'] + copy_file File.expand_path('../vendor/assets/jquery-ui-1.10.4.custom.min.js', __FILE__), + 'vendor/assets/javascripts/jquery-ui-1.10.4.custom.min.js' + copy_file File.expand_path('../vendor/assets/jquery-ui-1.10.4.custom.min.css', __FILE__), + 'vendor/assets/stylesheets/ui-lightness/jquery-ui-1.10.4.custom.min.css' +else + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js', + 'vendor/assets/javascripts/jquery-ui-1.10.4.custom.min.js' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css', + 'vendor/assets/stylesheets/ui-lightness/jquery-ui-1.10.4.custom.min.css' +end + +append_to_file 'app/assets/javascripts/application.js', "//= require jquery-ui-1.10.4.custom.min.js" + +git commit: "-a -m 'Added jQuery UI'" + +# ----- Generate Artist scaffold ------------------------------------------------------------------ + +puts +say_status "Model", "Generating the Artist scaffold...", :yellow +puts '-'*80, ''; sleep 0.25 + +generate :scaffold, "Artist name:String --orm=elasticsearch" +route "root to: 'artists#index'" + +git add: "." +git commit: "-m 'Added the generated Artist scaffold'" + +# ----- Generate Album model ---------------------------------------------------------------------- + +puts +say_status "Model", "Generating the Album model...", :yellow +puts '-'*80, ''; sleep 0.25 + +generate :model, "Album --orm=elasticsearch" + +git add: "." +git commit: "-m 'Added the generated Album model'" + +# ----- Add proper model classes ------------------------------------------------------------------ + +puts +say_status "Model", "Adding Album, Artist and Suggester models implementation...", :yellow +puts '-'*80, ''; sleep 0.25 + +if ENV['LOCAL'] + copy_file File.expand_path('../artist.rb', __FILE__), 'app/models/artist.rb' + copy_file File.expand_path('../album.rb', __FILE__), 'app/models/album.rb' + copy_file File.expand_path('../suggester.rb', __FILE__), 'app/models/suggester.rb' +else + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/artist.rb', + 'app/models/artist.rb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/album.rb', + 'app/models/album.rb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/suggester.rb', + 'app/models/suggester.rb' +end + +git add: "./app/models" +git commit: "-m 'Added Album, Artist and Suggester models implementation'" + +# ----- Add controllers and views ----------------------------------------------------------------- + +puts +say_status "Views", "Adding ArtistsController and views...", :yellow +puts '-'*80, ''; sleep 0.25 + +if ENV['LOCAL'] + copy_file File.expand_path('../artists/artists_controller.rb', __FILE__), 'app/controllers/artists_controller.rb' + copy_file File.expand_path('../artists/index.html.erb', __FILE__), 'app/views/artists/index.html.erb' + copy_file File.expand_path('../artists/show.html.erb', __FILE__), 'app/views/artists/show.html.erb' + copy_file File.expand_path('../artists/_form.html.erb', __FILE__), 'app/views/artists/_form.html.erb' + copy_file File.expand_path('../artists/artists_controller_test.rb', __FILE__), + 'test/controllers/artists_controller_test.rb' +else + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/artists/artists_controller.rb', + 'app/controllers/artists_controller.rb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/artists/index.html.erb', + 'app/views/artists/index.html.erb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/artists/show.html.erb', + 'app/views/artists/show.html.erb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/artists/_form.html.erb', + 'app/views/artists/_form.html.erb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb', + 'test/controllers/artists_controller_test.rb' +end + +git commit: "-a -m 'Added ArtistsController and related views'" + +puts +say_status "Views", "Adding SearchController and views...", :yellow +puts '-'*80, ''; sleep 0.25 + +if ENV['LOCAL'] + copy_file File.expand_path('../search/search_controller.rb', __FILE__), 'app/controllers/search_controller.rb' + copy_file File.expand_path('../search/search_helper.rb', __FILE__), 'app/helpers/search_helper.rb' + copy_file File.expand_path('../search/index.html.erb', __FILE__), 'app/views/search/index.html.erb' + copy_file File.expand_path('../search/search_controller_test.rb', __FILE__), + 'test/controllers/search_controller_test.rb' +else + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/search/search_controller.rb', + 'app/controllers/search_controller.rb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/search/search_helper.rb', + 'app/helpers/search_helper.rb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/search/index.html.erb', + 'app/views/search/index.html.erb' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/search/search_controller_test.rb', + 'test/controllers/search_controller_test.rb' +end + +route "get 'search', to: 'search#index'" +route "get 'suggest', to: 'search#suggest'" + +comment_lines 'test/test_helper.rb', /fixtures \:all/ + +git add: "." +git commit: "-m 'Added SearchController and related views'" + +# ----- Add assets ----------------------------------------------------------------- + +puts +say_status "Views", "Adding application assets...", :yellow +puts '-'*80, ''; sleep 0.25 + +git rm: 'app/assets/stylesheets/scaffold.css' + +gsub_file 'app/views/layouts/application.html.erb', //, '' + +if ENV['LOCAL'] + copy_file File.expand_path('../assets/application.css', __FILE__), 'app/assets/stylesheets/application.css' + copy_file File.expand_path('../assets/autocomplete.css', __FILE__), 'app/assets/stylesheets/autocomplete.css' + copy_file File.expand_path('../assets/form.css', __FILE__), 'app/assets/stylesheets/form.css' + copy_file File.expand_path('../assets/blank_cover.png', __FILE__), 'public/images/blank_cover.png' +else + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/assets/application.css', + 'app/assets/stylesheets/application.css' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/assets/autocomplete.css', + 'app/assets/stylesheets/autocomplete.css' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/assets/form.css', + 'app/assets/stylesheets/form.css' + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/assets/blank_cover.png', + 'public/images/blank_cover.png' +end + +git add: "." +git commit: "-m 'Added application assets'" + +# ----- Add an Elasticsearch initializer ---------------------------------------------------------- + +puts +say_status "Initializer", "Adding an Elasticsearch initializer...", :yellow +puts '-'*80, ''; sleep 0.25 + +initializer 'elasticsearch.rb', %q{ + Elasticsearch::Persistence.client = Elasticsearch::Client.new host: ENV['ELASTICSEARCH_URL'] || 'localhost:9200' + + if Rails.env.development? + logger = ActiveSupport::Logger.new(STDERR) + logger.level = Logger::INFO + logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" } + Elasticsearch::Persistence.client.transport.logger = logger + end +}.gsub(/^ /, '') + +git add: "./config" +git commit: "-m 'Added an Elasticsearch initializer'" + +# ----- Add IndexManager ----------------------------------------------------------------- + +puts +say_status "Application", "Adding the IndexManager class...", :yellow +puts '-'*80, ''; sleep 0.25 + +if ENV['LOCAL'] + copy_file File.expand_path('../index_manager.rb', __FILE__), 'lib/index_manager.rb' +else + get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/persistence-model/elasticsearch-persistence/examples/music/index_manager.rb', + 'lib/index_manager.rb' +end + +# TODO: get 'https://raw.github.com/...', '...' + +git add: "." +git commit: "-m 'Added the IndexManager class'" + +# ----- Import the data --------------------------------------------------------------------------- + +puts +say_status "Data", "Import the data...", :yellow +puts '-'*80, ''; sleep 0.25 + +source = ENV.fetch('DATA_SOURCE', 'http://ruby-demo-assets.s3.amazonaws.com/dischord.yml') + +run "rails runner 'IndexManager.import_from_yaml(\"#{source}\", force: true)'" + +# ----- Print Git log ----------------------------------------------------------------------------- + +puts +say_status "Git", "Details about the application:", :yellow +puts '-'*80, '' + +run "git --no-pager log --reverse --oneline" + +# ----- Start the application --------------------------------------------------------------------- + +unless ENV['RAILS_NO_SERVER_START'] + require 'net/http' + if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end) + puts "\n" + say_status "ERROR", "Some other application is running on port 3000!\n", :red + puts '-'*80 + + port = ask("Please provide free port:", :bold) + else + port = '3000' + end + + puts "", "="*80 + say_status "DONE", "\e[1mStarting the application.\e[0m", :yellow + puts "="*80, "" + + run "rails server --port=#{port}" +end diff --git a/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css b/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css new file mode 100755 index 000000000..672cea658 --- /dev/null +++ b/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.10.4 - 2014-06-04 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.autocomplete.css, jquery.ui.menu.css, jquery.ui.theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} \ No newline at end of file diff --git a/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js b/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js new file mode 100755 index 000000000..8af84cb1e --- /dev/null +++ b/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js @@ -0,0 +1,6 @@ +/*! jQuery UI - v1.10.4 - 2014-06-05 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.position.js, jquery.ui.autocomplete.js, jquery.ui.menu.js, jquery.ui.effect.js, jquery.ui.effect-highlight.js +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("
"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widths?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("