diff --git a/db/manifest b/db/manifest index 9f19d8fe7..e047ce7e2 100755 --- a/db/manifest +++ b/db/manifest @@ -77,4 +77,5 @@ whats_next.sql add_user_bio.sql users_geocoding.sql recordings_public_launch.sql -notification_band_invite.sql \ No newline at end of file +notification_band_invite.sql +bands_geocoding.sql diff --git a/db/up/bands_geocoding.sql b/db/up/bands_geocoding.sql new file mode 100644 index 000000000..cf612ac0e --- /dev/null +++ b/db/up/bands_geocoding.sql @@ -0,0 +1,4 @@ +ALTER TABLE bands ADD COLUMN lat NUMERIC(15,10); +ALTER TABLE bands ADD COLUMN lng NUMERIC(15,10); + +UPDATE bands SET country = 'US' WHERE country = 'USA'; diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index 5f47cdb9d..97200e21a 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -7,6 +7,8 @@ module JamRuby validates :biography, no_profanity: true + before_save :check_lat_lng + # musicians has_many :band_musicians, :class_name => "JamRuby::BandMusician" has_many :users, :through => :band_musicians, :class_name => "JamRuby::User" @@ -187,6 +189,28 @@ module JamRuby return band end + def check_lat_lng + if (city_changed? || state_changed? || country_changed?) + update_lat_lng + end + end + + def update_lat_lng + if self.city + query = { :city => self.city } + query[:region] = self.state unless self.state.blank? + query[:country] = self.country unless self.country.blank? + if geo = MaxMindGeo.where(query).limit(1).first + if geo.lat && geo.lng && (self.lat != geo.lat || self.lng != geo.lng) + self.lat, self.lng = geo.lat, geo.lng + return true + end + end + end + self.lat, self.lng = nil, nil + false + end + private def self.validate_genres(genres, is_nil_ok) if is_nil_ok && genres.nil? diff --git a/ruby/lib/jam_ruby/models/max_mind_geo.rb b/ruby/lib/jam_ruby/models/max_mind_geo.rb index 81868c4cd..713938552 100644 --- a/ruby/lib/jam_ruby/models/max_mind_geo.rb +++ b/ruby/lib/jam_ruby/models/max_mind_geo.rb @@ -40,7 +40,35 @@ module JamRuby end end User.find_each { |usr| usr.update_lat_lng } + Band.find_each { |bnd| bnd.update_lat_lng } end + + def self.where_latlng(relation, params, current_user=nil) + if 0 < (distance = params[:distance].to_i) + latlng = [] + if location_city = params[:city] + if geo = self.where(:city => params[:city]).limit(1).first + latlng = [geo.lat, geo.lng] + end + elsif current_user + if current_user.lat.nil? || current_user.lng.nil? + if params[:remote_ip] && (geo = self.ip_lookup(params[:remote_ip])) + latlng = [geo.lat, geo.lng] if geo.lat && geo.lng + end + else + latlng = [current_user.lat, current_user.lng] + end + elsif params[:remote_ip] && (geo = self.ip_lookup(params[:remote_ip])) + latlng = [geo.lat, geo.lng] if geo.lat && geo.lng + end + if latlng.present? + relation = relation.where(['lat IS NOT NULL AND lng IS NOT NULL']) + .within(distance, :origin => latlng) + end + end + relation + end + end end diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index bb4f5a1f7..3ab843f4a 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -93,14 +93,14 @@ module JamRuby M_ORDER_FOLLOWS = ['Most Followed', :followed] M_ORDER_PLAYS = ['Most Plays', :plays] M_ORDER_PLAYING = ['Playing Now', :playing] - B_ORDERINGS = M_ORDERINGS = [M_ORDER_FOLLOWS, M_ORDER_PLAYS, M_ORDER_PLAYING] + ORDERINGS = B_ORDERINGS = M_ORDERINGS = [M_ORDER_FOLLOWS, M_ORDER_PLAYS, M_ORDER_PLAYING] B_ORDERING_KEYS = M_ORDERING_KEYS = M_ORDERINGS.collect { |oo| oo[1] } - B_DISTANCE_OPTS = M_DISTANCE_OPTS = [['Any', 0], [1000.to_s, 1000], [500.to_s, 500], [250.to_s, 250], [100.to_s, 100], [50.to_s, 50], [25.to_s, 25]] + DISTANCE_OPTS = B_DISTANCE_OPTS = M_DISTANCE_OPTS = [['Any', 0], [1000.to_s, 1000], [500.to_s, 500], [250.to_s, 250], [100.to_s, 100], [50.to_s, 50], [25.to_s, 25]] - def self.musician_order_param(params) + def self.order_param(params, keys=M_ORDERING_KEYS) ordering = params[:orderby] - ordering.blank? ? M_ORDERING_KEYS[0] : M_ORDERING_KEYS.detect { |oo| oo.to_s == ordering } + ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering } end def self.musician_search(params={}, current_user=nil) @@ -110,32 +110,10 @@ module JamRuby .where(['minst.instrument_id = ? AND users.id IS NOT NULL', instrument]) end - if 0 < (location_distance = params[:distance].to_i) - location_city, distance, latlng = params[:city], nil, [] - if location_distance && location_city - if geo = MaxMindGeo.where(:city => params[:city]).limit(1).first - distance, latlng = location_distance, [geo.lat, geo.lng] - end - elsif current_user - if current_user.lat.nil? || current_user.lng.nil? - if params[:remote_ip] && (geo = MaxMindGeo.ip_lookup(params[:remote_ip])) - latlng = [geo.lat, geo.lng] if geo.lat && geo.lng - end - else - latlng = [current_user.lat, current_user.lng] - end - elsif params[:remote_ip] && (geo = MaxMindGeo.ip_lookup(params[:remote_ip])) - latlng = [geo.lat, geo.lng] if geo.lat && geo.lng - end - if latlng.present? - distance ||= location_distance || M_MILES_DEFAULT - rel = rel.where(['lat IS NOT NULL AND lng IS NOT NULL']) - .within(distance, :origin => latlng) - end - end + rel = MaxMindGeo.where_latlng(rel, params, current_user) sel_str = 'users.*' - case ordering = self.musician_order_param(params) + case ordering = self.order_param(params) when :plays # FIXME: double counting? sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") @@ -263,5 +241,77 @@ module JamRuby end end + def self.band_search(params={}, current_user=nil) + rel = Band.scoped + # rel = Arel::Table.new(:bands) + + unless (genre = params[:genre]).blank? + rel = Band.joins("RIGHT JOIN band_genres AS bgenres ON bgenres.band_id = bands.id") + .where(['bgenres.genre_id = ? AND bands.id IS NOT NULL', genre]) + end + + rel = MaxMindGeo.where_latlng(rel, params, current_user) + + sel_str = 'bands.*' + case ordering = self.order_param(params) + when :plays # FIXME: double counting? + sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" + rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") + .joins("LEFT JOIN recordings AS records ON records.owner_id = users.id") + .group("users.id") + .order("play_count DESC, users.created_at DESC") + when :followed + sel_str = "COUNT(follows) AS search_follow_count, #{sel_str}" + rel = rel.joins("LEFT JOIN bands_followers AS follows ON follows.band_id = bands.id") + .group("bands.id") + .order("COUNT(follows) DESC, bands.created_at DESC") + when :playing + rel = rel.joins("LEFT JOIN connections ON connections.user_id = users.id") + .where(['connections.music_session_id IS NOT NULL AND connections.aasm_state != ?', + 'expired']) + .order("users.created_at DESC") + end + + rel = rel.select(sel_str) + perpage = [(params[:per_page] || M_PER_PAGE).to_i, 100].min + page = [params[:page].to_i, 1].max + rel = rel.paginate(:page => page, :per_page => perpage) + rel = rel.includes([:users]) + + objs = rel.all + srch = Search.new + srch.page_num, srch.page_count = page, objs.total_pages + srch.band_results_for_user(objs, current_user) + end + + def band_results_for_user(results, user) + @search_type, @bands = PARAM_BAND, results + if user + @user_counters = results.inject({}) { |hh,val| hh[val.id] = []; hh } + mids = "'#{@bands.map(&:id).join("','")}'" + + # this gets counts for each search result + results.each do |bb| + counters = { } + counters[COUNT_FOLLOW] = BandFollowing.where(:band_id => bb.id).count + # counters[COUNT_RECORD] = ClaimedRecording.where(:band_id => bb.id).count + # counters[COUNT_SESSION] = MusicSession.where(:band_id => bb.id).count + @user_counters[bb.id] << counters + end + + # this section determines follow/like/friend status for each search result + # so that action links can be activated or not + + rel = Band.select("bands.id AS bid") + rel = rel.joins("LEFT JOIN bands_followers AS follows ON follows.follower_id = '#{user.id}'") + rel = rel.where(["bands.id IN (#{mids}) AND follows.band_id = bands.id"]) + rel.all.each { |val| @user_counters[val.bid] << RESULT_FOLLOW } + + else + @user_counters = {} + end + self + end + end end diff --git a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb new file mode 100644 index 000000000..b487890bb --- /dev/null +++ b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +describe 'Band search' do + + before(:each) do + @geocode1 = FactoryGirl.create(:geocoder) + @geocode2 = FactoryGirl.create(:geocoder) + @bands = [] + @bands << @band1 = FactoryGirl.create(:band) + @bands << @band2 = FactoryGirl.create(:band) + @bands << @band3 = FactoryGirl.create(:band) + @bands << @band4 = FactoryGirl.create(:band) + + @bands.each do |bb| + FactoryGirl.create(:band_musician, :band => bb, :user => FactoryGirl.create(:user)) + (rand(4)+1).downto(1) do |nn| + FactoryGirl.create(:band_musician, :band => bb, :user => FactoryGirl.create(:user)) + end + end + + end + + context 'default filter settings' do + + it "finds all bands" do + # expects all the bands + num = Band.count + results = Search.band_search({ :per_page => num }) + expect(results.bands.count).to eq(num) + end + + it "finds bands with proper ordering" do + # the ordering should be create_at since no followers exist + expect(BandFollower.count).to eq(0) + results = Search.band_search({ :per_page => Band.count }) + results.bands.each_with_index do |uu, idx| + expect(uu.id).to eq(@bands.reverse[idx].id) + end + end + + it "sorts bands by followers" do + users = [] + 4.downto(1) do |nn| + users << FactoryGirl.create(:user) + end + + # establish sorting order + @band4.followers.concat(users[1..-1]) + @band3.followers.concat(users[1..3]) + @band2.followers.concat(users[0]) + @bands.map(&:reload) + + expect(@band4.followers.count).to be 3 + expect(BandFollower.count).to be 7 + + # refresh the order to ensure it works right + @band2.followers.concat(users[1..-1]) + results = Search.band_search({ :per_page => @bands.size }, users[0]) + expect(results.bands[0].id).to eq(@band2.id) + + # check the follower count for given entry + expect(results.bands[0].search_follow_count.to_i).not_to eq(0) + # check the follow relationship between current_user and result + expect(results.is_follower?(@band2)).to be true + end + + it 'paginates properly' do + pending + # make sure pagination works right + params = { :per_page => 2, :page => 1 } + results = Search.band_search(params) + expect(results.bands.count).to be 2 + end + + end + + def make_recording(usr) + connection = FactoryGirl.create(:connection, :band => usr) + instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + track = FactoryGirl.create(:track, :connection => connection, :instrument => instrument) + music_session = FactoryGirl.create(:music_session, :creator => usr, :band_access => true) + music_session.connections << connection + music_session.save + recording = Recording.start(music_session, usr) + recording.stop + recording.reload + genre = FactoryGirl.create(:genre) + recording.claim(usr, "name", "description", genre, true, true) + recording.reload + recording + end + + def make_session(usr) + connection = FactoryGirl.create(:connection, :band => usr) + music_session = FactoryGirl.create(:music_session, :creator => usr, :band_access => true) + music_session.connections << connection + music_session.save + end + +=begin + context 'band stat counters' do + + it "displays bands top followings" do + pending + @band4.followers.concat([@band4]) + @band3.followers.concat([@band4]) + @band2.followers.concat([@band4]) + expect(@band4.top_followings.count).to be 3 + expect(@band4.top_followings.map(&:id)).to match_array((@bands - [@band1]).map(&:id)) + end + + it "recording stat shows recording count" do + pending + recording = make_recording(@band1) + expect(recording.bands.length).to be 1 + expect(recording.bands.first).to eq(@band1) + @band1.reload + expect(@band1.recordings.length).to be 1 + expect(@band1.recordings.first).to eq(recording) + expect(recording.claimed_recordings.length).to be 1 + expect(@band1.recordings.detect { |rr| rr == recording }).to_not be_nil + + results = Search.band_search({},@band1) + uu = results.bands.detect { |mm| mm.id == @band1.id } + expect(uu).to_not be_nil + + expect(results.record_count(uu)).to be 1 + expect(results.session_count(uu)).to be 1 + end + + end + + context 'band sorting' do + + it "by plays" do + pending + make_recording(@band1) + # order results by num recordings + results = Search.band_search({ :orderby => 'plays' }, @band2) + expect(results.bands[0].id).to eq(@band1.id) + + # add more data and make sure order still correct + make_recording(@band2); make_recording(@band2) + results = Search.band_search({ :orderby => 'plays' }, @band2) + expect(results.bands[0].id).to eq(@band2.id) + end + + it "by now playing" do + pending + # should get 1 result with 1 active session + make_session(@band3) + results = Search.band_search({ :orderby => 'playing' }, @band2) + expect(results.bands.count).to be 1 + expect(results.bands.first.id).to eq(@band3.id) + + # should get 2 results with 2 active sessions + # sort order should be created_at DESC + make_session(@band4) + results = Search.band_search({ :orderby => 'playing' }, @band2) + expect(results.bands.count).to be 2 + expect(results.bands[0].id).to eq(@band4.id) + expect(results.bands[1].id).to eq(@band3.id) + end + + end + + context 'filter settings' do + it "searches musicisns for an instrument" do + pending + minst = FactoryGirl.create(:band_instrument, { + :band => @band1, + :instrument => Instrument.find('tuba') }) + @band1.band_instruments << minst + @band1.reload + ii = @band1.instruments.detect { |inst| inst.id == 'tuba' } + expect(ii).to_not be_nil + results = Search.band_search({ :instrument => ii.id }) + results.bands.each do |rr| + expect(rr.instruments.detect { |inst| inst.id=='tuba' }.id).to eq(ii.id) + end + expect(results.count).to be 1 + end + + it "finds bands within a given distance of given location" do + pending + num = Band.count + expect(@band1.lat).to_not be_nil + # short distance + results = Search.band_search({ :per_page => num, + :distance => 10, + :city => 'Apex' }, @band1) + expect(results.count).to be num + # long distance + results = Search.band_search({ :per_page => num, + :distance => 1000, + :city => 'Miami', + :state => 'FL' }, @band1) + expect(results.count).to be num + end + + it "finds bands within a given distance of bands location" do + pending + expect(@band1.lat).to_not be_nil + # uses the location of @band1 + results = Search.band_search({ :distance => 10, :per_page => Band.count }, @band1) + expect(results.count).to be Band.count + end + + it "finds no bands within a given distance of location" do + pending + expect(@band1.lat).to_not be_nil + results = Search.band_search({ :distance => 10, :city => 'San Francisco' }, @band1) + expect(results.count).to be 0 + end + + end +=end +end diff --git a/ruby/spec/jam_ruby/models/band_location_spec.rb b/ruby/spec/jam_ruby/models/band_location_spec.rb new file mode 100644 index 000000000..fae562a53 --- /dev/null +++ b/ruby/spec/jam_ruby/models/band_location_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Band do + + before do + @geocode1 = FactoryGirl.create(:geocoder) + @geocode2 = FactoryGirl.create(:geocoder) + @band = FactoryGirl.create(:band) + end + + describe "with profile location data" do + it "should have lat/lng values" do + geo = MaxMindGeo.find_by_city(@band.city) + @band.lat.should == geo.lat + @band.lng.should == geo.lng + end + it "should have updated lat/lng values" do + @band.update_attributes({ :city => @geocode2.city, + :state => @geocode2.region, + :country => @geocode2.country, + }) + geo = MaxMindGeo.find_by_city(@band.city) + @band.lat.should == geo.lat + @band.lng.should == geo.lng + end + end + + describe "without location data" do + it "should have nil lat/lng values without address" do + @band.update_attributes({ :city => nil, + :state => nil, + :country => nil, + }) + @band.lat.should == nil + @band.lng.should == nil + end + end + +end diff --git a/ruby/spec/jam_ruby/models/band_search_spec.rb b/ruby/spec/jam_ruby/models/band_search_spec.rb index 2af217b3f..ce9ca8767 100644 --- a/ruby/spec/jam_ruby/models/band_search_spec.rb +++ b/ruby/spec/jam_ruby/models/band_search_spec.rb @@ -5,7 +5,7 @@ describe User do let(:user) { FactoryGirl.create(:user) } before(:each) do - + @user = FactoryGirl.create(:user) @band = Band.save(nil, "Example Band", "www.bands.com", "zomg we rock", "Apex", "NC", "US", ["hip hop"], user.id, nil, nil) end diff --git a/web/app/views/clients/_bands.html.erb b/web/app/views/clients/_bands.html.erb index 8505890b1..417f3446d 100644 --- a/web/app/views/clients/_bands.html.erb +++ b/web/app/views/clients/_bands.html.erb @@ -1,13 +1,16 @@ -
-
- -
- <%= image_tag "content/icon_bands.png", {:height => 19, :width => 19} %> -
- -

bands

- <%= render "screen_navigation" %> -
-

This feature not yet implemented

-
+<%= content_tag(:div, :layout => 'screen', 'layout-id' => 'bands', :class => "screen secondary") do -%> + <%= content_tag(:div, :class => :content) do -%> + <%= content_tag(:div, :class => 'content-head') do -%> + <%= content_tag(:div, image_tag("content/icon_bands.png", {:height => 19, :width => 19}), :class => 'content-icon') %> + <%= content_tag(:h1, 'bands') %> + <%= render "screen_navigation" %> + <% end -%> + <%= form_tag('', :id => 'find-band-form') do -%> + <%= content_tag(:div, render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_BAND}), :class => 'band-filter', :id => 'session-controls') %> + <%= content_tag(:div, :class => 'content-scroller') do -%> + <%= content_tag(:div, content_tag(:div, '', :id => 'band-filter-results'), :class => 'content-wrapper band-wrapper') %> + <% end -%> + <% end -%> + <% end -%> +<% end -%> diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb index 3d3e25793..973b93880 100644 --- a/web/app/views/clients/_musicians.html.erb +++ b/web/app/views/clients/_musicians.html.erb @@ -7,7 +7,7 @@ <%= render "screen_navigation" %> <% end -%> <%= form_tag('', :id => 'find-musician-form') do -%> - <%= content_tag(:div, render(:partial => "musician_filter"), :class => 'musician-filter', :id => 'session-controls') %> + <%= content_tag(:div, render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_MUSICIAN}), :class => 'musician-filter', :id => 'session-controls') %> <%= content_tag(:div, :class => 'content-scroller') do -%> <%= content_tag(:div, content_tag(:div, '', :id => 'musician-filter-results'), :class => 'content-wrapper musician-wrapper') %> <% end -%> diff --git a/web/app/views/clients/_web_filter.html.erb b/web/app/views/clients/_web_filter.html.erb new file mode 100644 index 000000000..16568bfe6 --- /dev/null +++ b/web/app/views/clients/_web_filter.html.erb @@ -0,0 +1,36 @@ +<% filter_label = defined?(search_type) && search_type == Search::PARAM_BAND ? :band : :musician %> +<%= content_tag(:div, :style => "min-width:770px;") do -%> + <%= content_tag(:div, :class => 'filter-element') do -%> + <%= content_tag(:div, 'Filter By:', :class => 'filter-element', :style => "padding-top:3px;") %> + + <%= select_tag("#{filter_label}_order_by", options_for_select(Search::ORDERINGS), {:class => "#{filter_label}-order-by"} ) %> + <% end -%> + <%= content_tag(:div, :class => 'filter-element') do -%> + <% if :musician == filter_label %> + + <%= content_tag(:div, 'Instrument:', :class => 'filter-element') %> + <%= content_tag(:div, :class => 'filter-element') do -%> + <%= select_tag("#{filter_label}_instrument", + options_for_select([['Any', '']].concat(JamRuby::Instrument.all.collect { |ii| [ii.description, ii.id] }))) %> + <% end -%> + <% elsif :band == filter_label %> + + <%= content_tag(:div, 'Genre:', :class => 'filter-element') %> + <%= content_tag(:div, :class => 'filter-element') do -%> + <%= select_tag("#{filter_label}_genre", + options_for_select([['Any', '']].concat(JamRuby::Genre.all.collect { |ii| [ii.description, ii.id] }))) %> + <% end -%> + <% end %> + <% end -%> + + <%= content_tag(:div, :class => 'filter-element') do -%> + <%= content_tag(:div, 'Within', :class => 'filter-element') %> + <%= content_tag(:div, :class => 'query-distance-params') do -%> + <% default_distance = :musician == filter_label ? Search::M_MILES_DEFAULT : Search::B_MILES_DEFAULT %> + <%= select_tag("#{filter_label}_query_distance", options_for_select(Search::DISTANCE_OPTS, default_distance)) %> + <% end -%> + <%= content_tag(:div, :class => 'filter-element') do -%> + miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => "#{filter_label}-filter-city") %> + <% end -%> + <% end -%> +<% end -%>